The first time GDB feels genuinely useful is not when it prints a prompt. It is when a crash that looked random becomes one source line, one call path, and one value that could never have worked. We will deliberately break a tiny C program, follow the evidence, repair it, and then move beyond run and quit.

Install the tools and record their versions

Ubuntu terminalbash
sudo apt update
sudo apt install build-essential gdb
gdb --version
gcc --version
GNU gdb (Ubuntu ...)
gcc (Ubuntu ...) ...

Why versions belong in a bug report

  • build-essential supplies GCC and the standard native build tools.

  • gdb --version identifies debugger features and behavior available on that Ubuntu release.

  • Installation changes system packages but does not modify the project source.

  • Exact version output varies across supported Ubuntu releases.

Create a crash worth investigating

crash.cc
#include <stdio.h>
 
static void update_score(int *score)
{
    *score += 10;
}
 
int main(void)
{
    int *score = NULL;
    puts("updating score");
    update_score(score);
    printf("score: %d\n", *score);
    return 0;
}

A small, reproducible null-pointer failure across two stack frames.

The defect is small but realistic

  • score is a null pointer, not storage for an integer.

  • update_score() dereferences its parameter for both a read and a write.

  • Dereferencing NULL is undefined behavior and normally raises SIGSEGV on Ubuntu.

  • The helper creates a second frame, so GDB can show where the bad value came from.

Compile for a useful debugging session

Directory containing crash.cbash
gcc -g3 -Og -Wall -Wextra -Wpedantic crash.c -o crash
file crash
./crash
crash: ELF 64-bit ... with debug_info, not stripped
updating score
Segmentation fault (core dumped)

Those flags are deliberate

  • -g3 emits source-level debug information plus macro definitions.

  • GCC recommends -Og for the edit-compile-debug cycle because it preserves useful debug tracking.

  • Warning flags can expose suspicious C before the program runs.

  • A successful compile proves syntax and linking, not memory safety.

Catch the signal and read the stack

Program directorybash
gdb --quiet ./crash
(gdb) run
(gdb) backtrace
(gdb) frame 0
(gdb) info args
(gdb) info locals
(gdb) print score
Program received signal SIGSEGV, Segmentation fault.
update_score (score=0x0) at crash.c:5
#0 update_score (score=0x0) at crash.c:5
#1 main () at crash.c:12
$1 = (int *) 0x0

Turn the transcript into a causal story

  • run starts the inferior—the program controlled by GDB.

  • backtrace, or bt, prints frame zero at the stop point followed by callers.

  • frame 0 selects the crashing function for argument and local-variable inspection.

  • score=0x0 connects the faulting dereference to the value passed by main.

Stop before the invalid access

GDB promptbash
(gdb) break update_score
(gdb) run
(gdb) list
(gdb) print score
(gdb) up
(gdb) list
(gdb) print score
(gdb) next
Breakpoint 1, update_score (score=0x0) at crash.c:5
$1 = (int *) 0x0
#1 main () at crash.c:12
  • break update_score stops at function entry before the dereference.

  • list shows nearby source and print evaluates a C expression in the selected frame.

  • up selects the caller and reveals where NULL originated.

  • next steps over calls; use step when entering a called function is useful.

Fix the ownership error

crash.c (fixed main)c
int main(void)
{
    int score = 0;
 
    puts("updating score");
    update_score(&score);
    printf("score: %d\n", score);
    return 0;
}

What changed in memory

  • score is now an actual integer object in the main stack frame.

  • &score passes a valid address to the helper.

  • That pointer remains valid until main returns.

  • The caller prints the integer directly instead of dereferencing a nullable pointer.

Verify the fix reproducibly

Program directorybash
gcc -g3 -Og -Wall -Wextra -Wpedantic crash.c -o crash
gdb --quiet --batch -ex 'break update_score' -ex run -ex 'print *score' -ex continue ./crash
Breakpoint 1, update_score (...) at crash.c:5
$1 = 0
score: 10
Inferior 1 exited normally

A debugger session can be a regression check

  • --batch exits after scripted commands instead of waiting at a prompt.

  • Each -ex executes one GDB command in order.

  • print *score proves the pointer references readable integer storage before the update.

  • Normal exit verifies this path and input, not every possible use of the function.

Watch data change

GDB promptbash
(gdb) break main
(gdb) run
(gdb) next
(gdb) watch score
(gdb) continue
Hardware watchpoint 2: score
Old value = 0
New value = 10

Watchpoints ask a different question

  • A breakpoint asks where execution arrives; a watchpoint asks when an expression changes.

  • Hardware watchpoint capacity is limited by the CPU and target.

  • The variable must be in scope when the watchpoint is created.

  • This technique is valuable when state becomes corrupt long before a later crash.

When GDB output looks incomplete

  • No debugging symbols found: rebuild with -g and use the executable that exactly matches the failure.

  • Value optimized out: reproduce with -Og, or inspect assembly and registers in the optimized build.

  • Corrupt backtrace: stack memory may be overwritten, symbols may mismatch, or unwind information may be unavailable.

  • Cannot attach: ptrace policy, process ownership, or container capabilities may deny access.

  • Crash disappears under GDB: timing, uninitialized state, or a race may be involved; preserve core dumps and inputs.

Primary references