Debugging with GDB
Sprinkling printf statements throughout your code to figure out what's going wrong is a time-honored technique, but it has real limits: you have to guess in advance what to print, recompile every time you want to check something new, and it quickly clutters real code. GDB (the GNU Debugger) lets you pause a running program, inspect its actual state, and step through execution line by line — no recompiling required to ask a new question.
Why print-Debugging Has Limits
You have to predict beforehand exactly what information you'll need to see.
Every new question means adding a
printf, recompiling, and rerunning.Print statements can't easily inspect complex state on demand, or pause execution partway through a loop to examine things interactively.
Removing all the debug prints afterward is its own chore (and easy to forget).
Compiling with -g
For GDB to show meaningful source line numbers, variable names, and function names (instead of raw memory addresses), compile with the -g flag, which embeds debugging symbols into the executable.
gcc -g -Wall program.c -o program
Essential GDB Commands
Command | Effect |
|---|---|
break <location> | Set a breakpoint at a function name or file:line (e.g. |
run | Start (or restart) the program under GDB, stopping at the first breakpoint |
next | Execute the next line, stepping OVER function calls |
step | Execute the next line, stepping INTO function calls |
continue | Resume execution until the next breakpoint or the program ends |
print <expr> | Evaluate and display the value of a variable or expression |
backtrace | Show the current call stack — which function called which, all the way up |
watch <expr> | Pause execution automatically whenever the value of an expression changes |
Worked Example Session
Consider this small (buggy) program, compiled with gcc -g buggy.c -o buggy:
#include <stdio.h>
#include <stdlib.h>
int sumFirstN(int *values, int n) {
int total = 0;
for (int i = 0; i <= n; i++) { /* bug: should be i < n */
total += values[i];
}
return total;
}
int main(void) {
int numbers[5] = { 10, 20, 30, 40, 50 };
int result = sumFirstN(numbers, 5);
printf("Sum: %d\n", result);
return 0;
}The off-by-one loop condition reads one element past the end of the array — likely to misbehave or segfault. Here's how a GDB session might diagnose it:
$ gdb ./buggy
(gdb) break sumFirstN
Breakpoint 1 at 0x1169: file buggy.c, line 6.
(gdb) run
Starting program: ./buggy
Breakpoint 1, sumFirstN (values=0x7fffffffe3a0, n=5) at buggy.c:6
6 for (int i = 0; i <= n; i++) {
(gdb) next
7 total += values[i];
(gdb) print i
$1 = 0
(gdb) watch i
Hardware watchpoint 2: i
(gdb) continue
... (GDB stops each time "i" changes, letting you watch it climb toward the bug) ...
(gdb) print n
$2 = 5
(gdb) # i is allowed to reach 5 -- but valid indices for a 5-element
(gdb) # array are only 0-4. That confirms the off-by-one bug.
(gdb) continue
Program received signal SIGSEGV, Segmentation fault.
sumFirstN (values=0x7fffffffe3a0, n=5) at buggy.c:7
7 total += values[i];
(gdb) backtrace
#0 sumFirstN (values=0x7fffffffe3a0, n=5) at buggy.c:7
#1 0x0000555555555195 in main () at buggy.c:13next vs step
The distinction between next and step matters constantly during a debugging session: use next when you trust a function call and just want to move past it, and step when you specifically want to follow execution into that function's body.
Common Workflow
Compile with
-gso GDB has source-level information.Set a breakpoint near where you suspect the problem is, or at
mainto start from the beginning.Run the program and step through it, printing variables as you go to compare actual vs. expected values.
When the program crashes or misbehaves, use
backtraceto see the full call chain that led there.Use
watchon a specific variable when you know what is going wrong but not when it happens.