CDebugging with GDB

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.

Bash
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. break main, break util.c:42)

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:

C
#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:

Bash
$ 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:13
Note
The `backtrace` output immediately shows exactly which function was running when the crash happened (`sumFirstN`, at line 7) and which function called it (`main`, at line 13) — pinpointing the crash site instantly, without any guesswork or added print statements.
next 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 -g so GDB has source-level information.

  • Set a breakpoint near where you suspect the problem is, or at main to 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 backtrace to see the full call chain that led there.

  • Use watch on a specific variable when you know what is going wrong but not when it happens.

Tip
Many IDEs (VS Code, CLion, etc.) provide a graphical front-end over GDB (or LLDB) — breakpoints become clickable margins, variables appear in a sidebar, and stepping is a toolbar button. Learning the raw GDB commands still pays off, though, since it works everywhere, including over SSH on a remote server with no GUI at all.
Warning
A segmentation fault (`SIGSEGV`) means the program touched memory it isn't allowed to access — a very common outcome of bugs like out-of-bounds array access, dereferencing a `NULL` or dangling pointer, or writing past the end of an allocated buffer. GDB's `backtrace` right after a crash is usually the fastest way to locate exactly where that happened.