Introduction to GDB for C Debugging

GDB (GNU Debugger) is the go‑to debugger for C and C++ programs on Linux and many Unix‑like systems. It allows you to see what is happening inside your program while it runs or when it crashes. Mastering GDB is essential for efficiently fixing segmentation faults, memory leaks, logic errors, and other common C programming mistakes. This guide provides a comprehensive walkthrough of using GDB to debug real‑world C issues, from basic breakpoints to advanced features like watchpoints and reverse debugging.

Whether you are a student learning C or an experienced developer maintaining large codebases, this article will help you transition from guess‑based debugging to a systematic, tool‑driven approach. By the end, you will be able to set up your environment, interpret GDB output, and resolve errors faster.

Setting Up for Debugging with GDB

Compiling with Debug Symbols

GDB relies on debug symbols to map machine instructions back to source code. To include these symbols, compile your C program with the -g flag:

gcc -g -o myprogram myprogram.c

You can also use -ggdb to produce GDB‑specific symbols, but plain -g works well for most scenarios. Avoid using optimization flags like -O2 while debugging, as they can reorder instructions and make debugging confusing. Use -O0 (no optimization) for the clearest experience.

If you have multiple source files, compile them all with -g and link normally:

gcc -g -c file1.c file2.c
gcc -g -o myprogram file1.o file2.o

Starting GDB and Basic Commands

Launch GDB with your compiled program:

gdb ./myprogram

Once inside the GDB shell, you have access to many commands. The most frequently used are:

  • break <function> or break <file>:<line> – Set a breakpoint that halts execution at a specific location.
  • run – Start the program. You can pass command‑line arguments: run arg1 arg2.
  • next (alias n) – Execute the next source line, stepping over function calls.
  • step (alias s) – Step into a function call to debug its internals.
  • print <variable> (alias p) – Display the current value of a variable.
  • backtrace (alias bt) – Show the call stack after a crash or breakpoint.
  • continue (alias c) – Resume execution until the next breakpoint.
  • quit – Exit GDB.

GDB also supports tab completion and command history. Familiarising yourself with these basics will let you start debugging immediately.

Essential GDB Command Reference

For quick lookup, here is a table of commands you will use often:

  • list (or l) – Show source code around the current line.
  • info breakpoints – List all breakpoints and their status.
  • delete <n> – Remove breakpoint number n.
  • display <variable> – Automatically print a variable after each stop.
  • watch <expression> – Halt execution when the expression’s value changes.
  • frame <n> – Select a specific stack frame for inspection.
  • up / down – Move up or down the call stack.
  • whatis <variable> – Display the data type of a variable.
  • ptype – Show detailed type information (especially useful for structs).
  • call <function(args)> – Call a function within the debugged program.

You can also define your own commands or scripts using define and source.

Common C Programming Errors and How to Debug Them with GDB

Segmentation Faults

Causes and Symptoms

Segmentation faults (segfaults) occur when a program tries to access memory it does not have permission to use — for example, dereferencing a NULL pointer, writing past the end of an array, or using a dangling pointer. The operating system sends a SIGSEGV signal, and the program terminates abruptly. Without a debugger, you are left guessing which line caused the crash.

Using GDB to Diagnose

Start GDB and run the program. GDB will catch the signal and pause at the exact instruction that caused the fault. Then you can examine the state:

(gdb) run
Program received signal SIGSEGV, Segmentation fault.
0x00005555555546a7 in main () at myprogram.c:42
42        *ptr = 10;
(gdb) print ptr
$1 = (int *) 0x0

Here ptr is NULL. The backtrace command shows the sequence of function calls leading up to the crash:

(gdb) backtrace
#0  0x00005555555546a7 in main () at myprogram.c:42
#1  0x00007ffff7a2d840 in __libc_start_main (...)

You can then inspect the offending variable and fix the code (e.g., allocate memory before use).

Example Walkthrough

Consider this faulty program:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *arr = NULL;
    arr[0] = 5;  // segfault
    return 0;
}

Compile with gcc -g -o segfault_ex segfault_ex.c. Run GDB:

gdb ./segfault_ex
(gdb) run
Program received signal SIGSEGV, Segmentation fault.
0x00005555555546a0 in main () at segfault_ex.c:7
7           arr[0] = 5;
(gdb) print arr
$1 = (int *) 0x0

Now you know arr is NULL. Correct by allocating memory: arr = malloc(sizeof(int));.

Memory Leaks and Invalid Pointers

Memory leaks happen when allocated memory is never freed. While GDB alone does not detect leaks, it helps track pointer behaviour. Use breakpoints to see where memory is allocated and where it remains unfreed. For deep leak analysis, combine GDB with Valgrind (valgrind.org), which reports leaks and invalid accesses.

Invalid pointers (e.g., using memory after free) cause undefined behaviour. GDB can catch the moment of misuse if you set a watchpoint on the pointer variable:

(gdb) watch *ptr
(gdb) continue

The watchpoint triggers when the memory pointed to by ptr is read or written. This is invaluable for finding use‑after‑free bugs.

Logic Errors

Not all bugs lead to crashes. Logic errors produce wrong results while the program runs to completion. GDB helps by letting you step through code line by line and inspect variable values.

Setting Conditional Breakpoints

If your bug only manifests under certain conditions (e.g., a loop index equals 1000), set a conditional breakpoint:

(gdb) break 42 if i == 1000
(gdb) run

This saves time by only stopping when the condition is true. You can then examine neighbouring variables.

Stepping Through Code

Use next and step to follow the execution path. For example, if an if branch seems to be taken incorrectly, set a breakpoint at the condition, then use print to see which variables differ from what you expect. You can even modify a variable on the fly with set variable x = 5 to test scenarios without recompiling.

Buffer Overflows

Buffer overflows overwrite adjacent memory and often lead to subtle bugs or security vulnerabilities. GDB combined with address sanitizers (-fsanitize=address) is a powerful approach. Compile your program with sanitizers and GDB together:

gcc -g -fsanitize=address -o myprogram myprogram.c

Then run under GDB. The sanitizer will print detailed error messages, and GDB will catch the SIGABRT so you can inspect the stack. For simple overflow detection, you can also set a watchpoint on the memory after a buffer.

Advanced GDB Features for Debugging

Watchpoints

Watchpoints break execution when a variable’s value changes. They are ideal for tracing how a variable gets corrupted. Use:

(gdb) watch myVar

For a pointer, use watch *ptr to monitor the pointed‑to memory. GDB supports read watchpoints (rwatch) and read/write watchpoints (awatch). Hardware watchpoints are used if available, otherwise GDB simulates them by single‑stepping, which is slower but works.

Backtraces and Stack Inspection

When your program crashes or hits a breakpoint, backtrace prints the call stack. Use frame N to switch to a specific frame and inspect local variables. This is especially helpful for understanding how you reached a buggy function.

You can also examine registers with info registers and disassemble code with disassemble.

Reverse Debugging (Record and Replay)

GDB supports a powerful feature called reverse debugging that lets you step backwards through program execution. This is invaluable when you missed the moment a variable became corrupted. To enable:

(gdb) target record-full
(gdb) run
(gdb) reverse-step   # step backward
(gdb) reverse-next   # next backward
(gdb) reverse-continue

Note that record‑and‑replay can be memory‑intensive, but for intermittent bugs it saves hours of logging.

Debugging Multithreaded Programs

GDB supports debugging multithreaded programs using pthreads. Use info threads to list all threads. Switch with thread N. Set thread‑specific breakpoints: break function thread N. Watchpoints work across threads. Common issues like data races can be caught by setting watchpoints on shared variables and checking which thread modifies them.

Integrating GDB with Other Tools

While GDB is excellent for interactive debugging, it pairs well with static analysis tools and memory checkers:

  • Valgrind (valgrind.org) – Run your program with valgrind --tool=memcheck ./myprogram to detect leaks and invalid accesses before debugging the root cause with GDB.
  • AddressSanitizer – Integrated into GCC/Clang (-fsanitize=address). It provides detailed reports on buffer overflows and use‑after‑free, and GDB can catch the abort.
  • Ghidra / IDA Pro – For reverse engineering assembly, though GDB’s own disassembly is sufficient for most debugging needs.
  • gprof / perf – For profiling performance, often complementary to debugging.

For an in‑depth reference, see the official GDB documentation. Another excellent resource is the Learn C interactive tutorial for practicing basics.

Best Practices for Debugging C Programs with GDB

  1. Compile with debug symbols and no optimisation – Use -g -O0 during development. Optimisation makes GDB lose track of variables and line numbers.
  2. Use a consistent naming convention for breakpoints (e.g., br main always). Utilise GDB’s .gdbinit file to set default commands.
  3. Write small reproducible test cases – Isolate the bug into a minimal program to speed up debugging.
  4. Combine with version control – Use git bisect to find which commit introduced the bug, then use GDB on the broken commit.
  5. Log variable changes with display – Instead of manually typing print at each break, use display var to auto‑print.
  6. Keep GDB up to date – Newer versions have better reverse debugging, multi‑thread support, and Python scripting.
  7. Learn GDB scripting (Python) – You can write custom pretty‑printers for complex data structures and automate repetitive tasks.

Conclusion

Debugging is an essential skill in every C programmer’s toolkit, and GDB provides the visibility and control needed to fix errors quickly. From segmentation faults to subtle logic bugs, setting breakpoints, inspecting variables, and using watchpoints can turn hours of frustration into a systematic process. Advanced features like reverse debugging and multithreaded support allow you to tackle even the most elusive bugs.

Start small: compile your next program with -g, run it under GDB, and practice stepping through code. Combine GDB with Valgrind or AddressSanitizer for memory issues, and you will catch the majority of C programming errors before they reach production. Over time, you will not only debug faster but also write more robust code as you learn to prevent these errors in the first place.