Some Thoughts on Debugging Support

James L. Peterson

July 1996

Since the conference call on debugging support for multiprocessor systems (31 July), I have been thinking of some problems that I have with debugging even for uniprocessor systems and ways that the compiler could help.

First, I view the purpose of debugging to be one of "understanding" the execution of the program. A problem arises where my view of what the program does is not consistent with what the program actually does and the debugging problem is trying to understand where my view of what the program does is different from reality. Thus I am trying to understand what the program actually does. This problem will, of course, get much worse with multiple processors, because of the increased complexity of what can happen.

Typically the program gets into a state which is undesirable. The problem is a bug if I don't believe that it should be possible for it to get into that state, and the problem then becomes one of determining both the state that it is in and how it got there. One very helpful way to do that is to use a debugger (like dbx) to determine where the program is, and the values of program variables. This shows what the state is, but not (other than the stack trace), how we got there. It would be useful here to be able to back the program up, following back in its execution to see what it did to get to the undesired state. It would be possible (and might be very interesting) to build a system that would actually let a programmer execute backwards, but there is a simpler solution.

I have found it very useful to be able to produce a trace of the forward execution of a program. By putting this in a log file, I can then trace the program execution backwards by going to the end of the log and working my way back until I find where my view of the program and reality diverged. There are two levels of trace that are useful. For some programs, and for identifying roughly what is going on, a trace of procedure entry and exit is useful. I can get such a trace by running a transformation over my source program, adding a call to "enterprocedure(name)" at the beginning of each procedure and a "leaveprocedure(name)" at the end. The enterprocedure is fairly easy to do, although there are problems with automatic variables initialized by expressions involving function calls. The leaveprocedure is more difficult, because you actually need to catch both the last statement of the routine and any/all return or exit statements. Someone suggested a way to do this with clever macro call usage, but I haven't tried it yet.

The name parameter is the name of the procedure that is entered/exited and can be printed to form a trace of procedure call activity. If both entry and exit can be correctly traced, the enter/leave procedures can keep a procedure call stack, and potentially could do both tracing and dynamic function call statistics (but that's a different problem).

In any case, doing this, on a large program, is a real pain, and it appears that it could be very easily done by the compiler without a need for source code modification, as is currently needed. In addition, it should be fairly easy for the compiler to generate both a print (or function call) of procedure entry/exit and a formatted list of parameter values. That is rather than simply printing that function check was entered and left, the parameters could be printed in a reasonable format. This is really hard to do now because printing the parameters automatically requires knowledge of the type of the parameter which is hard to gather with a transformation program, but is exactly what the compiler has. With knowledge of the type, default value printing (such as dbx does -- printing an int with %d, a char * with %s, a pointer with 0x%X, etc.) is a reasonable task.

A more verbose level of tracing can also be provided by the compiler. When I was a student, the AlgolW compiler could generate a statement by statement trace, giving the source language string which was executed, alone with the values of variables which were referenced and the values of variables which were computed and stored. Thus, for example, at its extreme, it would print

hex.c,140:		    if (n > 2)
	n: 80
hex.c,141:			DesiredLineLength = n;
	n: 80
	80 => DesiredLineLength

for the execution of the corresponding source code. This level of trace can be generated automatically by the compiler, although obviously at a high cost in code size and speed. However, it can be invaluable in understanding the execution of a program, particularly for the beginning programmer.

There are problems with this approaches, of course. One obvious problem is the size of the traces that may be generated. With available disk storage and editors that can handle large files, however, I have found this is not a problem. With magnetic media and display tubes, an application can be run, megabytes of trace can be generated, examined, the bug found, and the trace deleted with very little cost. The more serious problem is when the compiler generates different code with and without the trace support, making the bug appear and disappear.

While these compiler supported trace options are valuable for debugging single applications, they can also be valuable for multi-processor applications. For multi-processors, it is needed only to tag each trace with the processor for that trace and to be able to merge the traces, either as they are generated (using a single output stream, for example), or after the fact (by time-stamping the trace lines and sort/merging by time-stamp). This trace capability may be the best way to find the race conditions and scheduling problems that will arise with multi-processor applications.

In summary, I would suggest:

The compiler should be modified to optionally generate trace output, at the source level of:

  1. procedure entry (with arguments) and exit (with values).

  2. at the source statement, with referenced and computed values.

This can be done much more effectively by the compiler than by source level program transformation. This would be a great help in debugging, and particularly with program education situations (learning to program or how a program works). It can then be extended to the multi-processor environment.