Debugging with gdb and ddd

Goals

By the end of this lab you should:

Introduction

Now that you are familiar with the Linux programing enviroment and how to program under it, there is one major tool left to learn: the debugger.

The very popular (and free) debugger companion for g++ is gdb. gdb allows you to execute your code in a number of ways, and inspect the state of your variables throughout the execution of your program. In this lab we will demonstrate some of the more common methods of using gdb, give you some links for more information, walk you through using the debugger, and hopefully let you tackle some real debugging problems.

Finding What Caused Your Program To Crash

One of the most common reasons to need a debugger is because your program crashed, usually producing either a "Segmentation Fault" or occasionally a "Bus Error" or other mysterious error (like "Floating Point Exception"). What we want you to take from this lab is the lightning-fast reflex instinct of grabbing the debugger when such an error occurs. (We are serious about this. In the future, if you get a runtime error and don't instinctively run the debugger on it, we will be very sad. This is a very powerful tool that we really really want you to become comfortable with. )

Let's try an example:

#include <iostream>

int main()
{
  using std::cout;

  // We are setting this pointer to not point at anything
  int* bar = 0;

  // Now try to print out what is in the bad pointer
  cout << *bar << endl;

  // And now try to set the bad pointer
  *bar = 42;
}

This program will certainly crash (hopefully you can see why. If not, please go re-read the chapter of your text regarding pointers.) So we compile it as normal and see what happens:

> g++ -Wall -Werror -W -pedantic test1.cc -o test1
> ./test1
Segmentation Fault

We have a problem (as expected), so lets see what the debugger says:

Using gdb

Start the debugger on the command line and give it the name of your executable as an argument. To continue with our example:

> gdb test1
GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
Copyright 2003 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-gnu".
(gdb)

Now the debugger waits for instructions. To get it to run our program and see what happened, give it the instruction "run".

(gdb) run
Starting program: /home/dsheldon/cs12/test1

Program received signal SIGSEGV, Segmentation fault.
0x080485fd in main ()
(gdb) bt
#0  0x080485fd in main ()
#1  0x401124ad in __libc_start_main () from /lib/libc.so.6
(gdb)

Which is not terribly helpful. We are now told the memory location of the instruction that killed the program, and the fact that the program died in the function "main ()." This is not news (we only defined main, of course that is where the problem is! ).

The problem is that gdb requires some extra information to be in the executable that it is trying to debug. This would be problematic, except for the fact that the same people responsible for g++ are responsible for gdb. So adding in the extra symbol information is quite easy. Go back and recompile your file with "-g" option to g++

> g++ -g -Wall -Werror -W -pedantic test1.cc -o test1

and rerun the debugger ( hit Ctrl+D to terminate gdb, and let it terminate your program ).

(gdb) run
Starting program: /home/dsheldon/cs12/test1

Program received signal SIGSEGV, Segmentation fault.
0x080485fd in main () at t.cc:13
13              cout << *bar << endl;
(gdb)

Ooh, that's much better. Not only do we know what happened, we even know what line it happened on. Very useful. Let's look at another (contrived) example:

#include <iostream>

int fac(int n)
{
  using std::cout;
  
  if (n == 1)
  {
    int* bar = 0;
    cout << *bar << endl;
  }
  return n * fac(n - 1);
}

int main()
{
  using std::cout;
  cout << fac(5) << endl;
}

Now we want to see what gdb will let us do with regard to functions. Compile as before (with the -g flag), and run the debugger on the program.

> gdb test2
GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
Copyright 2003 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-gnu".
(gdb) run
Starting program: /home/dsheldon/cs12/test2

Program received signal SIGSEGV, Segmentation fault.
0x080485f9 in fac(int) (n=1) at test2.cc:10
10                      cout << *bar << endl;
(gdb) bt
#0  0x080485f9 in fac(int) (n=1) at test2.cc:10
#1  0x0804861e in fac(int) (n=2) at test2.cc:12
#2  0x0804861e in fac(int) (n=3) at test2.cc:12
#3  0x0804861e in fac(int) (n=4) at test2.cc:12
#4  0x0804861e in fac(int) (n=5) at test2.cc:12
#5  0x0804864d in main () at test2.cc:17
#6  0x401124ad in __libc_start_main () from /lib/libc.so.6
(gdb)

(Note: bt stands for "back trace".) Ooh, this is nice. It will keep track of all the stack activation frames, their parameters, and where in your code they are listed. With this it is pretty easy to tell how your program got into the state it was in. But now, how do we see what that state really is? On the same run of gdb, let's peek around at the internals of the program.

(gdb) print bar
$1 = (int *) 0x0
(gdb) p n
$2 = 1

The command "print", which can be abbreviated as just "p", shows the current type and value of the variable ( or expression ) requested. This lets us examine what all the variables were just as the program died. What if we want to see what the value of something in an earlier function (lower on the stack) is?

(gdb) up
#1  0x0804861e in fac(int) (n=2) at test2.cc:12
12              return n * fac(n - 1);
(gdb) p bar
No symbol "bar" in current context
(gdb) p n
$4 = 2

The commands "up" and "down" let you move through the function stack. Slightly annoyingly, "up" moves you lower in the stack and "down" moves you higher ( so you must think of the function stack as growing down, rather than growing up. ) Here we went up to the next higher recursive call, and tried to print out "bar." Bar was defined locally to the scope of the "if (n == 1)" statement, so it doesn't exist in this part of the function (remember scoping rules!) But the variable "n", which exists in both scopes ( as a different local variable, remember ), shows the correct value for this activation frame. With just these tools, you can do quite a lot of debugging of broken code.

Other Useful gdb Commands

Another useful set of commands are those related to "breakpoints." A breakpoint is a point in your code where the debugger will stop execution if the point is ever reached. This is a complicated notion, so we will demonstrate with an example. Start gdb on the second example program above.

GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
Copyright 2003 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-gnu".
(gdb) break 16
Breakpoint 1 at 0x8048628: file test2.cc, line 16.
(gdb)

The command "break" allows us to set a breakpoint on a specific line of our source code. For programs that have only a single source file, this can be done by simply giving the line number. For programs with multiple source files (like the third homework assignment!), this can be done by specifying the name of the file, a colon, and the line number (like "maze.cc:20"). So we set a breakpoint at the beginning of main (line 16), let us also set one at line 9, where the bad pointer in our factorial function is defined.

(gdb) break 9
Breakpoint 2 at 0x80485e4: file test2.cc, line 9.
(gdb)

Now we can run our program as before.

(gdb) run
Starting program: /home/dsheldon/cs12/test2

Breakpoint 1, main () at test2.cc:16
16      {
(gdb) step
main () at test2.cc:17
17              cout << fac(5) << endl;
(gdb) step
fac(int) (n=5) at test2.cc:7
7               if (n == 1)
(gdb) step
12              return n * fac(n - 1);
(gdb) step
fac(int) (n=4) at test2.cc:7
7               if (n == 1)
(gdb) continue
Continuing.

Breakpoint 2, fac(int) (n=1) at test2.cc:9
9                       int* bar = 0;

The "run" command is stopped when it reaches the first breakpoint. We can continue executing line-by-line using the "step" command ( to step through the code. ) If we tire of using "step", we can continue the execution using the "continue" command, which runs from the current point in the execution until the program crashes or it hits a breakpoint (much like "run" only without restarting the program.)

With these tools in gdb, we can solve pretty much any problem we encounter. The debugger is a powerful tool, and one that you WILL have to understand at some point in your career. It is very much in your best interest to get proficient with it early, it will help you out a lot. People that are gdb gurus are a valuable commodity.

ddd - A Graphical Front-End to gdb

gdb is a great tool for finding segmentation faults quickly, but if you have a logic error, it may be tricky to find it using gdb. Sometimes using a visual debugger is much simpler when you are trying to trace through your program and verify that the logic is correct. There are many graphical front-ends to the gdb debugger, but one of the best ones is ddd. It is envoked exactly the same way as gdb and actually has the gdb command prompts in one of its windows. If you are working on a system where you have a graphical environment, you may want to give ddd a try as well. Here's a screen shot of ddd in action:

ddd screenshot

Where to Find More Information

gdb has a fairly extensive on-line help system. From within gdb, simply type "help" and it will give you a list of topics, each of which has a long list of commands contained within. Learning everything there is to know about gdb is beyond the scope of the class, but it is a lofty goal.

For more friendly help, simply type "gdb tutorial" into Google.