How to Use gdb

Brad Vander Zanden



gdb is a GNU debugger that can be very helpful in finding problems with your programs. It allows you to do a number of useful things including:

  1. controlling the execution of your program by placing breakpoints and single stepping your program.
  2. printing the values of variables in your program, and
  3. determining where a segmentation fault or bus error occurs in your program.

Thus far you have debugged your programs by inserting print statements into your program. While that technique often proves helpful and indeed is the way that I most commonly debug programs, gdb provides far more functionality and is worth learning. We will start you out with a simple tutorial that shows you some of its features and then show you how you can use it to help you with debugging.

In order to use gdb you need to compile your files with the -g option. For example:

g++ -g -c student1.c
g++ -o student1 student1.o


A Sample GDB Session

Start by copying the files in ~bvz/cs140/debugging/lab2 to your directory.

Now take a look at reverse.cpp:

#include <iostream>
#include <vector>
using namespace std;

int main() {
  vector<int> data;
  int value;
  int i;

  while (cin >> value) {
    data.push_back(value);
  }

  for (i = data.size(); i >= 0; i--) {
    cout << data.at(i) << endl;
  }
}
    
This program is supposed to reverse a list of integers. For example, given the input:
    10 20 30 40 50
  
we expect the output to be:
    50
    40
    30
    20
    10
  
Let's compile and then try to execute it:
g++ -o reverse reverse.cpp     
./reverse < input.txt

You should get an error message of some type indicating that you have an index out of bounds error. When I ran it on the hydra machine I got the error message:

    terminate called after throwing an instance of 'std::out_of_range'
  what():  vector::_M_range_check
    [1]    6993 abort      ./reverse < input.txt
  
Conspicuously missing is the line number where the index out of bounds error occurred. When a program has a logic error and runs to completion, we usually have to start with print statements to try to figure out what is going wrong. However, when a program crashes, the first thing we would like to know is where the program crashed.

gdb can provide us with this information.

To use gdb with reverse, we must first compile it so that it works with gdb. Do you remember which flag to use? If not, look back at the introduction to this lab and then recompile reverse so that it works with gdb. Then type:

gdb reverse
gdb is the command you type to invoke the gdb debugger and reverse is the name of the executable you wish to debug.


Listing the lines in a program

You can list the lines in your program using the list command, or l for short. list lists 10 lines of your program, centered about the next line to be executed. If you do it now, it will probably list the first 10 lines in your file since your program has not yet started execution. You can make it list specific lines in your program by giving it the starting and ending line numbers:

l first_line_number, last_line_number
For example, typing l 3,10 will cause gdb to list lines 3-10 of the current file.

Executing a Program in GDB

You start running a program using gdb's run command or r for short. Try it now on the reverse program. Simply type r < input.txt at the gdb prompt. Remember that the < operator redirects stdin to input.txt:

(gdb) r < input.txt
You should get some output that looks like:
Starting program: /home/bvz/courses/140/sp18/debugging/lab2/reverse < input.txt
terminate called after throwing an instance of 'std::out_of_range'
  what():  vector::_M_range_check

Program received signal SIGABRT, Aborted.
0x00007ffff7225207 in __GI_raise (sig=sig@entry=6)
    at ../nptl/sysdeps/unix/sysv/linux/raise.c:56
56	  return INLINE_SYSCALL (tgkill, 3, pid, selftid, sig);
								      

The program prints the same index out of bounds error message that we saw previously, but now there is additional information that the program aborted in a file called raise.c at line 56 (I've bold-faced and underlined the relevant text). gdb also prints the contents of the line where the seg fault occurred. Note that our program seg faulted in a function that we clearly did not write.

Our first step is to find which line in our program ended up calling the function that seg faulted. We can do this by using the backtrace command--bt--to print the stack:

(gdb) bt    
#0  0x00007ffff7225207 in __GI_raise (sig=sig@entry=6)
    at ../nptl/sysdeps/unix/sysv/linux/raise.c:56
#1  0x00007ffff72268f8 in __GI_abort () at abort.c:90
#2  0x00007ffff7b347d5 in __gnu_cxx::__verbose_terminate_handler ()
    at ../../../../libstdc++-v3/libsupc++/vterminate.cc:95
#3  0x00007ffff7b32746 in __cxxabiv1::__terminate (handler=)
    at ../../../../libstdc++-v3/libsupc++/eh_terminate.cc:38
#4  0x00007ffff7b32773 in std::terminate ()
    at ../../../../libstdc++-v3/libsupc++/eh_terminate.cc:48
#5  0x00007ffff7b32993 in __cxxabiv1::__cxa_throw (obj=0x604100, 
    tinfo=0x7ffff7dbf1d0 , dest=
    0x7ffff7b46ef0 )
    at ../../../../libstdc++-v3/libsupc++/eh_throw.cc:87
#6  0x00007ffff7b87857 in std::__throw_out_of_range (__s=)
    at ../../../../../libstdc++-v3/src/c++11/functexcept.cc:80
#7  0x000000000040134d in std::vector >::_M_range_check (this=0x7fffffffde70, __n=5) at /usr/include/c++/4.8.2/bits/stl_vector.h:794
#8  0x0000000000400eed in std::vector >::at (
    this=0x7fffffffde70, __n=5) at /usr/include/c++/4.8.2/bits/stl_vector.h:812
#9  0x0000000000400ced in main () at reverse.cpp:15
At the very bottom of the stack you can see that we were executing line 15 in reverse.cpp when the program crashed. Let's print this line:
  (gdb) l 15
Look at the result--it's garbage. The reason is that gdb can't read our minds. It's printing lines 10-19 of the function that crashed, not lines 10-19 of reverse.cpp. We first need to point gdb to the stack frame whose lines we want to print. We can do this with the select-frame command. Note that in the above stack trace that the last frame containing reverse.cpp is labeled #9. So we must first select stack frame 9 and then print our lines of interest:
  (gdb) select-frame 9
  (gdb) l 15
10	  while (cin >> value) {
11	    data.push_back(value);
12	  }
13	
14	  for (i = data.size(); i >= 0; i--) {
15	    cout << data.at(i) << endl;
16	  }
17	}
We see that line 15 attempted to print the element at index location i in data. Let's have a look at the contents of i and data. We can do this with gdb's print command, which can be abbreviated as p:
(gdb) p data
$1 = std::vector of length 5, capacity 8 = {10, 20, 30, 40, 50}
(gdb) p i
$2 = 5
The first print statement tells us that data is currently a vector of length 5 with the contents {10, 20, 30, 40, 50}. It also has a capacity for 8 elements, which means that we can add 3 more integers to the vector before the vector would need to re-size itself.

The second print statement tells us that i has the value 5. Initially you might wonder why i is out of bounds. After all, its current value is the length of the vector. Do you know what the problem is? At this point you have to rely on your knowledge of C++. You have extracted all the information you will be able to obtain about the bug and the rest is up to you.

Take a moment to fix the problem and re-compile the program to make it work. If you can't figure out the solution, the TA will tell you.

at() versus the [] operator

In general it is safer to access the elements of a vector using the function at() rather than the operator[]. The reason is that the at() function checks if the index is out of bounds and throws an error if the index is out of bounds. The operator[] will simply access the element at that index and if the index is out of bounds you will get garbage. If you use the operator[] to set an element and the index is out of bounds, then you will corrupt memory. Unfortunately you will not get an immediate seg fault in either the reading or writing case and your error will only show up later in the program, when the bug will be harder to find. For example, in reverse1.cpp I have changed the at() function call with the [] operator. Try compiling and running reverse1. It should run to completion and generate the output:

0   // Your value might be different than 0
50
40
30
20
10
      
Notice the 0 at the top of the output. This is a random value located at data[5] when I ran this program. When I ran the program a couple days later the 0 was replaced with 32767. Your first value may well be random as well. You will be much harder pressed to debug this program and discover that the problem is an index out of bounds error.

Another Way to View a Function's Variables

The command info locals will display the value of all local variables in the current stack frame (remember that a stack frame corresponds to a function so you are really displaying the values of all local variables in a function) and the command info args will display the values of all parameters of the current function. For our current program, here is the output of the info locals command:

  
(gdb) info locals
data = std::vector of length 5, capacity 8 = {10, 20, 30, 40, 50}
value = 50
i = 5

Running Your Program When It Expects Command Line Arguments

The next program we will work with is swap.cpp, a program that uses command line arguments. swap is supposed to perform the following actions:

  1. copy the command line arguments into a vector
  2. request the user to input, using stdin, two indices whose elements should be swapped
  3. swap the elements at the two indices
  4. print the resulting vector
Try compiling swap.cpp. You should get some compile error messages. Correct the compilation errors. If you have difficulty, the TA will give you the answer in a few minutes. We will occasionally give you a program with compilation errors because it is important to be able to recognize them and fix them.

Once you have corrected the compilation errors, remember to compile swap.cpp with the -g option. You can now run swap in gdb by listing the command line arguments after the run command. For example:

r 10 20 30 40 50
After the program prompts you for the indices of the elements to swap, the program should crash with a segmentation violation. Use the commands you learned earlier in this exercise to discover where the error occurred and see if you can figure out why the program seg faulted. Take a few minutes to try to locate the error and then fix it. If you have difficulty, the TA will give you the answer in a few minutes.

What Error Conditions Should You Be Checking For?

swap.cpp checks to make sure that the read operation which reads values into index1 and index2 succeeds before proceeding. The read will fail if the user provides non-integers as input. However, before the program proceeds, it needs to check for some other possible error conditions associated with index1 and index2. Take a moment to try to figure out what these error conditions might be. You can find the answer in swap1.cpp, which is the file you will be working with next.

Using GDB To Control The Execution Of Your Program

If your program is seg faulting then the run command will probably suffice to start your debugging session. When the program seg faults gdb will tell you the file, the procedure, and the line number where the seg fault occurred. Once you find the line number where the seg fault occurred, you may be able to determine the source of the error. However, the program may run to completion and produce incorrect output. In this case you need to form an initial hypothesis about the source of the error and do one of two things: 1) start inserting print statements into your program, or 2) use gdb to incrementally execute your program and keep checking the value of crucial variables until you discover a variable that does not have an expected value--at this point you will have hopefully narrowed your problem to a small section of code and can figure out what is wrong with it.

For example, try compiling swap1.cpp with the -g flag and then running your program with the command line arguments 10 20 30 40 50. You should get something like the following output:

UNIX> gdb swap
(gdb) r 10 20 30 40 50
Starting program: /home/bvz/courses/140/sp18/debugging/lab2/swap 10 20 30 40 50
Please enter the integer indices of two elements: 1 3
0: 10
1: 20
2: 30
3: 40
4: 50
[Inferior 1 (process 5532) exited normally]
  
The "Starting program" line shows you how you might execute this program at the command line. We then enter our two integer indices and look at the output. What's wrong with it?

Time to start finding the error. You may very well be able to determine the problem by simply looking at the code, but let's assume that you have no idea what is causing the problem. One thing you might decide to do is have gdb incrementally execute the program for you. You can use gdb to stop the program at some point during its execution and start single-stepping it. You can cause the program to stop at a certain line or when a certain function is called by creating breakpoints. A breakpoint is created using the break command or b for short. It takes either a line number or a function as an argument.

Each of the follow commands will set a breakpoint in the swap program:

b 21                 // set a breakpoint at line 21 in the current file
b swap    // set a breakpoint on the function called swap 
b swap.cpp:56      // set a breakpoint at line 56 in swap.cpp
When you place a breakpoint on a function, gdb will stop the program any time that the function is called, no matter where in the program the function is called. It will stop your program at the first executable statement in your function, which might be an initialization statement in your variable declarations.

When a breakpoint is created gdb will assign it a number. If you want to later delete the breakpoint you can do so by typing delete and the breakpoint number. Alternatively you can type clear and either the line number or the function to which the breakpoint is attached. For example:

delete 1
clear swap
clear swap.cpp:56
These three commands will delete the three breakpoints created earlier.

If you forget what breakpoints you have set, you can type info b to print the breakpoints. For example:

(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000400d32 in main(int, char**) 
                                                   at swap1.cpp:21
The Disp field is short for Disposition and indicates whether the breakpoint is marked to be disabled or deleted when it is next encountered. The Enb field is short for Enabled and indicates whether or not the breakpoint is currently enabled. You do not need to worry about either of these fields. The What field tells you where the breakpoint is set, and that information, plus the number of the breakpoint, are probably the two pieces of information you need.


Continuing from a Breakpoint

You can cause the program to continue executing and proceed to the next breakpoint by typing continue, or c for short. Try it out by re-instating the breakpoints you typed in previously, re-starting the program, and then typing c each time you reach a breakpoint. You will need to re-type the previous breakpoints if you deleted them.


Finding the Bug

Now let's find that bug. First quit out of gdb and then re-enter gdb so that you start with a clean slate.

You should be able to deduce from the output that the vector is being correctly initialized, since the output from the vector at the end of the program matches the command line arguments. Hence a good hypothesis to begin with is that the command line arguments are being correctly assigned to the names vector but somehow the elements at the indices input by the user are not being swapped. This suggests that we put a breakpoint on swap and start single-stepping in swap. First let's put in the breakpoint and then run the program:

 (gdb) b swap
Breakpoint 1 at 0x401284: file swap1.cpp, line 53.
(gdb) r 10 20 30 40 50
Starting program: /home/bvz/courses/140/sp18/debugging/lab2/swap 10 20 30 40 50
Please enter the integer indices of two elements: 1 3

Breakpoint 1, swap (data=std::vector of length 5, capacity 5 = {...}, 
    index1=1, index2=3) at swap1.cpp:53
 53	   string saveElement1;
  
Your output should look roughly like the output above. The line number might be a bit different depending on how many lines you used to fix the program earlier.

The first thing you should do is to verify that the input to the function is correct. If it is not, then you have made progress toward solving the problem, because you know that something must have messed up the input before swap was called. Let's try printing out the function's parameters using the command info args:

(gdb) info args
data = std::vector of length 5, capacity 5 = {"10", "20", "30", "40", "50"}
index1 = 1
index2 = 3
  
The function's input looks fine, so it is time to start single stepping through the program.

Single Stepping Through a Program

Single stepping through the program allows us to execute one statement at a time, rather than continuing to the next breakpoint. gdb provides you with two commands for doing so:

  1. step (s for short): step executes each statement and, if it encounters a function call, it will step into the function, thus allowing you to follow the flow-of-control into subroutines.

  2. next (n for short): next also executes each statement but if it encounters a function call it will execute the function as an atomic statement. In other words, it will execute all the statements in the function and in any functions that that function might call. It will seem as though you typed continue with a breakpoint set at the next statement. The one exception to this statement is that if there is a breakpoint nested in the function, then gdb will break when it reaches that breakpoint.

To see the difference between the two types of commands, place a breakpoint on the last conditional that checks to see if index2 is within the vector bounds (which is line 40 in my program but might be slightly different in yours), delete the breakpoint on the swap function, and re-run your program from scratch by typing r. gdb assumes you want to re-use the same command line arguments and hence does not require you to re-type them. gdb will ask you if you want to start from the beginning and you should reply 'y'. I have bold-faced some of the stuff you need to do at the start:

(gdb) b 40
Breakpoint 2 at 0x401148: file swap1.cpp, line 40.
(gdb) clear swap
Deleted breakpoint 1
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /home/bvz/courses/140/sp18/debugging/lab2/swap 10 20 30 40 50
Please enter the integer indices of two elements: 1 3

Breakpoint 2, main (argc=6, argv=0x7fffffffdf58) at swap2.cpp:40
40	    if (index2 > names.size()) {
  
Try typing "s" to single-step through your program:
(gdb) s
std::vector >::size (
    this=0x7fffffffde00) at /usr/include/c++/4.8.2/bits/stl_vector.h:646
646	      { return size_type(this->_M_impl._M_finish - this->_M_impl._M_start); }
(gdb) s
main (argc=6, argv=0x7fffffffdf58) at swap1.cpp:44
44	    swap(names, index1, index2);
(gdb) s
std::vector >::vector (
    this=0x7fffffffde30, __x=std::vector of length 5, capacity 8 = {...})
    at /usr/include/c++/4.8.2/bits/stl_vector.h:312
312	        _Alloc_traits::_S_select_on_copy(__x._M_get_Tp_allocator()))
(gdb) s
std::_Vector_base >::_M_get_Tp_allocator (this=0x7fffffffde00) at /usr/include/c++/4.8.2/bits/stl_vector.h:118
118	      { return *static_cast(&this->_M_impl); }
(gdb) 
  
Yuck! This probably looks like garbage to you. First gdb single-steps into the system's size function and then it single-steps into the code that copies the names vector into the data parameter vector at the start of the swap function. You can keep typing "s" but it's an exercise in futililty. It's going to take a long time to actually get to the first executable statement in swap.

A better approach is to use the "next" command ("n") for short. It will step over all this messiness. Start by re-running the program and use "n" a couple of times:

(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /home/bvz/courses/140/sp18/debugging/lab2/swap 10 20 30 40 50
Please enter the integer indices of two elements: 1 3

Breakpoint 4, main (argc=6, argv=0x7fffffffdf58) at swap1.cpp:40
40	    if (index2 > names.size()) {
(gdb) n
44	    swap(names, index1, index2);
(gdb) n
47	    for (i = 0; i < names.size(); i++) {    
This is much better as we are only executing statements in the code we wrote. Unfortunately, "next" executed the swap function without stepping into it, so we are still not sure what went wrong.

The best thing to do is to re-instate the breakpoint on swap, and then use the next command to single-step through the swap function. I will execute the first three instructions in swap and then stop:

(gdb) clear swap.cpp:40   // eliminate the previous breakpoint
(gdb) b swap              // re-instate the breakpoint on swap
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /home/bvz/courses/140/sp18/debugging/lab2/swap 10 20 30 40 50
Please enter the integer indices of two elements: 1 3

Breakpoint 6, swap (data=std::vector of length 5, capacity 5 = {...}, 
    index1=1, index2=3) at swap1.cpp:53
53	   string saveElement1;
(gdb) n
55	   saveElement1 = data[index1];
(gdb) n
56	   data[index1] = data[index2];
(gdb) n
57	   data[index2] = saveElement1;
  
You might not realize it but the first executable instruction in swap is "string saveElement1". The reason is that the program invokes the default 0-argument string constructor to initialize saveElement1. The next two statements that get executed are lines 55 and 56. It may appear as though I have also executed line 57, but gdb is just echoing out the next statement that will get executed when I enter the "n" command. By executing the first two statements in swap, I should have saved the value at index1 and copied the element from index2 to index1. Let's make sure that that is what happened:
(gdb) info args
data = std::vector of length 5, capacity 5 = {"10", "40", "30", "40", "50"}
index1 = 1
index2 = 3
(gdb) info locals
saveElement1 = "20"
(gdb)
Everything looks good. 20 has been saved to saveElement1 and 40 has been copied to index location 1. Let's execute the final statement in swap and have a look at the variables. I have bold-faced what you need to type:
(gdb) n
58	}
(gdb) info args
data = std::vector of length 5, capacity 5 = {"10", "40", "30", "20", "50"}
index1 = 1
index2 = 3
(gdb) info locals
No locals.
We have executed the last statement in swap and unfortunately when this happens, gdb de-allocates the local variables in the frame (i.e., it starts the process of popping the frame). However, fortunately we can still see the values of the arguments and confirm that swap correctly swapped the elements at locations 1 and 3. If you absolutely needed to see the value of saveElement1, you could put in a dummy statement, such as a cout statement, just to force gdb to execute one more statement in the function and stop it from de-allocating the local variables.

Locating and Fixing the Error

Okay, it's now up to you to diagnose the error and fix it. You have used gdb to confirm that swap performs the intended operation, and yet when you return to main and print the contents of name, the contents of name have not been swapped. At this point you again are going to have to use your knowledge of C++ to figure out what the problem is and correct it. You have enough information to diagnose the problem and no amount of printing or single-stepping through gdb is going to help you further. Take a few minutes to try to diagnose and fix the problem. If you cannot figure it out, the TA will tell you the answer in a few minutes.


Additional Help

You can locate additional information about gdb in three ways:

  1. using the help command in gdb
  2. typing man gdb in your unix shell
  3. surfing the internet