Your C program compiles cleanly, runs without crashing — and prints the wrong answer. You could scatter printf lines through the code and recompile ten times, or you could watch the program run, line by line, inspecting any variable at any moment. That second option is GDB, the GNU Debugger, and this tutorial teaches it by doing: we’ll hunt down a real bug that produces garbage output, then autopsy a real segfault. Every command and every line of output below was captured from an actual session with GDB 15.1 on Ubuntu 24.04.
Step 1 — Install GDB
On Ubuntu/Debian (including WSL on Windows):
sudo apt install gdb
On Fedora it’s sudo dnf install gdb. On Windows with MSYS2 (see our GCC on Windows 11 guide), it’s pacman -S mingw-w64-ucrt-x86_64-gdb. Check it worked:
gdb --version
GNU gdb (Ubuntu 15.1-1ubuntu1~24.04.1) 15.1
(macOS note: Apple ships LLDB instead, and GDB on Apple Silicon is a rough ride — if you’re on a Mac, use LLDB or a Linux VM for this tutorial.)
Step 2 — Compile With Debug Symbols: the -g Flag
GDB needs a map between the machine code and your source lines. The -g flag embeds it:
gcc -g -Wall -Wextra -o average average.c
Without -g, GDB still runs but shows raw addresses instead of your variable names and line numbers. Make -g a reflex in every development build — it costs nothing at runtime.
The Bug We’re Hunting
Here’s average.c — it should average five exam scores:
#include <stdio.h>
double average(int *values, int count)
{
int sum = 0;
int i;
for (i = 0; i <= count; i++)
sum += values[i];
return (double)sum / count;
}
int main(void)
{
int scores[5] = {90, 85, 78, 92, 88};
printf("Average: %.2f\n", average(scores, 5));
return 0;
}
The scores average to 86.60. The program says otherwise:
$ ./average Average: 13193.60
No crash, no warning (this compiles clean even with -Wall -Wextra) — just a spectacularly wrong number. Time for the debugger.
Step 3 — Breakpoints, Stepping, and print
Start GDB on the binary, set a breakpoint on the suspicious function, and run:
$ gdb ./average (gdb) break average Breakpoint 1 at 0x864: file average.c, line 5. (gdb) run Breakpoint 1, average (values=0xfffffffffbb0, count=5) at average.c:5 5 int sum = 0;
The program is now frozen at the first line of average, and GDB already shows the arguments: count=5, as expected. Step through lines with next and inspect anything with print:
(gdb) next 7 for (i = 0; i <= count; i++) (gdb) next 8 sum += values[i]; (gdb) next 7 for (i = 0; i <= count; i++) (gdb) print i $1 = 1 (gdb) print sum $2 = 90 (gdb) print values[1] $3 = 85
After one loop iteration: i is 1, sum is 90, and the next value queued up is 85. All correct so far — stepping every iteration by hand would take forever. We need to fast-forward to where things go wrong.
Step 4 — Conditional Breakpoints: Fast-Forward to the Bug
The array has five elements, indices 0–4. If the loop ever reaches i == 5, it’s off the end. Ask GDB to stop exactly there:
(gdb) break average.c:8 if i == 5 Breakpoint 2 at 0x870: file average.c, line 8. (gdb) run Breakpoint 2, average (values=0xfffffffffbb0, count=5) at average.c:8 8 sum += values[i];
It stopped — which already proves the loop runs too far. Now look at the crime scene:
(gdb) print i $1 = 5 (gdb) print values[i] $2 = 65535 (gdb) print sum $3 = 433
There’s the whole story in three numbers. sum is 433 — which is exactly 90+85+78+92+88, the correct total. But the loop is about to add values[5], which doesn’t exist; it’s whatever garbage sits past the array (65535 on this run). And indeed: (433 + 65535) / 5 = 13193.60 — the exact wrong answer we saw. The bug is the <= in the loop condition; it should be <. One character, found in one conditional breakpoint.
for (i = 0; i < count; i++) /* fixed */
$ gcc -g -o average average.c && ./average Average: 86.60
Step 5 — Debugging a Crash: backtrace
Wrong answers need stepping; crashes need backtrace. Here’s a program that dies:
#include <stdio.h>
#include <string.h>
void print_length(const char *text)
{
printf("Length: %zu\n", strlen(text));
}
int main(void)
{
const char *name = NULL;
print_length("hello");
print_length(name);
return 0;
}
$ ./crash Length: 5 Segmentation fault (core dumped)
Run it under GDB and the crash freezes instead of killing the process:
(gdb) run Program received signal SIGSEGV, Segmentation fault. __strlen_asimd () at ../sysdeps/aarch64/multiarch/strlen_asimd.S:96 (gdb) bt #0 __strlen_asimd () at ../sysdeps/aarch64/multiarch/strlen_asimd.S:96 #1 0x0000aaaaaaaa07ec in print_length (text=0x0) at crash.c:6 #2 0x0000aaaaaaaa0828 in main () at crash.c:13
bt (backtrace) prints the call stack, innermost frame first. Frame #0 is inside the C library’s strlen (the exact name varies by CPU — __strlen_avx2 on Intel, __strlen_asimd on ARM). That’s not where the bug is; library code is almost never the culprit. Read down to the first frame in your code: frame #1, and GDB has already printed the smoking gun — text=0x0. Confirm it by selecting the frame:
(gdb) frame 1
#1 0x0000aaaaaaaa07ec in print_length (text=0x0) at crash.c:6
6 printf("Length: %zu\n", strlen(text));
(gdb) print text
$1 = 0x0
strlen(NULL) — and frame #2 tells you who passed it: main at line 13. Two commands from segfault to root cause.
The Commands You’ll Actually Use
| Command | Short | What it does |
|---|---|---|
break average / break file.c:8 |
b |
Stop at a function or line |
break file.c:8 if i == 5 |
Stop only when a condition holds | |
run |
r |
Start the program |
next |
n |
Step one line, over function calls |
step |
s |
Step one line, into function calls |
print expr |
p |
Evaluate and show any expression |
continue |
c |
Resume until the next breakpoint |
backtrace |
bt |
Show the call stack |
frame N |
f |
Jump to stack frame N |
watch var |
Stop whenever a variable changes | |
quit |
q |
Leave GDB |
Bonus: gdb -tui ./average opens a split view with your source on top and the command line below — press Ctrl-X A to toggle it any time.
Common Issues
| Problem | Fix |
|---|---|
| GDB shows addresses, “No symbol table” | Recompile with -g |
| Breakpoint “pending” / never hits | Typo in function/file name, or the binary is stale — rebuild |
Variable prints <optimized out> |
Compiled with -O2 — debug with -O0 -g (or -Og) |
| Line numbers don’t match the source | Binary is older than the file — recompile |
| “ptrace: Operation not permitted” in containers | Run the container with --cap-add=SYS_PTRACE |
What’s Next
GDB finds the bugs that produce wrong output or crashes. For the bugs that don’t announce themselves — memory leaks and heap corruption — pair it with our companion guide: Find Memory Leaks in C with Valgrind.
Building multi-file projects to debug? Stop typing three gcc commands each time — learn Make in our beginner Makefile tutorial.
Want crashes to practise on? Our C quiz explainer series demonstrates real segfaults — from writing to string literals to freeing invalid pointers — all ideal GDB fodder.
As an Amazon Associate we earn from qualifying purchases.
Recommended Book
Debugging is half of programming, and no book teaches disciplined C like The C Programming Language by Kernighan & Ritchie (Amazon.com). Every example in it is worth stepping through in GDB.