Debugging

Steven Zeil

Last modified: Aug 24, 2023
Contents:

Your program is producing incorrect output, or perhaps has crashed with no output at all. How do you find out why?

There’s no easy answer to that. Debugging is hard work, and is as much an art as an engineering process. Basically, though, debugging requires that we reason backwards from the symptom (the incorrect output or behavior) towards possible causes in our code. This may require a chain of several steps:

Example

Hmmm. The program crashed near the end of this loop:

for (int i = 0; i < numElements; ++i)
{
cout << myArray[i] << endl;
}
myArray[0] = myArray[1];

Each time around the loop, the code prints myArray[i]. One possible reason for such a crash would be if i was outside the legal range of index values for that array. Now, what could cause that? Maybe the loop exit condition is incorrect. Maybe the array is too small. Maybe we counted the number of items in the array incorrectly.

As we work backwards, we form hypotheses about what might be going wrong, for example, “i was outside the legal range of index values for that array”.

An integral part of debugging is testing those hypotheses to see if we are on the right track. This often involves seeking additional information about the execution that was not evident from the original program output, such as how often the loop is executed, whether we actually exited the loop, what values of i or numElements were employed just before the crash.

An automated debugger can help us in these endeavors. Such a debugging tool typically allows us to set breakpoints at different locations in the code, so that if execution reaches one of those locations, the execution will pause. Debuggers also allow us to examine the program state, printing out the values of variables and the history of function calls that brought us to the current location. Debuggers typically let us step through the code, one statement at a time, so that we can watch the execution at a very fine level.

An automatic debugger is a powerful tool for aiding our reasoning process during debugging. It can be invaluable in those frustrating cases where the program crashes without any output at all, as the debugger will usually take us right to the location where the crash occurred.

Automatic debuggers can also be a tremendous waste of time, however. It’s all too tempting to single-step through the code aimlessly, hoping to notice when something goes wrong. Debuggers are best used to augment the reasoning process, not as a substitute for it. In that vein, it’s worth noting first the alternatives to automated debuggers (even if that takes us somewhat beyond the scope of a “Unix” course):

1 Debugging Output

One of the easiest ways to gather information about what’s happening in a program is to add output statements to the code. These can show the values of key variables and also serve as documentation of what statements in the code were actually reached during execution. In our example above, the easiest way to test our hypothesis about i going out of bounds would be to alter our code like this:

for (int i = 0; i < numElements; ++i)
{
   cerr << "i: " << i << endl;
   cout << myArray[i] << endl;
}
cerr << "Exited loop" << endl;
myArray[0] = myArray[1];

Note that we send the debugging information, not to the standard output, but to the standard error stream. This may or may not be significant, but in many programs standard output may be redirected to files or to other programs, and we would not want to introduce new complications by having this unanticipated extra output included in that redirection.

This solution is not perfect, however. For one thing, we need to remember that this extra output is not acceptable in the final version of the code and must be removed. Actually removing the debugging output statements may not be a good idea anyway. In my experience, I have often removed debugging output statements after fixing one bug, only to discover another bug and wish that I had the same output available once more. So, I seldom remove debugging output, preferring instead to comment it out:

for (int i = 0; i < numElements; ++i)
{
  // cerr << "i: " << i << endl;
   cout << myArray[i] << endl;
}
// cerr << "Exited loop" << endl;
myArray[0] = myArray[1];

But, if we wind up with lots of debugging output like this in our program, we may have to hunt to find and remove it all before turning in our final program. A better solution is to use conditional compilation:

for (int i = 0; i < numElements; ++i)
{
#ifdef DEBUG
   cerr << "i: " << i << endl;
#endif
   cout << myArray[i] << endl;
}
#ifdef DEBUG
cerr << "Exited loop" << endl;
#endif
myArray[0] = myArray[1];

Now our debugging output will only be compiled only if the compile-time symbol DEBUG is set. This can be done by defining it at the start of the file (or in a .h file that is #include’d by this one):

#define DEBUG 1

or by defining it when we compile the code:

g++ -g -c -DDEBUG myProgram.cpp

For the final program submission, we simply omit these definitions of DEBUG, thereby turning off all our debugging output at once.

Another possibility is to use the macro facilities of C and C++ to define special debugging commands that, again, are active only when DEBUG is defined: For example, I sometimes have a header file named debug.h:

#ifdef DEBUG

#define dbgprint(x) cerr << #x << ": " << x << endl
#define dbgmsg(message) cerr << message << endl

#else

#define dbgprint(x)
#define dbgmsg(message)

#endif

These are macros. The C/C++ compiler will now replace any strings of the form dbgprint(...) and dbgmsg(...) in your source code by the rest of the macro before actually compiling your code.1

So we can then write:

#include "debug.h"
  ⋮
for (int i = 0; i < numElements; ++i)
{
   dbgprint(i);
   cout << myArray[i] << endl;
}
dbgmsg("Exited loop");
myArray[0] = myArray[1];

If we compile that code this way:


g++ -g -c -DDEBUG myProgram.cpp

then what actually gets compiled is

#include "debug.h"
  ⋮
for (int i = 0; i < numElements; ++i)
{
  cerr << "i" << ": " << i << endl;
   cout << myArray[i] << endl;
}
cerr << "Exited loop" << endl;
myArray[0] = myArray[1];

but if we compile the code this way:

g++ -g -c myProgram.cpp

then what actually gets compiled is

#include "debug.h"
  ⋮
for (int i = 0; i < numElements; ++i)
{
  ;
   cout << myArray[i] << endl;
}
;
myArray[0] = myArray[1];

Again, you can see that we can turn our debugging output on and off at will.

One final refinement worth noting. When you have a large program with many source code files, it’s easy to get confused about which debugging output lines are coming from where. C and C++ define two special macros, __FILE__ will be replaced by the name of the file (in quotes) in which it lies, and __LINE__ is replaced by the line number in which it occurs. (In case it’s not clear, each of these symbols has a pair of underscore (_) characters in front and another pair in back.)

So we can rewrite debug.h like this:

#ifdef DEBUG

#define dbgprint(x) cerr << #x << ": " << x << " in "  << __FILE__ << ":" << __LINE__ << endl
#define dbgmsg(message) cerr << message " in "  << __FILE__ << ":" << __LINE__ << endl

#else

#define dbgprint(x)
#define dbgmsg(message)

#endif

to get debugging output like:

i: 0 in myProgram.cpp:23
i: 1 in myProgram.cpp:23
i: 2 in myProgram.cpp:23
  ⋮
i: 125 in myProgram.cpp:23
Exited loop in myProgram.cpp:26

2 Assertions

Sometimes, we can anticipate potential trouble spots as we are writing the code. Good programmers often engage in defensive programming, in which they introduce into their code special checks and actions just in case things don;t actually behave as expected.

One staple of defensive programming is the assertion, a boolean test that should be true if things are working as expected. The assert macro, defined in header file <assert.h> for C and <cassert> for C++, allows us to introduce assertions into our code so that the program stops with an informational message whenever the asserted condition fails.

For example, back when we were first writing myProgram.cpp, we might have anticipated trouble with that loop and written:

#include <cassert>assert (numElements <= myArraySize);
for (int i = 0; i < numElements; ++i)
{
   cout << myArray[i] << endl;
}
myArray[0] = myArray[1];

Assertions are controlled by the compile-time symbol NDEBUG (“not debugging”). If NDEBUG is defined, then each assertion is compiled as a comment - it doesn’t affect the actual program execution. But if NDEBUG is not defined, then the assertion gets translated to something along these lines (it varies slightly among different compilers):

#define assert(condition) if (!(condition)) {\
cerr << "assertion failed at " << __FILE__ << ":" << __LINE__ \
<< endl; \
abort(); \
}

so our sample code would become:

#include <cassert>
  ⋮
if (!(numElements <= myArraySize)) {\
cerr << "assertion failed at " << __FILE__ << ":" << __LINE__ \
<< endl; \
abort(); \
}
for (int i = 0; i < numElements; ++i)
{
   cout << myArray[i] << endl;
}
myArray[0] = myArray[1];

Unlike the kind of debugging out we have looked at earlier, assertions are silent unless things are going wrong anyway, so many programmers don’t bother turning them off in the final submitted version unless the conditions being tested are complicated enough to noticeably slow the execution speed.

3 Automated Debuggers

When debugging output and assertions aren’t convenient or you need more details about what’s going on, it’s time to look at automatic debuggers.

Although there are many automatic debuggers out there, they all provide pretty much the same basic set of capabilities:

When compiling with gcc and g++, the -g option causes the compiler to emit information required for an automatic debugger. The debugger of choice with these compilers is called gdb.

For Java programs, the same -g option causes the compiler to emit information useful for its run-time debugger, called jdb.

3.1 Working with a Debugger - General Strategies

Debuggers like gdb and jdb can be especially useful in dealing with silent crashes, where you really don’t know where in the program the crash occurred.

  1. Look at the output produced before the crash. That can give you a clue as to where in the program you were when the crash occurred.

  2. Run the program from within a debugger (gdb if you have compiled with g++). Don’t worry about breakpoints or single-stepping or any of that stuff at first. Just run it.

    When the crash occurs, the debugger should tell you what line of code in what file was being executed at the moment of the crash.

    Actually, it’s not quite that simple. There’s a good chance that the crash will occur on some line of code you didn’t actually write yourself, deep inside some system library function that was called by some other system library function that was called by some other…until we finally get back to your own code. That crash occurred because you are using a function but passed it some data that was incorrect or corrupt in some way.

    Your debugger should let you view the entire runtime stack of calls that were in effect at the moment of the crash. (Use the command “backtrace” or “bt” in gdb to view the entire stack.) So you should be able to determine where the crash occurred. That’s not as good as determining why, but it’s a start.

  3. Take a good look at the data being manipulated at the location of the crash. Are you using pointers? Could some of them be null? Are you indexing into an array? Could your index value be too large or negative? Are you reading from a file? Could the file be at the end already, or might the data be in a different format than you expected?

    If you used a debugger to find the crash locations, you can probably move up and down the stack (gdb commands “up” and “down”) and to view the values of variables within each active call. This may give a clue about what was happening.

  4. Form some hypotheses (take a guess) as to what was going on at the time of the crash. Then test your hypothesis! You can do this a number of ways:

    1. Add debugging output. If you think one of your variables may have a bad or unanticipated value, print it out. Rerun the program and see if the value looks OK. E.g.,

      cerr << "x = " << x << "   y = " << y << endl;
      cerr << "myPointer = " << myPointer << endl;
      cerr << "*myPointer = " << *myPointer << endl;
      
    2. Add an assertion to test for an OK value. E.g.,

      assert (myPointer != 0);
      

      Rerun the program and see if the assertion is violated.

    3. In the debugger, set a breakpoint shortly before the crash location. Run the program and examine the values of the variables via the debugger interface. Single step toward the crash, watching for changes in the critical variables.

    Once you have figured out what was the immediate cause of the crash, then you’re ready for the really important part.

  5. Try to determine the ultimate reason for the problem.

    Sometimes the actual problem is right where the crash occurs. Unfortunately, it’s all to common for the real “bug” to have occurred much earlier during the execution. But once you know which data values are incorrect or corrupted, you can start trying to reason backwards through your code to ask what could have caused that data to become incorrect.

    As you reason backwards, continue to form hypotheses about what the earlier cause might be, and keep testing those hypotheses as described in the prior step.

4 emacs Debugging Mode

The easiest way to run gdb and jdb is, again, from inside emacs. The reason for this is quite simple. emacs will synchronize a display of the source code with the current debugger state, so that as you use the debugger to move through the code, emacs will show you just where in the code you are.

Try creating a longer program in C or C++, and compile it to produce an executable program foo. From within emacs, look at one of the source code files for that program and then give the command M-x gdb. (For Java programs, use M-x jdb, instead.)

At the prompt “Run gdb like this:”, type the program name foo. emacs will then launch gdb, and eventually you will get the prompt “(gdb)” in a window. You can now control gdb by typing commands into the gdb window. The most important commands are:

set args …
If your program expects arguments on its command lane when it is invoked from the shell, list those arguments in this command before running the program. (These may include redirection of the input and output).
break function
Sets a breakpoint at the entry to the named function (i.e., indicates that you want execution to pause upon entry to that function).
break lineNumber
Sets a breakpoint at the indicated line number in the current source code file. You can execute this command in emacs either by typing it directly in to the debugger command window pane, or by changing to a window containing the source code, positioning the cursor on the desired line, and giving the emacs command C-X spacebar,
run
Starts the program running.
c
Continues execution after it has been paused at a breakpoint.
n
Executes the next statement, then pauses execution. If that statement is a function/procedure call, the entire call is performed before pausing.

You can also do this by giving the emacs command C-C C-N.

s
Like n, executes the next statement, but if that statement is a function procedure call, this commands steps into the body of the function/procedure and pauses there.

You can also do this by giving the emacs command C-C C-S.

bt
(backtrace) prints a list of all active function calls from main down to the function where execution actually paused.
up
Moves the current debugger position up the call stack, allowing you to examine the caller of the current procedure/function.

You can also do this via the emacs command C-C <

down
(or just “d” for short) Moves the current debugger position down the call stack, reversing the effect of a previous “up” command. The allows you to examine the caller of the current procedure/function.

You can also do this via the emacs command C-C >

p expression
Prints the value of expression, which may include any variables that are visible at the current execution location.
quit
Ends your gdb session.

gdb also has a help command, that supplies more details on its available commands.

For Java programs, the jdb commands are similar to those listed above, but vary in minor details (e.g., you must spell out the command word “print” instead of abbreviating it to “p”). The easiest thing to do is to stick to the emacs commands (e.g., C-c n rather than typing out “next”) and let emacs deal with the minor differences between debuggers.


1: The #x is a special trick supported by the C/C++ macro processor. It takes whatever string x stands for, and puts it inside quotes.]