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
- Break inside a loop and print the loop variable and the accumulator on three consecutive stops.
- Set a breakpoint on the line that calls a function, then use step to enter it and bt to see both frames.
- 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.
-
Why is a program built with
-g -O0for a debugging session?Answer
It keeps source lines and the code shape visible
-gstores line information and-O0stops the optimiser from moving or removing the code being inspected. -
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.
-
What is the difference between
nextandstep?Answer
nextskips a call,stepenters itnextruns the current line without entering a called function, whilestepgoes into that call. -
What does the top frame of
btshow?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.
-
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.
-
After one
nextovertotal += i, what aretotalandi?Answer
total is 1 and the next i is 2
The first addition stores 1, and the loop update advances
ito 2 before the next stop. -
What does the
info argscommand list?Answer
Every parameter of the current frame
info argsprints the parameters of the selected frame with their values. -
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
maincallingcrashon line 12. -
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.
-
Why does the page pass
set disable-randomization offto 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