Debugging with gdb

A debugger stops the program at a chosen line and lets you look at the variables before it continues. The tool used here is gdb, and the binary must be built with -g so that the line numbers and names are in the executable. Lesson 2 built the first session; this lesson uses the same commands for real questions.

A breakpoint

A breakpoint names a function or a source line. When the program reaches it, gdb stops before the line runs and prints where it is and with which arguments. -batch runs a list of commands and exits, which is the form this page can reproduce.

#include <stdio.h> static int sum_to(int n) { int total = 0; for (int i = 1; i <= n; i++) { total += i; } return total; } int main(void) { printf("%d\n", sum_to(5)); return 0; }

gcc -g -O0 -Wall -Wextra sum.c -o sum
gdb -batch -ex 'set disable-randomization off' -ex 'break sum_to' -ex run -ex 'print n' -ex 'bt 2' ./sum

Breakpoint 1 at 0x118c: file sum.c, line 5.

Breakpoint 1, sum_to (n=5) at sum.c:5
5 int total = 0;
$1 = 5
#0 sum_to (n=5) at sum.c:5
#1 0x00005ae1497251c1 in main () at sum.c:15

The prompt is not shown in batch mode; the lines are the breakpoint location, the stop, the printed argument and a two-frame backtrace. The address changes on every run, the file and line do not. The set disable-randomization off is needed in a container that does not allow the debugger to turn address randomisation off.

Stepping

next runs the current line and stops at the next one, without entering a function call. step goes into the call. Starting at the body of the loop, two print commands followed by next show how the variables change from iteration to iteration.

#include <stdio.h> static int sum_to(int n) { int total = 0; for (int i = 1; i <= n; i++) { total += i; } return total; } int main(void) { printf("%d\n", sum_to(5)); return 0; }

gdb -batch -ex 'set disable-randomization off' -ex 'break sum.c:8' -ex run -ex 'print i' -ex 'print total' -ex next -ex 'print total' -ex next -ex 'print i' ./sum

Breakpoint 1 at 0x119c: file sum.c, line 8.

Breakpoint 1, sum_to (n=5) at sum.c:8
8 total += i;
$1 = 1
$2 = 0
7 for (int i = 1; i <= n; i++) {
$3 = 1

Breakpoint 1, sum_to (n=5) at sum.c:8
8 total += i;
$4 = 2

Before the first addition i is 1 and total is 0. After the line total is 1, and the loop reaches the next iteration with i equal to 2. The breakpoint stops again on the same line, so continue would repeat the same steps with larger values.

Inspecting arguments

info args prints every parameter of the current frame with its value, and print reads a single expression. A function with two parameters makes the difference visible. The frame also shows the caller, which is how a parameter value is traced back to the call.

#include <stdio.h> static int rectangle_area(int width, int height) { int area = width * height; return area; } int main(void) { printf("%d\n", rectangle_area(3, 4)); return 0; }

gcc -g -O0 -Wall -Wextra args.c -o args
gdb -batch -ex 'set disable-randomization off' -ex 'break rectangle_area' -ex run -ex 'print width' -ex 'print height' -ex 'info args' ./args

Breakpoint 1 at 0x118f: file args.c, line 5.

Breakpoint 1, rectangle_area (width=3, height=4) at args.c:5
5 int area = width * height;
$1 = 3
$2 = 4
width = 3
height = 4

gdb already prints the arguments on the stop line, and info args repeats them in a list. Printing area before the assignment would show an indeterminate value, which is why the breakpoint is placed on the first line of the body rather than after the multiplication.

A backtrace

When a program crashes, gdb stops at the faulting instruction and bt prints the call stack. The top frame is where the fault happened, and the frames below show who called it. For a null pointer dereference the faulting line is the proof.

#include <stdio.h> static void crash(void) { int *pointer = NULL; printf("%d\n", *pointer); } int main(void) { crash(); return 0; }

gcc -g -O0 -Wall -Wextra crash.c -o crash
gdb -batch -ex 'set disable-randomization off' -ex run -ex bt ./crash

Program received signal SIGSEGV, Segmentation fault.
0x000061e9d1bb5199 in crash () at crash.c:7
7 printf("%d\n", *pointer);
#0 0x000061e9d1bb5199 in crash () at crash.c:7
#1 0x000061e9d1bb51bd in main () at crash.c:12

The crash is on line 7, the dereference, and the caller is main on line 12. The addresses are run-specific; the file, the line and the function names are what a report should quote. A backtrace with symbols turns "it crashes somewhere" into a line to read.

No debug information

Without -g the executable has no mapping from addresses to source lines, so the same crash produces a backtrace with a bare address and no line numbers. The optimiser is also free to inline crash into main, which is why even the function name disappears.

#include <stdio.h> static void crash(void) { int *pointer = NULL; printf("%d\n", *pointer); } int main(void) { crash(); return 0; }

gcc -O2 -Wall -Wextra crash.c -o crash_nodebug
gdb -batch -ex 'set disable-randomization off' -ex run -ex bt ./crash_nodebug


Program received signal SIGSEGV, Segmentation fault.
0x0000609e9bc5f030 in main ()
#0 0x0000609e9bc5f030 in main ()

The comparison is the lesson: build the version you want to debug with -g -O0, and keep the optimised build for benchmarks. The flags themselves are the subject of the next lesson 62, and the empty line at the start of the output is the shell's own separator from the build.

Exercises

  1. Break inside a loop and print the loop variable and the accumulator on three consecutive stops.
  2. Set a breakpoint on the line that calls a function, then use step to enter it and bt to see both frames.
  3. Compile a crashing program with and without -g, run bt on both, and list the differences in the output.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. Why is a program built with -g -O0 for a debugging session?

    Answer

    It keeps source lines and the code shape visible

    -g stores line information and -O0 stops the optimiser from moving or removing the code being inspected.

  2. When does a breakpoint on a line stop the program?

    Answer

    Before the statement on that line runs

    The stop happens before the statement, so the variables still show the state the line is about to use.

  3. What is the difference between next and step?

    Answer

    next skips a call, step enters it

    next runs the current line without entering a called function, while step goes into that call.

  4. What does the top frame of bt show?

    Answer

    The function and line where the stop happened

    The top frame is the current instruction, and the frames below show the callers that led to it.

  5. When a breakpoint stops on the first line of a function, what do its parameters already show?

    Answer

    The values passed by the caller

    The call has already set up the frame, so the parameters hold the argument values before the body executes.

  6. After one next over total += i, what are total and i?

    Answer

    total is 1 and the next i is 2

    The first addition stores 1, and the loop update advances i to 2 before the next stop.

  7. What does the info args command list?

    Answer

    Every parameter of the current frame

    info args prints the parameters of the selected frame with their values.

  8. Which line and caller does the crash backtrace name?

    Answer

    crash.c line 7, called from main line 12

    The fault is the dereference on line 7, and the next frame shows main calling crash on line 12.

  9. What does a backtrace lose when the binary is built without -g?

    Answer

    The source lines and often the function names

    Without debug information GDB shows bare addresses, and optimisation may inline the function out of the trace.

  10. Why does the page pass set disable-randomization off to GDB?

    Answer

    The container does not allow disabling ASLR

    The debugger cannot turn address randomisation off in this environment, so the setting is disabled explicitly.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 and gdb 16.3 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev