One approach to debugging is the brute-force method: simply scatter tracing statements through your code and watch them fill your screen. There are some types of debugging for which this is ideal -- for example, graphing a trace of intermediate values may reveal that an algorithm is showing signs of instability -- but it can be very cumbersome.
Better, in many cases, is to use a debugger. A debugger allows you to roam through your code, stopping in troublesome functions, examining data, and stepping through your code line-by-line. There is a great deal you can do with a debugger, but they are not often the easiest tools to master. However, there is a good deal you can do armed only with experience of the foothills of the debugger's learning curve.
I will describe the Sun debugger dbx
, here.
The use of the GNU debugger, gdb
, and the
dbx
debugger on Digital Unix, are broadly
similar.
First, compile crash.c
, listed in Appendix A.5, in the usual way (cc -o crash
crash.c
), run it, and watch it crash when it tries
to dereference a zero pointer. This leaves a
core
file in the current
directory[Note 9]. You can obtain rudimentary
post-mortem information from this core file with
dbx
, as follows:
The% dbx crash core [...] program terminated by signal SEGV (no mapping at the fault address) (dbx) where =>[1] bananas(0x0, 0x1, 0xef691338, 0x10074, 0x2, 0xeffff8d0), at 0x10870 [2] main(0x1, 0xeffff94c, 0xeffff954, 0x20800, 0x1, 0x0), at 0x108fc (dbx) quit
where
command to dbx
shows
where the program was when it crashed (the
[...]
shows where I have omitted some
unenlightening chatter from the debugger).The other information dbx
gives you is less
useful -- the debugger doesn't know enough about your
program to be more helpful. You can, however, tell the
compiler to pass on more information to the debugger when
it processes your code; do this with the -g
flag to the compiler, as in cc -g -o crash
crash.c
. If you compile several modules to produce
your executable, you'll need to give this option to the
compilation of each module you wish to debug. You'll also
have to give this option at the link stage on SunOS4, but
not on Solaris or for gdb
on Linux.
If we run crash
again now, and look at the core file, we see
that the debugger can provide us with more information.
Note that we can examine the values of variables in scope at the point where execution stopped, and that% dbx crash core [...] program terminated by signal SEGV (no mapping at the fault address) Current function is bananas 12 printf ("banana split: %d\n", *zp); (dbx) where =>[1] bananas(i = 0), line 12 in "crash.c" [2] main(), line 22 in "crash.c" (dbx) print i i = 0 (dbx) print buf buf = "banana number 0 " (dbx) print *zp dbx: reference through nil pointer (dbx) print zp zp = (nil) (dbx) quit
dbx
knows enough about C (or Fortran) to know
the type of variables it is asked to print. Printing the
value of the pointer zp
shows us why our
program has crashed.The debugger is not only useful for such post-mortem
diagnosis. It is also (for some people, primarily) used
for investigating a program's behaviour when it is running
normally, albeit with bugs we wish to track down. For
example, start the debugger and tell it to stop when it
enters the function bananas
:
You can set multiple breakpoints (so called) not only in functions, but also at line numbers, or linenumbers within files (using% dbx crash [...] (dbx) stop in bananas (2) stop in bananas
(dbx) stop at crash.c:12
). Then tell the
program to start running:
Note that, as well as output from the debugger itself, we also see output from the running program. If the program required input from the user, it would receive it as normal. The program stops at the first line of code within the(dbx) run Running: crash (process id 29707) Entering the bananas function: 1 stopped in bananas at line 6 in file "crash.c" 6 sprintf (buf, "banana number %d\n", i); (dbx) next stopped in bananas at line 7 in file "crash.c" 7 if (i != 0) (dbx) print i i = 1
bananas
function. We can move one
line forward in the code, verify that the parameter
i
has its expected value, then tell the
program to resume execution.
(dbx) cont banana number 1 No bananas left! stopped in bananas at line 6 in file "crash.c" 6 sprintf (buf, "banana number %d\n", i); (dbx) print i i = 0 (dbx) cont signal SEGV (no mapping at the fault address) in bananas at line 12 in file "crash.c" 12 printf ("banana split: %d\n", *zp); (dbx) quit
The Solaris debugger can be used through a GUI: give the
command debugger
to start this up. The Digital
GUI debugger is ladebug
. There is a GUI
interface to gdb
, called xxgdb
.
In general, you can't debug optimized code (that is, code
compiled with the option -O
), because the
optimizer may rearrange lines or remove redundant
variables. However, both dbx
and
gdb
do have support for debugging code with
mild optimization, though there are some things, such as
stepping from line to line, you might not be able to do.
This introduction has merely scratched the surface of
what you can do with the debugger, by showing you the bare
minimum of commands you need to navigate around your
running program. There is a great deal more information
available in Sun's debugging manual
[sundebug], or in gdb
's
info pages (info gdb
).