Tracing and Debugging Nachos Programs

There are at least three ways to trace execution: (1) add printf() statements to the code, (2) use the xxgdb debugger or another debugger of your choosing, and (3) insert calls to the DEBUG() function that nachos provides.

Many people debug with printfs because any idiot can do it, whereas even smart people need to spend a few hours learning to use a debugger. However, investing those few hours will save you many more hours of debugging time that could be better spent watching TV. Printfs can be useful, but be aware that printfs do not always work right, because data is not always printed synchronously with the call to printf(). Rather, printf buffers ("saves up") printed characters in memory, and writes the output only when it has accumulated enough to justify the cost of invoking the write system call. If your program crashes while characters are still in the printf buffer, then you may never see those messages print. If you use printf, it is good practice to follow every printf with a call to fflush(stdout) to avoid this problem.

If you want to debug with print statements, the nachos DEBUG() function (declared in threads/utility.h) is your best bet. In fact, the Nachos code is already peppered with calls to the DEBUG function. You can see some of them by doing an fgrep DEBUG *h *cc in the threads subdirectory. These are basically print statements that keep quiet unless you want to hear what they have to say. By default, these statements have no effect at runtime. To see what's going on, you need to invoke nachos with a special command-line argument that activates the DEBUG statements you want to see.

See main.cc for a specification of the flags to the nachos command. The relevant one for DEBUG is -d. The -d flag followed by a space and a series of debug flags cause the DEBUG statements in nachos with those debug flags to be printed when they are executed. For example, the the t debug flag activates the DEBUG statements in the threads directory. The machine subdirectory has some DEBUG statements with the i and m debug flags (see threads/utility.h for a description of the meanings of the current debug flags (there are no current DEBUG statements with the s debug flag). Feel free to add new debug flag values of your own.

For a quick peek at what's going on, run nachos -d ti to activate the DEBUG statements in threads and machine. If you want to know more, add some some more DEBUG statements. For each of the problems assigned below I suggest you sprinkle your code liberally with DEBUG() statements.

Miscellaneous Debugging Tips

The ASSERT() function, also declared in threads/utility.h, is extremely useful in debugging, particularly for concurrent code. Use ASSERT to indicate that certain conditions should be true at runtime. If the condition is not true (i.e., the expression evaluates to 0), then your program will print a message and crash right there before things get messed up further. ASSERT early and often! ASSERTs help to document your code as well as exposing bugs early.

When you trace the execution path, it is helpful to keep track of the state of each thread and which procedures are on each thread's execution stack. You will notice that when one thread calls SWITCH, another thread starts running, and the first thing the new thread does is to return from SWITCH. This is because of the way context switches work in Nachos. Because gdb and xxgdb do not understand threads, tracing in xxgdb across a call to SWITCH might be confusing sometimes.

Warning: in our implementation of threads, each thread is assigned a small, fixed-size execution stack. This may cause bizarre problems (such as segmentation faults at strange lines of code) if you declare large data structures to be automatic variables (e.g., int buf[1000];). You will probably not notice this during the semester, but if you do, you may change the size of the stack by modifying the StackSize define in switch.h.


Jeff Chase