3.1. Debugging with GDB
GDB can help programmers find and fix bugs in their programs. GDB works with programs compiled in a variety of languages, but we focus on C here. A debugger is a program that controls the execution of another program (the program being debugged) — it allows programmers to see what their programs are doing as they run. Using a debugger can help programmers discover bugs and determine the causes of the bugs they find. Here are some useful actions that GDB can perform:
-
Start a program and step through it line by line
-
Pause the execution of a program when it reaches certain points in its code
-
Pause the execution of a program on user-specified conditions
-
Show the values of variables at the point in execution that a program is paused
-
Continue a program’s execution after a pause
-
Examine the program’s execution state at the point when it crashes
-
Examine the contents of any stack frame on the call stack
GDB users typically set breakpoints in their programs. A breakpoint specifies a point in the program where GDB will pause the program’s execution. When the executing program hits a breakpoint, GDB pauses its execution and allows the user to enter GDB commands to examine program variables and stack contents, step through the execution of the program one line at a time, add new breakpoints, and continue the program’s execution until it hits the next breakpoint.
Many Unix systems also provide the Data Display Debugger (DDD), an easy-to-use GUI wrapper around a command-line debugger program (GDB, for example). The DDD program accepts the same parameters and commands as GDB, but it provides a GUI interface with debugging menu options as well as the command line interface to GDB.
After discussing a few preliminaries about how to get started with GDB, we present two example GDB debugging sessions that introduce commonly used GDB commands in the context of finding different types of bugs. The first session (GDB on badprog.c), shows how to use GDB commands to find logic bugs in a C program. The second session (GDB on segfaulter.c) shows an example of using GDB commands to examine the program execution state at the point when a program crashes in order to discover the cause of the crash.
In the common GDB commands section, we describe commonly used GDB commands in more detail, showing more examples of some commands. In later sections, we discuss some advanced GDB features.
3.1.1. Getting Started with GDB
When debugging a program, it helps to compile it with the -g
option, which
adds extra debugging information to the binary executable file. This extra
information helps the debugger find program variables and functions in the
binary executable and enables it to map machine code instructions to lines of C
source code (the form of the program that the C programmer understands). Also,
when compiling for debugging, avoid compiler optimizations (for example, do not build
with -O2
). Compiler-optimized code is often very difficult to debug because
sequences of optimized machine code often do not clearly map back to C source
code. Although we cover the use of the -g
flag in the following sections, some
users may get better results with the -g3
flag, which can reveal extra debugging
information.
Here is an example gcc
command that will build a suitable executable for debugging with GDB:
$ gcc -g myprog.c
To start GDB, invoke it on the executable file. For example:
$ gdb a.out (gdb) # the gdb command prompt
When GDB starts, it prints the (gdb)
prompt, which allows the user to
enter GDB commands (such as setting breakpoints) before it starts running
the a.out
program.
Similarly, to invoke DDD on the executable file:
$ ddd a.out
Sometimes, when a program terminates with an error, the operating system dumps a core file containing information about the state of the program when it crashed. The contents of this core file can be examined in GDB by running GDB with the core file and the executable that generated it:
$ gdb core a.out (gdb) where # the where command shows point of crash
3.1.2. Example GDB Sessions
We demonstrate common features of GDB through two example sessions of using GDB to debug programs. The first is an example of using GDB to find and fix two bugs in a program, and the second is an example of using GDB to debug a program that crashes. The set of GDB commands that we demonstrate in these two example sessions includes:
Command | Description |
---|---|
|
Set a breakpoint |
|
Start program running from the beginning |
|
Continue execution of the program until it hits a breakpoint |
|
Quit the GDB session |
|
Allow program to execute the next line of C code and then pause it |
|
Allow program to execute the next line of C code; if the next line contains a function call, step into the function and pause |
|
List C source code around pause point or specified point |
|
Print out the value of a program variable (or expression) |
|
Print the call stack |
|
Move into the context of a specific stack frame |
Example Using GDB to Debug a Program (badprog.c)
The first example GDB session debugs the
badprog.c program. This program is supposed
to find the largest value in an array of int
values. However, when run, it
incorrectly finds that 17 is the largest value in the array instead of the
correct largest value, which is 60. This example shows how GDB can examine the
program’s runtime state to determine why the program is not computing the
expected result. In particular, this example debugging session reveals two bugs:
-
An error with loop bounds resulting in the program accessing elements beyond the bounds of the array.
-
An error in a function not returning the correct value to its caller.
To examine a program with GDB, first compile the program with -g
to add
debugging information to the executable:
$ gcc -g badprog.c
Next, run GDB on the binary executable program (a.out
). GDB initializes
and prints the (gdb)
prompt, where the user can enter GDB commands:
$ gdb ./a.out GNU gdb (Ubuntu 8.1-0ubuntu3) 8.1.0.20180409-git Copyright (C) 2018 Free Software Foundation, Inc. ... (gdb)
At this point GDB has not yet started running the program. A common first
debugging step is to set a breakpoint in the main()
function to pause the
program’s execution right before it executes the first instruction in main()
.
The break
command sets a "breakpoint" (pauses the program) at a specified
location (in this case at the start of the main()
function):
(gdb) break main Breakpoint 1 at 0x8048436: file badprog.c, line 36.
The run
command tells GDB to start the program:
(gdb) run Starting program: ./a.out
If the program takes command line arguments, provide them after
the run
command (for example, run 100 200
would run a.out
with the command line
arguments 100
and 200
).
After entering run
, GDB starts the program’s execution at its beginning,
and it runs until it hits a breakpoint. Upon reaching a breakpoint,
GDB pauses the program before executing the line of code at the breakpoint,
and prints out the breakpoint number and source code
line associated with the breakpoint. In this example, GDB pauses the
program just before executing line 36 of the program. It then
prints out the (gdb)
prompt and waits for further instructions:
Breakpoint 1, main (argc=1, argv=0x7fffffffe398) at badprog.c:36 36 int main(int argc, char *argv[]) { (gdb)
Often when a program pauses at a breakpoint, the user wants to see the C source code
around the breakpoint. The GDB list
command displays the code surrounding
the breakpoint:
(gdb) list 29 } 30 return 0; 31 } 32 33 /***************************************/ 34 int main(int argc, char *argv[]) { 35 36 int arr[5] = { 17, 21, 44, 2, 60 }; 37 38 int max = arr[0];
Subsequent calls to list
display the next lines of source code following
these. list
can also be used with a specific line number (for example, list 11
)
or with a function name to list the source code at a specified part of
the program. For example:
(gdb) list findAndReturnMax 12 * array: array of integer values 13 * len: size of the array 14 * max: set to the largest value in the array 15 * returns: 0 on success and non-zero on an error 16 */ 17 int findAndReturnMax(int *array1, int len, int max) { 18 19 int i; 20 21 if (!array1 || (len <=0) ) {
The user may want to execute one line of code at a time after hitting
a breakpoint, examining program state after each line is executed.
The GDB next
command executes just the very next line of C code.
After the program executes this line of code, GDB pauses the program again.
The print
command prints the values of program variables.
Here are a few calls to next
and print
to show their effects on the next
two lines of execution. Note that the source code line listed after
a next
has not yet been executed — it shows the line where the program
is paused, which represents the line that will be executed next:
(gdb) next 36 int arr[5] = { 17, 21, 44, 2, 60 }; (gdb) next 38 int max = arr[0]; (gdb) print max $3 = 0 (gdb) print arr[3] $4 = 2 (gdb) next 40 if ( findAndReturnMax(arr, 5, max) != 0 ) { (gdb) print max $5 = 17 (gdb)
At this point in the program’s execution, the main function has initialized
its local variables arr
and max
and is about to make a call
to the findAndReturnMax()
function. The GDB next
command executes
the next full line of C source code. If
that line includes a function call, the full execution of that function
call and its return is executed as part of a single next
command.
A user who wants to observe the execution of the function
should issue GDB’s step
command instead of the next
command:
step
steps into a function call, pausing the program before the first line
of the function is executed.
Because we suspect that the bug in this program is related to
the findAndReturnMax()
function, we want to step into the function’s
execution rather than past it. So, when paused at line 40, the step
command will next pause the program at the start of
the findAndReturnMax()
(alternately, the user could set a breakpoint
at findAndReturnMax()
to pause the program’s execution at that point):
(gdb) next 40 if ( findAndReturnMax(arr, 5, max) != 0 ) { (gdb) step findAndReturnMax (array1=0x7fffffffe290, len=5, max=17) at badprog.c:21 21 if (!array1 || (len <=0) ) { (gdb)
The program is now paused inside the findAndReturnMax
function, whose
local variables and parameters are now in scope. The print
command
shows their values, and list
displays the C source code around
the pause point:
(gdb) print array1[0] $6 = 17 (gdb) print max $7 = 17 (gdb) list 16 */ 17 int findAndReturnMax(int *array1, int len, int max) { 18 19 int i; 20 21 if (!array1 || (len <=0) ) { 22 return -1; 23 } 24 max = array1[0]; 25 for (i=1; i <= len; i++) { (gdb) list 26 if(max < array1[i]) { 27 max = array1[i]; 28 } 29 } 30 return 0; 31 } 32 33 /***************************************/ 34 int main(int argc, char *argv[]) { 35
Because we think there is a bug related to this function,
we may want to set a breakpoint inside
the function so that we can examine the runtime state part way through
its execution. In particular, setting a breakpoint on the line
when max
is changed may help us see what this function is doing.
We can set a breakpoint at a specific line number in the
program (line 27) and use the cont
command to tell GDB to let
the application’s execution continue from its paused point. Only when
the program hits a breakpoint will GDB pause the program and grab control
again, allowing the user to enter other GDB commands.
(gdb) break 27 Breakpoint 2 at 0x555555554789: file badprog.c, line 27. (gdb) cont Continuing. Breakpoint 2, findAndReturnMax (array1=0x...e290,len=5,max=17) at badprog.c:27 27 max = array1[i]; (gdb) print max $10 = 17 (gdb) print i $11 = 1
The display
command asks GDB to automatically print out the same set of
program variables every time a breakpoint is hit. For example, we will display
the values of i
, max
, and array1[i]
every time the program hits a
breakpoint (in each iteration of the loop in findAndReturnMax()
):
(gdb) display i 1: i = 1 (gdb) display max 2: max = 17 (gdb) display array1[i] 3: array1[i] = 21 (gdb) cont Continuing. Breakpoint 2, findAndReturnMax (array1=0x7fffffffe290, len=5, max=21) at badprog.c:27 27 max = array1[i]; 1: i = 2 2: max = 21 3: array1[i] = 44 (gdb) cont Continuing. Breakpoint 2, findAndReturnMax (array1=0x7fffffffe290, len=5, max=21) at badprog.c:27 27 max = array1[i]; 1: i = 3 2: max = 44 3: array1[i] = 2 (gdb) cont Breakpoint 2, findAndReturnMax (array1=0x7fffffffe290, len=5, max=44) at badprog.c:27 27 max = array1[i]; 1: i = 4 2: max = 44 3: array1[i] = 60 (gdb) cont Breakpoint 2, findAndReturnMax (array1=0x7fffffffe290, len=5, max=60) at badprog.c:27 27 max = array1[i]; 1: i = 5 2: max = 60 3: array1[i] = 32767 (gdb)
We found our first bug! The value of array1[i]
is 32767, a value not
in the passed array, and the value of i
is 5, but 5 is not a valid
index into this array. Through GDB we discovered that the for
loop bounds
need to be fixed to i < len
.
At this point, we could exit the GDB session and fix this bug in the code.
To quit a GDB session, type quit
:
(gdb) quit The program is running. Exit anyway? (y or n) y $
After fixing this bug, recompiling, and running the program, it still does not
find the correct max value (it still finds that 17 is the max value and not 60).
Based on our previous GDB run, we may suspect that there is an error in
calling or returning from the findAndReturnMax()
function. We re-run
the new version of our program in GDB, this time setting a breakpoint at
the entry to the findAndReturnMax()
function:
$ gdb ./a.out ... (gdb) break main Breakpoint 1 at 0x7c4: file badprog.c, line 36. (gdb) break findAndReturnMax Breakpoint 2 at 0x748: file badprog.c, line 21. (gdb) run Starting program: ./a.out Breakpoint 1, main (argc=1, argv=0x7fffffffe398) at badprog.c:36 36 int main(int argc, char *argv[]) { (gdb) cont Continuing. Breakpoint 2, findAndReturnMax (array1=0x7fffffffe290, len=5, max=17) at badprog.c:21 21 if (!array1 || (len <=0) ) { (gdb)
If we suspect a bug in the arguments or return value of a function, it may be
helpful to examine the contents of the stack. The where
(or bt
, for
"backtrace") GDB command prints the current state of the stack. In this
example, the main()
function is on the bottom of the stack (in frame 1) and is executing a
call to findAndReturnMax()
at line 40. The findAndReturnMax()
function is on the top of the
stack (in frame 0), and is currently paused at line 21:
(gdb) where #0 findAndReturnMax (array1=0x7fffffffe290, len=5, max=17) at badprog.c:21 #1 0x0000555555554810 in main (argc=1, argv=0x7fffffffe398) at badprog.c:40
GDB’s frame
command moves into the context of any frame on the stack. Within
each stack frame context, a user can examine the local variables and parameters
in that frame. In this example, we move into stack frame 1 (the caller’s
context) and print out the values of the arguments that the main()
function
passes to findAndReturnMax()
(for example, arr
and max
):
(gdb) frame 1 #1 0x0000555555554810 in main (argc=1, argv=0x7fffffffe398) at badprog.c:40 40 if ( findAndReturnMax(arr, 5, max) != 0 ) { (gdb) print arr $1 = {17, 21, 44, 2, 60} (gdb) print max $2 = 17 (gdb)
The argument values look fine, so let’s check the findAndReturnMax()
function’s return value.
To do this, we add a breakpoint right before findAndReturnMax()
returns to see what value it computes for its max
:
(gdb) break 30 Breakpoint 3 at 0x5555555547ae: file badprog.c, line 30. (gdb) cont Continuing. Breakpoint 3, findAndReturnMax (array1=0x7fffffffe290, len=5, max=60) at badprog.c:30 30 return 0; (gdb) print max $3 = 60
This shows that the function has found the correct max value (60). Let’s
execute the next few lines of code and see what value the main()
function
receives:
(gdb) next 31 } (gdb) next main (argc=1, argv=0x7fffffffe398) at badprog.c:44 44 printf("max value in the array is %d\n", max); (gdb) where #0 main (argc=1, argv=0x7fffffffe398) at badprog.c:44 (gdb) print max $4 = 17
We found the second bug! The findAndReturnMax()
function identifies the
correct largest value in the passed array (60), but it doesn’t
return that value back to the main()
function.
To fix this error, we need to either change findAndReturnMax()
to return
its value of max
or add a "pass-by-pointer" parameter that the
function will use to modify the value of the main()
function’s max
local variable.
Example Using GDB to Debug a Program That Crashes (segfaulter.c)
The second example GDB session (run on the segfaulter.c program) demonstrates how GDB behaves when a program crashes and how we can use GDB to help discover why the crash occurs.
In this example, we just run the segfaulter
program in GDB and let it crash:
$ gcc -g -o segfaulter segfaulter.c $ gdb ./segfaulter (gdb) run Starting program: ./segfaulter Program received signal SIGSEGV, Segmentation fault. 0x00005555555546f5 in initfunc (array=0x0, len=100) at segfaulter.c:14 14 array[i] = i;
As soon as the program crashes, GDB pauses the program’s execution
at the point it crashes and grabs control.
GDB allows a user to issue commands to examine the
program’s runtime state at the point of the program crash, often leading
to discovering why the program crashed and how to fix the cause of the crash.
The GDB where
and list
commands are particularly useful for determining where
a program crashes:
(gdb) where #0 0x00005555555546f5 in initfunc (array=0x0, len=100) at segfaulter.c:14 #1 0x00005555555547a0 in main (argc=1, argv=0x7fffffffe378) at segfaulter.c:37 (gdb) list 9 int initfunc(int *array, int len) { 10 11 int i; 12 13 for(i=1; i <= len; i++) { 14 array[i] = i; 15 } 16 return 0; 17 } 18
This output tells us that the program crashes on line 14, in the
initfunc()
function. Examining the values of the parameters and
local variables on line 14 may tell us why it crashes:
(gdb) print i $2 = 1 (gdb) print array[i] Cannot access memory at address 0x4
The value of i
seems fine, but we see an error when trying to access
index i
of array
. Let’s print out the value of array
(the
value of the base address of the array) to see if that tells us anything:
(gdb) print array $3 = (int *) 0x0
We have found the cause of the crash! The base address of the array is
zero (or NULL
), and we know that dereferencing a null pointer
(via array[i]
) causes programs to crash.
Let’s see if we can figure out why the array
parameter is NULL
by looking in
the caller’s stack frame:
(gdb) frame 1 #1 0x00005555555547a0 in main (argc=1, argv=0x7fffffffe378) at segfaulter.c:37 37 if(initfunc(arr, 100) != 0 ) { (gdb) list 32 int main(int argc, char *argv[]) { 33 34 int *arr = NULL; 35 int max = 6; 36 37 if(initfunc(arr, 100) != 0 ) { 38 printf("init error\n"); 39 exit(1); 40 } 41 (gdb) print arr $4 = (int *) 0x0 (gdb)
Moving into the caller’s stack frame and printing out the value of the
arguments main()
passes to initfunc()
shows that the main()
function
passes a null pointer to the initfunc()
function. In other words, the
user forgot to allocate the arr
array prior to the call to initfunc()
.
The fix is to use the malloc()
function to allocate some space to arr
at line 34.
These two example GDB sessions illustrate commonly used commands for finding bugs in programs. In the next section, we discuss these and other GDB commands in more detail.