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

break

Set a breakpoint

run

Start program running from the beginning

cont

Continue execution of the program until it hits a breakpoint

quit

Quit the GDB session

next

Allow program to execute the next line of C code and then pause it

step

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

List C source code around pause point or specified point

print

Print out the value of a program variable (or expression)

where

Print the call stack

frame

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:

  1. An error with loop bounds resulting in the program accessing elements beyond the bounds of the array.

  2. 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.