When your C program crashes with a segfault, the first instinct is to add printf statements until the problem appears. GDB is faster. It stops the program at the exact line of the crash, shows you the call stack, and lets you inspect every variable. Here is how to actually use it.
Compile for Debugging First
gcc -g -O0 -Wall -o program program.c
-g embeds debug symbols (line numbers, variable names). -O0 disables optimization — optimized code reorders instructions in ways that confuse the debugger. Always debug with -O0.
Finding a Segfault — The Core Workflow
/* buggy.c */
#include <stdio.h>
void process(int *arr, int len) {
for (int i = 0; i <= len; i++) { /* off-by-one: should be i < len */
arr[i] *= 2;
}
}
int main() {
int data[] = {1, 2, 3, 4, 5};
process(data, 5);
return 0;
}
gcc -g -O0 -o buggy buggy.c
gdb ./buggy
Inside GDB:
(gdb) run
Program received signal SIGSEGV, Segmentation fault.
0x000000000040114a in process (arr=0x7fffffffe560, len=5) at buggy.c:5
5 arr[i] *= 2;
(gdb) backtrace
#0 0x000000000040114a in process (arr=0x7fffffffe560, len=5) at buggy.c:5
#1 0x0000000000401168 in main () at buggy.c:11
(gdb) print i
$1 = 5
(gdb) print arr[5]
Cannot access memory at address 0x7fffffffe574
GDB tells you: the crash is at buggy.c:5, in the process function, and i == 5 when it crashes — exactly one past the valid index range. Case closed.
Essential GDB Commands
| Command | Shortcut | What it does |
|---|---|---|
run |
r |
Start the program |
backtrace |
bt |
Show the call stack |
frame N |
f N |
Switch to stack frame N |
print expr |
p expr |
Print value of expression |
info locals |
Show all local variables | |
break file:line |
b |
Set a breakpoint |
next |
n |
Step over (execute next line) |
step |
s |
Step into (enter function calls) |
continue |
c |
Resume until next breakpoint |
quit |
q |
Exit GDB |
Setting Breakpoints and Inspecting State
(gdb) break main /* break at start of main */
(gdb) break buggy.c:5 /* break at line 5 */
(gdb) run
Breakpoint 1, main () at buggy.c:10
(gdb) next /* step to next line */
(gdb) print data /* prints array: {1, 2, 3, 4, 5} */
$2 = {1, 2, 3, 4, 5}
(gdb) step /* step INTO process() */
(gdb) watch i /* watchpoint: stop when i changes */
(gdb) continue
Debugging a Null Pointer Crash
/* null_crash.c */
#include <stdlib.h>
typedef struct Node {
int val;
struct Node *next;
} Node;
int count(Node *head) {
int n = 0;
while (head != NULL) { /* BUG: should check head, not head->next */
head = head->next;
n++;
}
return n;
}
int main() {
Node *n = malloc(sizeof(Node));
n->val = 1;
n->next = NULL;
count(n);
free(n);
return 0;
}
(gdb) run
(gdb) bt
#0 0x... in count (head=0x0) at null_crash.c:12
#1 0x... in main () at null_crash.c:22
(gdb) f 0
(gdb) info locals
head = 0x0
n = 1
head is 0x0 (null) when it tries to dereference it. The issue in this function is actually correct — the loop stops when head == NULL. But the crash tells you to look at what’s being passed in, not the loop logic. Switch frames with f 1 to inspect main.
Debugging a Core Dump
/* Enable core dumps first */
ulimit -c unlimited
./program /* crashes — generates core file */
gdb ./program core /* load the core dump into GDB */
(gdb) bt /* show what happened */
Core dumps capture the full program state at the moment of a segfault. You can analyze a crash that happened in production without reproducing it, using the core file and the original binary with debug symbols.
Conditional Breakpoints
(gdb) break process:5 if i == 4
(gdb) run
Conditional breakpoints stop execution only when the condition is true. When you have a loop that runs 10,000 times and crashes on iteration 9,999, a conditional breakpoint on i == 9998 stops just before the crash — saving you 9,998 manual continues.
One Tip That Saves Hours
Before using GDB, compile with -fsanitize=address,undefined. AddressSanitizer will print the exact line, the access pattern, and a full stack trace — often faster than starting a GDB session. GDB is most valuable when ASan cannot catch the issue (e.g., logic errors, wrong values) or when you need to inspect state interactively. Use our gcc compiler online to try sanitizer flags before setting up a local GDB session.
TL;DR
- Compile with
-g -O0before debugging — optimized code confuses GDB run→backtrace→frame N→printis the core crash investigation workflowinfo localsshows all variables in the current frame- Watchpoints (
watch var) stop execution when a variable changes — great for catching corruption - Conditional breakpoints:
break func:line if condition— saves time in loops - Core dumps let you analyze production crashes post-mortem
- Try AddressSanitizer first — it often gives the answer faster than a GDB session