There are two basic approaches to debugging: instrumenting the
code, and using an interactive debugger. One way to
instrument C and C++ programs is to use the assert() facility
that is defined (as a macro that calls a library function) in the
assert.h
header file. You pass one argument to assert, an
expression that must evaluate to a value of true or false. If the
expression is false when the reference to assert() is called,
the program is aborted with an error message that tells the expression
that failed, the file in which the error occurred, and the line number
within the file of the assert() call. The idea is that you make
assertions that must be true for your program to operate correctly. If
an assertion fails, you know you have a bug and exactly where it
occurred. An especially attractive feature of using assert() is
that you can effectively remove all assert() calls from your
code without having to edit the source files. (Editing a file always
seems to make working programs stop working. That's why you write the
comments as you write your code rather than adding them after the
program "works.") If you define the preprocessor symbol,
NDEBUG
when you compile your code, all references to
assert() are removed by the preprocessor before passing the code
on to the compiler. The usual way to accomplish this is to use the
-DNDEBUG
option on the g++ command line.
The problem with using assert() is that you have to anticipate a bug in order to decide where to put in the assertions. More often you find that a program doesn't work and have to locate the error without the help of assert() calls because you didn't anticipate the bug in the first place. (Assert() is good when one person writes some code that will be used by other programmers' code. The first programmer inserts assert()s to check function preconditions so that the other programmers must pass parameter values that are in the proper range, for example.)
When you have to instrument your code in order to track down a bug, the general strategy is to insert output (print) statements that show the values of key variables at various points in the execution of the program. Seeing which output statements get printed helps trace the flow of the program, and seeing the values of variables can help locate the same kind of problems that assert() does. The problem with adding output statements to code is that you have to edit your code to put them in and then again after all the debugging is done, to take them out again. As mentioned above, editing a source file is a prime way to introduce bugs into your code. (When you think about it, it's the only way to introduce bugs!). A much better alternative is to use an interactive debugger, which is the topic of this document.
To use an interactive debugger, you must use a compiler switch
(-g
) with g++ to tell the compiler to put debugging
information in the object module that it produces. This debugging
information is in its own special section of the object module, and is
simply discarded by the system's program loader when the program is run
without the debugger. But if you run the program using the debugger,
this information allows the debugger to show you the source code for
each statement as it is executed, and to examine or modify the variables
in your program as it executes.
Note that optimizing compilers may rearrange parts of your program so that the statements you wrote into your code might execute in a different sequence from the way you wrote them, and some statements might even get eliminated completely. For this reason it is best not to use any optimization options while your code is under development.
info gdb
" to access that
material. (If you type just "info
" you can see what other
GNU commands we have documentation for.)
Start gdb from the command line by typing the command "gdb
<file>
" where <file> is the name of the file you want
to debug. The next thing you will see (after some messages) is an
asterisk prompt from gdb when it is ready for you to type a command.
The first command you type will probably be "b main
" which
tells gdb to set a breakpoint at the beginning of your main()
function. A breakpoint is a place in your program where you want it to
stop executing so that you can see what is going on. In this case, you
want to run your program at full speed until it gets past the
initialization code that makes the call to main(). The command
to start the program running is just "r
" (which stands for
"run
"). If your program processes command line arguments
(using argc
and argv
), you enter the arguments
you want to use on the run command line, and gdb will pass them on to
your main() function.
So now your program has started running, and stopped at main(),
with gdb issuing its asterisk prompt for you to enter more commands.
The first two commands you will probably want to try are
"s
" (step
) and "n
"
(next
). Both cause one statement of your program to
execute; the difference is that step
steps into function
references, and next
steps over them as if a function
reference was just an expression being evaluated. To single-step
through your program, you need to keep repeating n
(or
s
) commands; gdb lets you use the
<Enter>
key to repeat the last command you typed
without actually retyping the command itself. (This may seem trivial,
but it reduces the number of keystrokes by 50%!)
If you step into a function and want the program to run at full speed
until the function returns to its caller, you can use the
"fin
" (finish
) command to do that.
If you want to set more breakpoints, you may do so at any time. In
addition to function names, you can use source code line numbers to tell
where you want to set a breakpoint. (Use the "l
" command
to list your program with line numbers.) Once you have a breakpoint
set, you can use the "c
" (continue
) command to
let the program run at full speed until it reaches a breakpoint, aborts,
or exits. You can have as many breakpoints set as you wish.
To look at the value of a variable, use the "p
"
(print
) command. You can use *
and
&
to follow pointers and to see addresses, and you can
use standard C/C++ syntax to look at parts of arrays or structures. If
you have an array called x
and an int
called
i
, you can type "p x[i]
" to see the value of
that element of the array, for example.
A particularly nice feature of gdb is that it can be used to do what is
called "post-mortem" debugging. If you run a program and it crashes
(usually with an obscure message, like "segmentation violation"), there
will be a file named "core
" produced containing an image of
memory at the time the program crashed. (On Linux systems you might
need to execute the statement "ulimit -c unlimited" to get core files;
you might put it in your ~/.profile.) You can then invoke gdb (with
no command line argument), and type the command "core core
"
in response to the asterisk prompt. At this point you will probably
issue the "bt
" (back trace
) or
"where
" command, which will tell you exactly where
your program was executing when it aborted, and what the entire function
calling sequence was that got you from main() to where your
program died. If your program died in a library function (probably
because you passed it a parameter with a bad value), you can use the
"up
" command to move up the back trace stack until you get
to the point in your own code where the problem occurred.
The bt
command is also great if you use the c
or r
command, but your program aborts before it reaches a
breakpoint or exits.
That's it. That's enough of gdb to make it a productive tool for you to
use. You still need two more commands, though. One is "h
"
(help
) to access the builtin help system, and the other is
"q
" (quit
) to exit the program.
However, there will be another window that will show the source code for
your program, starting at main(). (This is better than having to
use the "l
" command to list little snippits of your code,
but still not really exciting ...) There is also a little floating
toolbar window with buttons for the gdb commands described above, as
well as several others. To set your breakpoint in main(), click
on the function name in the source code window (it will highlight), and
click on the red Break button in the little toolbar window. Click on
"Run" to run the program.
OK, now for the good part. First, click on the name of a variable in
the source code window, and click on the "Print()" button at the bottom
of that same window. You will see the "p
" command to print
the variable entered for you in the gdb console window, along with the
result. But the real beauty of ddd appears when you click on pointer,
array, or structure variable and then click on the "Display()" button.
A new window (the "Program Data" window) comes up that gives you a
graphical display of your data. If a datum is a pointer, you can click
on the "Display*()" button of the Program Data window to follow the
pointer: you'll see a box containing the name of the pointer variable,
its address, and an arrow pointing to a second box that contains
information about the memory location pointed to by the pointer
variable. The data values in the Program Data window are updated every
time you step through a line of code.
The graphical display can be as complex as your data structures. You can display as many different data items at the same time as you wish, and you can rearrange the layout of the graphical display to suit your preferences. Experiment with it to get a full appreciation for how useful it is.
There is an option to have the Program Data, the source code, and the gdb console windows put into a single window frame, and you can adjust their relative sizes within the frame. I find this useful on a workstation with a large screen, but the separate windows are more convenient on a smaller screen. You can set your prefereneces from the Options menu that appears in each of the three main windows. Don't forget to save your options using the option of that name so your preferences will still be in effect the next time you start ddd.
The man page for ddd is 50 pages long (!), so you can imagine that this is a very powerful tool. What's nice about it is that, like gdb, you need to learn only a little bit about it to start using it effectively.
Exit ddd from the File menu's Quit choice (or type Alt-Q).