Debugging Nachos


There are two kinds of debugger you can use on Nachos: external and internal. An external debugger allows you to interact with your program as it runs. GDB is an example of an external debugger. An internal debugger is something you build into the program; basically, it's the set of print routines you put into your program to generate diagnostic output, perhaps with some facility for controlling the level of output.

Internal Debugging in Nachos

Nachos includes three internal debugging tools: a trace facility, a simple, single-step execution mode, and an assertion facility. The trace facility is useful for understanding the flow of control through the Nachos code. It can also be very useful as a demonstration tool, since well-designed trace output can provide lots of information about what your program is doing. The single-step mode is primarily useful for looking at the details of the MIPS simulation. The assertion facility is a useful way to identify serious errors in your code that might otherwise manifest themselves in strange ways.

The Trace Facility

Each debugging message printed by the trace facility has a type. You can control which types of messages are actually printed using command line arguments to Nachos. The -d flag controls this. For example:

nachos -d td
will print debugging messages related to threads ("t") and the disk ("d") emulation. If you do not use -d, the trace facility will be silent. Using -d + will cause trace messages of all types to be printed. Remember, however, that one of the nice features of this facility is that you can focus on only those messages that you are interested in. Having to wade through reams of irrelevant trace output is almost as bad as not having any trace output at all.

It is a good idea to extend this trace facility as you work on the project. You can add new types of messages very easily by editing the file code/lib/debug.h. For example, you could add a "system call" type, that prints a message each time your operating system handles a system call.

Single Stepping

The single-step facility is enabled by the -s command-line argument. It causes the machine simulation to single-step through instruction execution. After each instruction is executed, the machine will print the values of all of its registers, as well as the current simulated time. It will also print a prompt. Your options are to execute one more instruction, to execute instructions until a specified time is reached, or to exit single-step mode and run without further interruption. Most of the time, you will find that you do not need this facility. However, those interested in the details of the machine emulation may wish to play with it.

Assertions

The assertion facility allows you to state explicitly things that you believe will be true at a certain point in your program's execution. The assertion is tested and if it is found to be false, the program is terminated and a message is printed indicating which assertion failed. For example, the following piece of code can be found the Nachos main function:

        myId = kernel->procTable->GetNewProcId(-1);
        ASSERT(myId >= 0);
Here, the assertion is that a valid process identifier was, in fact, returned by the call to GetNewProcId. Note that ASSERT should be used to describe conditions that you expect to be true, and for which no graceful recovery exists for your program if they are false.

External Debugger

An external debugger is an invaluable tool for diagnosing and fixing problems in Nachos. If you are not using one, you are probably wasting lots of your time. Or perhaps your code is always right the first time...

GDB is a good debugger to use with Nachos. However, if you have another favorite, by all means use it. The remainder of this document is a brief introduction to some useful GDB commands, with Nachos-specific examples. It is intended for those who have never used GDB.

Using GDB with Nachos

To run Nachos under control of GDB, you first have to start GDB, specifying the program you want to debug. Assuming that you are in the same directory as the Nachos executable, you do this:
cpu02[CS350][166] gdb nachos
GNU gdb 5.3
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "sparc-sun-solaris2.8"...
(gdb) 
The (gdb) at the end is GDB's prompt, indicating that you can enter a GDB command. Suppose that you wish to have Nachos run the halt user program and that you'd like to examine what happens when the register values for that program are being initialized. You could do this as follows:
(gdb) break AddrSpace::InitRegisters
Breakpoint 1 at 0x252ec: file ../userprog/addrspace.cc, line 234.
(gdb) run -x ../test/halt
Starting program: /u/kmsalem/nachos/code/build.solaris/nachos -x ../test/halt

Breakpoint 1, AddrSpace::InitRegisters (this=0x51e50)
    at ../userprog/addrspace.cc:234
234         Machine *machine = kernel->machine;
(gdb)
The break command tells GDB to halt Nachos execution at a specified function or line number in your code; in this case, at the beginning of the AddrSpace::InitRegisters method, which initializes the registers. The run command tells GDB to run nachos. Any parameters you give to the run command are passed to nachos by GDB, so you should use exactly the same arguments you would have used if running nachos directly from the shell.

The running nachos program eventually makes a call to AddrSpace::InitRegisters. When this happens, GDB freezes nachos, prints out a message saying why and where it is breaking (in this case, at line 234 in the addrspace.cc file, which is the beginning of the AddrSpace::InitRegisters function), and then provides a prompt.

At this point, you can do many things. You can examine the values of program variables. You can look at the call stack to find out how the program reached this particular call to AddrSpace::InitRegisters. You can step through the execution of the method one line of code at a time, or you can tell GDB to continue executing the program until another breakpoint is reached. You can even modify program variables using GDB.

Perhaps you would like to step through the method one statement at a time. You might do something like this:

(gdb) list 230,255
230     
231     void
232     AddrSpace::InitRegisters()
233     {
234         Machine *machine = kernel->machine;
235         int i;
236     
237         for (i = 0; i < NumTotalRegs; i++)
238             machine->WriteRegister(i, 0);
239     
240         // Initial program counter -- must be location of "Start"
241         machine->WriteRegister(PCReg, 0);   
242     
243         // Need to also tell MIPS where next instruction is, because
244         // of branch delay possibility
245         // Since instructions occupy four bytes each, the next instruction
246         // after start will be at virtual address four.
247         machine->WriteRegister(NextPCReg, 4);
248     
249        // Set the stack register to the end of the address space, where we
250        // allocated the stack; but subtract off a bit, to make sure we don't251        // accidentally reference off the end!
252         machine->WriteRegister(StackReg, numPages * PageSize - 16);
253         DEBUG(dbgAddr, "Initializing stack pointer: " << numPages * PageSize - 16);
254     }
255     
(gdb) next
237         for (i = 0; i < NumTotalRegs; i++)
(gdb) next
238             machine->WriteRegister(i, 0);
(gdb) print i
$1 = 0
(gdb) next
238             machine->WriteRegister(i, 0);
(gdb) next
238             machine->WriteRegister(i, 0);
(gdb) next
238             machine->WriteRegister(i, 0);
(gdb) print i
$2 = 3
(gdb) break 241
Breakpoint 2 at 0x25344: file ../userprog/addrspace.cc, line 241.
(gdb) continue
Continuing.

Breakpoint 2, AddrSpace::InitRegisters (this=0x51e50)
    at ../userprog/addrspace.cc:241
241         machine->WriteRegister(PCReg, 0);   
(gdb) 
The list command prints out the source code between lines 230 and 255. This just lets you see what is coming. (Another good way to see the source code is to run an editor alongside GDB.) Then, a sequence of next commands cause the method to be executed one line at a time. Each next command executes one line and then displays the following line (which would be executed by the next next). The print command can be used to display the values of program variables. The InitRegisters method is setting register values in a loop. After single-stepping through the first few loop iterations, you may decide you've seen enough. As shown above, you might set a breakpoint at a line after the loop using the break command, and then tell GDB to continue. This tells GDB to allow the nachos program to run until the next breakpoint is reached. When execution reaches line 241, which is the next breakpoint, GDB returns control to you.

Another very useful command is bt, which produces a stack backtrace. This is a how you find out the sequence of function calls that got you where you are. For example, generating a backtrace here would result in:

(gdb) bt
#0  AddrSpace::InitRegisters (this=0x51e50) at ../userprog/addrspace.cc:241
#1  0x00022144 in main (argc=3, argv=0xffbefa34) at ../threads/main.cc:359
(gdb) 
In the trace, each stack frame is numbered, with the latest (innermost) frame numbered zero, and shown at the top. In this example, we see that AddrSpace::InitRegisters was called directly from main. The stack trace will also show you the values of all of the arguments of each procedure in the stack.

Another nice feature of GDB is that it will freeze your program and give you a prompt if your program fails, e.g., with a segmentation fault or some other error. The backtrace command is very helpful at this point, since it will show you exactly what your program was doing when it died.

At this point, perhaps you have learned all you want to know about AddrSpace::InitRegisters, and you'd like to have the program run to completion (or at least until another breakpoint is hit). The "continue" command will do this:

(gdb) continue
Continuing.
Machine halting!

Ticks: total 22, idle 0, system 10, user 12
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: faults 0
Network I/O: packets received 0, sent 0

Program exited normally.
(gdb) 
After the nachos program terminates, you get another GDB prompt. You could, for example, run nachos again using another run command. If you are finished, type quit to exit GDB.

That is about it. The way to learn more is to play with GDB, and to read the on-line command documentation. Typing help at the GDB prompt will give you a list of classes of GDB commands. Typing help followed by a specific command class will give you information about specific commands in that class. help followed by a command name will give you details about a particular command. You can also check out the GDB man page (man gdb), which includes a small list of suggested useful commands.

One Warning

For GDB to work properly, all of your Nachos code should be compiled with the -g option to g++. The Nachos makefiles are set up to do this. If you change this, you will find that GDB will be unable to find variables and methods in your code.
$Id: debug.html,v 1.2 1999/05/02 18:33:20 cs350 Exp $