How to Use GDB to Debug C Programs — A Hands-On Tutorial

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>