ACM Intermediate UNIX Tutorial - Debugging
Basic debugging concepts
Ok, instead of going right into the heart of GDB, it might be a good idea to talk a bit about
debugging in general. Not that everyone isn't already an expert at it, but hey, there might be
someone who isn't.
Without further adieu, here are the basic techniques [begin silly programming 101 rant]:
- Don't write buggy code. This is, of course, the most important technique, and the hardest
to master and all that. There are some things you can do to reduce the number of bugs, though:
- Write code in small chunks. Test the chunks. If you have some little chunk that's guaranteed to work,
you don't have to worry about whether the chunk has the bug, or the code that calls the chunk has the
bug.
- Think about design first a little bit. If you really must code something immediately, write
some generic code and think about design while you do that!
- Try to create invariants in your code. That is, use assert(). A LOT. Don't be afraid to assert
every little thing, eg. pointer args are not null, arguments have a certain value, loop counters have
a certain value at the end of the loop, etc. Core dumps are good - especially when they come from
an assert!
- Include a ridiculous amount of debugging spew. Often, you don't want to use a debugger. For instance,
when dealing with some huge loop, using the debugger can be a lot more painful than using a lot of print statements.
Make full use of stderr - eg. #define DEBUG(...) fprintf(stderr, ...). You can always redirect stderr to null and still
see the regular output!
- When you get a core dump, look at the call stack (backtrace). This tells you exactly where it crashed.
Then look at the variables and arguments (info locals, info args). 75% of the time, this alone is enough to find
the bug - no further debugging required!
- If you need to see the state of the program at a certain point, and then sort of trace along with it for
a little point, set a breakpoint. A breakpoint tells the debugger to let the program run along all happily, until
it reaches a certain point. Then it stops executing and lets you continue.
- If you want to know when a certain variable changes, set a watch. A watch is essentially a breakpoint on modifications
to a variable. When the variable changes, the program stops.
- It's possible, in the debugger, to change the values of variables by hand, call functions, etc. This can be quite
handy!
- What if you have a bizarre error? Code fails in a place where you can see that no error exists? It's
probably something very annoying: Corruption of the stack. When this happens, you're in trouble - it's very hard
to debug.
- What if your program tends to crash in new/delete/malloc/free etc? This probably signifies corruption of the heap.
This means you deleted something wrong, or deleted something twice, etc. In this case, there is something that can help you -
malloc debuggers! First, set the environment variable MALLOC_CHECK_ to 2 (yes, there's a _ at the end). Run the program,
see if it finds anything. If not, it's time for efence!
Ok, that was a quick list of debugging ideas. Now on to using GDB
GDB
GDB is the standard GNU command line debugger. It's full-featured, but just using it raw is a little bit
painful. You can't really see your code, and the output it interspersed with your program's output, so it's
basically a big mess. It's fine for getting a backtrace and some variable spew after a core dump, but for interactive
tracing it's nice to use a frontend to it.
Starting GDB
Starting gdb is easy. Just type: gdb program where program is, you know, your program. You can then start
running your program in gdb by typing: run. If you need to give your program some arguments, give the arguments
to run. As an example:
$ gdb helloworld
GNU gdb 4.17.0.4 with Linux/x86 hardware watchpoint and FPU support
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux"...(no debugging symbols found)...
(gdb) run -q
Starting program: /u15/husted/uns-stuff/tutorials/helloworld -q
Hello, world!
Program exited normally.
(gdb)
There, we ran gdb on the "helloworld" program, and gave helloworld the argument, "-q" (which it studiously ignored,
it being hello world after all
To trace into a core file, start gdb like this: gdb program core. If your core file is named something else
(for instance, if you renamed it) you'd change "core". Thus, an example:
$ ./helloworld
Segmentation fault (core dumped)
$ gdb helloworld core
GNU gdb 4.17.0.4 with Linux/x86 hardware watchpoint and FPU support
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux"...
Core was generated by `./helloworld'.
Program terminated with signal 11, Segmentation fault.
Reading symbols from /lib/libc.so.6...bdone.
Reading symbols from /lib/ld-linux.so.2...done.
#0 0x8048480 in main () at helloworld.c:4
4 int *a = 0; *a = 0;
(gdb) bt
#0 0x8048480 in main () at helloworld.c:4
(gdb) info locals
a = (int *) 0x0
(gdb) info args
No arguments.
(gdb)
There, we ran gdb on the core file, and determined that for some reason, the 4th line of the program had some sort
of null pointer bug in it.
Basic gdb commands:
- backtrace [bt] - print out the call stack of your program. So, if you're in f1(), which was called by f2(), which
was called by main(), it'll print out f1, f2, and main in that order, with the arguments to each.
- info locals - print out the local variables
- info args - print out the arguments to the local function
- up - move up the call stack one function. So, as in the previous example, this would put you in f2(), at the point
where it called f1().
- down - like up, but down. Move back down the stack
- print <expr> - print an expression. Can be any expression in C. So, if you had a variable, say, "int *p = &a;"
you could view it with "print *p" You can even include function calls in here. In C++, to print out the local class instance,
try "print *this"
- ^C (ctrl-c) - stops execution of the program while it's running in gdb
- cont - restarts execution after it's stopped
- next [n] - next command, not entering functions (do this while stopped to trace your program)
- step [s] - next command, entering functions
- finish - run to end of function, then stop again
- [return] - do last command again. So, you could type n[return], and then keep hitting return to step through your program.
- display <expr> - print the value of the expression, every time you hit return
- break <where> - tell the debugger to stop at that point. The location can have a number of forms:
function - the function to stop in, wheras file : line will stop at a certain line in a file
- watch <expr> - tell the debugger to keep an eye on the expression, and stop when the expression changes
- clear - delete the breakpoints and watchpoints
Front ends to GDB
As I said before, using GDB is kind of a pain for anything beyond backtrace. This is why most everyone uses a front-end
to it.
One of the most common front ends to gdb is, of course, emacs. If you enter GDB mode in emacs (M-x gdb) it prompts
you for a program to run under the debugger (you can enter core here as well) and then basically puts GDB running in one
window (with some little navigation buttons on top if you're using xemacs) and shows your source code in the other. As
you walk through your program in gdb, the source code window follows along, highlighting exactly where you are. Very convenient -
especially since you can fix the bug right there, without leaving emacs at all.
There are also a number of GUI front ends to GDB. One feature most of them have, which is very nice, is the ability to
put your program's interaction in one window, and gdb's command console in another. This keeps them from getting all
jumbled up, which can be essential with many programs. I'll list a few of the front ends here:
- code-medic - This is a pretty good front end. It lets you view your variables as a tree, shows your source code etc.,
lets you keep a call stack window open, etc.
- ddd - This is also a pretty good front end. It has about the same features as code medic, plus perhaps a few more.
The downside is, it's unstable and not very easy to use, at least in some peoples' opinion. It can also support some
other debuggers besides gdb.
- xxgdb - The old fashioned, old-school X11 front-end. This basically just gives you gdb, a code window, and a few buttons.
It'll almost always be available if GDB is, though.
- kdevelop - This is KDE's development environment, which is basically a copy of MSVC. It includes debugging of course,
which means, you guessed it, another front end to gdb. Expect it to be rather like MSVC. In other words, yucky.
The best advice here is probably to just try out a few of them! Some people like code-medic, some ddd, lots of people
like emacs gdb-mode, some people like kdevelop because it looks like MSVC, etc. Some even like gdb raw on the console.
Justin Husted
Last modified: Tue May 16 20:50:17 PDT 2000