UMBC CMSC 202 Computer Science II
Lab7: Debugging with gdb
Objective
In this lab you will use practice using the gdb debugger.
Introduction: Why you should use a debugger
When you run your program within a debugger, you can stop the program
at critical points and examine the values of variables and objects.
The debugger provides you with much more capability and flexibility
than debugging your program using print statements. For example:
- The debugger allows you to examine the value of a variable
after your program has crashed. Using print statements
you can only see values before a program crashes.
- If you need to examine the value of a variable that you
previously had not suspected, you don't have to edit, recompile
and re-run your program.
- The debugger allows you to examine values of variables in
the function call stack. I.e., while you are debugging a function
foo(), not only can you print out the local variables of
foo(), you can also print out the local variables of the
function that called foo(), and the function that called
that function, and so on and so forth.
In this lab, we will use gdb, a debugger with a command
line interface. Although the user interface is a bit clunky, you
will find that gdb has many useful features. It "understands"
C and C++ types and syntax. It works well with source code that is
distributed across multiple files.
You might ask: If gdb is so great, why don't more people
use it?
The answer is that most people don't take the time to learn gdb
before they have a bad bug. Trying to learn gdb when
you have a bad bug and a project due date hanging over your head does
not leave you with warm fuzzies about the debugger.
Assignment
We will take a tour of gdb's many features in this lab using two
simple examples of buggy programs. You should compile and run these
programs on a Linux system on an Intel platform (e.g., the UMBC GL system).
The bug in the first example might not manifest itself in the same way on
other Unix systems (e.g., Mac OS X).
Step 1: Get the files
Here are the files you need:
- buggy.cpp: a program with some buggy code.
- List7.h: the header file for the
List class from Lab 6.
- List7.cpp: the implementation
file for the List class --- with a bug.
- List7test.cpp: the main program
that exercises the List class.
You will not need to modify any of these files.
Step 2: A Simple Debugging Session
We will use the debugger to check out the buggy code in
buggy.cpp.
Follow these steps:
- Compile buggy.cpp using the -g option. This option tells the compiler
and the loader to keep the symbol table around for the debugger:
g++ -g buggy.cpp
- Run the program by typing "./a.out". It should give you a
segmentation fault and dump core. This creates a file with file
name like "core.1234".
-
Start the gdb debugger:
gdb a.out core.1234
Using gdb, you can look at the memory contents of your program
just after the segmentation fault. This is convenient if running your
program requires several steps involving user I/O.
Note: if you start gdb without the core file, you can still
run the program using "run".
- After gdb starts up, it prints out lots of information. Look
at the last two lines:
#0 0x080486d4 in main () at buggy.cpp:12
12 A[i] = i*i ;
This says the program crashed while executing main()
on line 12 of the file buggy.cpp. It also helpfully
prints out line 12 for you.
At any time while running gdb, you can type the command "where" to
find out where you are in the program.
If all you remember about gdb is the "where" command, you still
have a very useful tool in debugging pointer errors.
- Of course, we don't remember our programs. The "list" command shows
you the source code. Try these variations of the "list" command:
list 12
list main
list 1,18
If you type "list" right after another "list" command, it shows you another
10 lines below the previously listed lines.
-
The command "help list" will tell you more about list. In general, "help"
followed by a topic or a command will give you documentation on that topic.
Typing just "help" will give you a list of topics.
- To see the value of variables and objects in the program, use the
"print" command. Try these different print commands
(you can also use "p" as a shortcut for print):
print i
print A[i]
print A
print &A
print &i
print &A[2]
Conclusion: somehow the value of i has grown so large that
A[i] causes a segmentation fault.
- Now we will re-run the program to see what happened. In
gdb we can set "breakpoints" which stop the program at critical
places. Type:
break 12
This stops the execution of the program before the code on line 12
is executed. Type "run" to start the program running. You should see
something like:
Breakpoint 1, main () at buggy.cpp:12
12 A[i] = i*i ;
Type "print A" and "print i" to see that the values of A and
i are as expected.
To execute the code in line 12, type:
step
The "step" command executes 1 line of source code and shows you the next
line. Print out A and i again to see the effects of
line 12.
- We can continue to step through the program this way, but it is
simpler to issue the "continue" command (or just "c" for short).
continue
This runs the program until the next breakpoint.
-
We can keep typing "continue" and "print" commands this way, but all those
prints get a bit tedious. Type:
display A
display i
This tells the compiler to print out A and i whenever it
stops at a breakpoint.
-
Type "continue" again. Now you see the next iteration of the while loop.
To continue again, you just have to hit the return key. Notice that the
value of i eventually grows quite large.
How did that happen?
-
If you missed the iteration where the value of i ballooned, type
"kill" to stop running the program and "run" to restart it. Your
breakpoints and display items are still intact.
-
To make sure you don't blow past the critical iteration of the while loop
again, stop at the iteration when i is 4. Then use "step". You
don't have to type "step" every time. After the first step, hitting the
return key is equivalent to typing "step".
-
Now type "p &A[5]" and "p &i". Notice that these are the same hexadecimal
(base 16) values (or should be on GL).
- Now you can quit, using "quit".
Summary:
- Compile using the -g option, then "gdb a.out".
- Use "list" to see your source code.
- Use "break" to set breakpoints.
- Start running with "run".
- Use "print" to see values.
- "display" prints automatically.
- Use "step" to execute one more line.
- Use "continue" to execute until the next breakpoint.
- The commands "kill" and "quit" do what you think
In general you can have many breakpoints and display items. Sometimes you
forget where and what they are.
To see a list of your breakpoints and display items, use:
info breakpoints
info display
You can remove all breakpoints using "delete" or individual breakpoints
using "delete 1", "delete 2", ... (Use the breakpoint number from the
"info" list.) Sometimes you just want to temporary disable a breakpoint.
You can do that with "disable 1", "disable 2", ... To enable the breakpoint
again, use "enable 1", "enable 2", ...
To list your display items, "info display". Use "undisplay" to
remove a display item. Use "disable display 1", "disable display
2",... to temporarily disable a display item.
Step 3: A more complicated example
Now, let's try something more complicated, involving programs with
different source files and code that uses objects and pointers.
We'll see that gdb actually understands C++.
- Remove the a.out and core file from the previous exercise.
- Compile a buggy implementation of the List class from Lab 6. Use the
files linked here, not the files from Lab 6:
g++ -g -Wall -ansi List7.cpp List7test.cpp
Run the program, you should see something like:
Testing Insert...
Should Print: -9 -7 -5 -4 -4 -3 -1 -1 -1 0 1 1 1 2 2 3 5 5 6 7 8 9
Actual Print: -9 -7 -5 -4 -4 -3 -1 -1 -1 0 1 1 1 2 2 3 5 5 6 7 8 9
Testing Remove...
Remove 5...
Should Print: -9 -7 -5 -4 -4 -3 -1 -1 -1 0 1 1 1 2 2 3 5 6 7 8 9
Actual Print: -9 -7 -5 -4 -4 -3 -1 -1 -1 0 1 1 1 2 2 3 5 6 7 8 9
Remove -1...
Should Print: -9 -7 -5 -4 -4 -3 -1 -1 0 1 1 1 2 2 3 5 6 7 8 9
Actual Print: -9 -7 -5 -4 -4 -3 -1 -1 0 1 1 1 2 2 3 5 6 7 8 9
Remove 9...
Segmentation fault (core dumped)
Apparently, the first two removes worked correctly, but the third one did
not.
- Run gdb with the name of the core dump file in your directory:
gdb a.out core.5678
Make sure you are using the core dump from this program and not the
previous one. The last two lines of gdb's output says
something like:
#0 0x08048949 in List::remove (this=0xbffffb0c, data=9) at List7.cpp:96
96 current->next = ptr->next ;
The remove() function is apparently the culprit. Not surprising.
-
We can list the source code from different files this way:
list List7.cpp:1,20
list List7test.cpp:1,20
list main
list remove
list List::remove
When the source code is spread out over multiple files, listing by function
name is much more convenient.
- To set a break point at a line number in a particular file:
break List7.cpp:96
We can also break after entering a function:
break List::remove
-
Run the program using "run". It should break on line 88 after entering the
remove function. Type "where". You should see something like:
#0 List::remove (this=0xbffffa9c, data=5) at List7.cpp:88
#1 0x08048e25 in main () at List7test.cpp:40
This says that the execution of the program is stopped in function
List::remove() at line number 88 of List7.cpp.
Also, often quite important, it says remove() was called
from main() at line 40 of List7test.cpp.
This is called the "backtrace of the stack frames".
-
Where we stopped the program, the variable current is not yet
initialized. Type:
step
to initialize it.
- We can now print out various local variables in remove.
Unfortunately, everything other than data is a pointer,
so printing them just gives obscure hexadecimal values. Fortunately,
gdb understands C/C++ operators *, &
and ->. Try these print statements:
print data
print m_head
print current
print *m_head
print *current
print current->next
print *(current->next)
We can also print out values of local variables in main(). Try:
print main::List1
print &main::List1
Sometimes we forget the type of the variables we are using:
ptype current
ptype *current
ptype current->m_data
ptype main::List1
Note the * at the end of the reported type of current.
This says current is a Node pointer rather than a
Node.
We can even use the "this" pointer:
print this
print &main::List1
The output confirms that the host/calling object is indeed List1
from main().
-
Type "where" and "info breakpoints" to review where we are. There should be
a breakpoint on line 96. This is the part of the while loop that is
executed when the node to be removed is found. Type:
list List7.cpp:96
continue
-
Now execution has terminated in the portion of the code that is
buggy. See if you can figure out what happened... These print
commands should help:
print *current
print *current->next
print ptr
print *ptr
print *ptr->next
-
Type "continue" (or use the return key) 4 times. The program should trigger
a segmentation fault. Type in these commands:
list
print data
print ptr
print current
print *current
You should see why this program crashes.
- Use "quit" to get out of gdb.
One command we didn't go over here is the "next" command. This command is
very much like the "step" command. The difference is when the next line of
code is a function call. In this case, "step" will stop at the first line
of the function being called, whereas "next" will wait for the function
call to finish.
Step 4: Try it in your own program
If you have time remaining, practice debugging a piece of your own code,
say from Project 1 or Project 2. Remember to use the -g
option when you compile. Try stepping through your code using "next" versus
"step" to see the difference.
With some practice, the next time you have a buggy piece of code and
a deadline looming, you can pull out gdb and know how to use it.