Debug a Segfault with GDB

Goal: Take a program that crashes with a segmentation fault. Use GDB to find the exact bug — without adding a single printf.

Prerequisites: Debugging with GDB, Pointers and Memory, C Language Essentials


The Buggy Program

Save this as buggy.c. It has three bugs that cause crashes. Your job: find and fix them all using GDB.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
typedef struct {
    char *name;
    int score;
} Player;
 
Player *create_player(const char *name, int score) {
    Player *p = malloc(sizeof(Player));
    p->name = strdup(name);
    p->score = score;
    return p;
}
 
void free_player(Player *p) {
    free(p->name);
    free(p);
}
 
// Bug 1 is somewhere in this function
char *get_greeting(Player *p) {
    char buf[64];
    snprintf(buf, sizeof(buf), "Hello, %s! Score: %d", p->name, p->score);
    return buf;   // what's wrong here?
}
 
// Bug 2 is somewhere in this function
void print_top_players(Player **players, int count) {
    for (int i = 0; i <= count; i++) {   // careful with the condition
        printf("#%d: %s (%d)\n", i + 1, players[i]->name, players[i]->score);
    }
}
 
// Bug 3 is somewhere in this function
void use_after_free_demo(void) {
    Player *p = create_player("ghost", 0);
    free_player(p);
    printf("Ghost score: %d\n", p->score);   // hmm...
}
 
int main(void) {
    Player *players[3];
    players[0] = create_player("alice", 100);
    players[1] = create_player("bob", 85);
    players[2] = create_player("carol", 92);
 
    // This will crash — use GDB to find out why
    char *msg = get_greeting(players[0]);
    printf("%s\n", msg);
 
    print_top_players(players, 3);
 
    use_after_free_demo();
 
    for (int i = 0; i < 3; i++)
        free_player(players[i]);
 
    return 0;
}

Step 1: Compile with Debug Symbols

gcc -g -O0 -fsanitize=address -o buggy buggy.c
  • -g: include debug symbols (so GDB knows variable names, line numbers)
  • -O0: disable optimization (so variables aren’t optimized away)
  • -fsanitize=address: optional but catches memory bugs automatically

Step 2: Run in GDB

gdb ./buggy
(gdb) run

The program crashes. GDB shows where:

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff... in printf () from /lib/libc.so.6

Step 3: Backtrace — Where Did It Crash?

(gdb) backtrace
#0  printf () at ...
#1  main () at buggy.c:42

Frame #1 tells you: the crash is at buggy.c:42 — the printf("%s\n", msg) line.

(gdb) frame 1
(gdb) print msg
$1 = 0x7fffffffdc10 "\200\..." (garbage)

msg points to garbage! Look at get_greeting — it returns a pointer to a local buf[64] on the stack. That memory is invalid after the function returns.

Fix Bug 1

// Replace: return buf;
// With:
return strdup(buf);    // allocate a copy on the heap

Now the caller must free it. Add free(msg) after the printf.


Step 4: Run Again — Find Bug 2

After fixing bug 1, recompile and run again:

gcc -g -O0 -o buggy buggy.c
gdb ./buggy
(gdb) run

New crash in print_top_players. Use backtrace:

(gdb) bt
#0  print_top_players (players=..., count=3) at buggy.c:30
(gdb) print i
$1 = 3
(gdb) print count
$2 = 3

i == 3 but valid indices are 0, 1, 2. The loop uses i <= count instead of i < count — classic off-by-one.

Fix Bug 2

// Replace: for (int i = 0; i <= count; i++)
// With:
for (int i = 0; i < count; i++)

Step 5: Find Bug 3 with AddressSanitizer

Bug 3 (use-after-free) may not crash reliably. Compile with ASan:

gcc -g -O0 -fsanitize=address -o buggy buggy.c
./buggy

ASan output:

==12345==ERROR: AddressSanitizer: heap-use-after-free
READ of size 4 at 0x602000000014
    #0 use_after_free_demo buggy.c:37

Line 37: p->score is accessed after free_player(p). The memory has been freed — reading it is undefined behavior.

Fix Bug 3

void use_after_free_demo(void) {
    Player *p = create_player("ghost", 0);
    printf("Ghost score: %d\n", p->score);  // print BEFORE free
    free_player(p);
}

GDB Cheat Sheet for Debugging

CommandWhatWhen
runStart the programFirst thing
bt (backtrace)Show call stackAfter a crash
frame NSwitch to frame NTo inspect local variables
print varPrint variable valueAny time when stopped
break funcSet breakpointBefore running
next / stepStep over / intoSingle-step through code
watch varBreak when var changesFinding “who modified this?”
x/10xw ptrExamine 10 words at ptrInspecting raw memory
info localsShow all local varsQuick state dump

Verify: Clean Run

After all three fixes:

gcc -g -O0 -fsanitize=address -o buggy buggy.c
./buggy
# Hello, alice! Score: 100
# #1: alice (100)
# #2: bob (85)
# #3: carol (92)
# Ghost score: 0
 
valgrind --leak-check=full ./buggy
# All heap blocks were freed -- no leaks are possible

Exercises

  1. Watchpoint practice: Add a bug where a global counter gets corrupted by a stray pointer write. Use watch counter in GDB to find the exact line.

  2. Core dump debugging: Run the original buggy program outside GDB. Generate a core dump (ulimit -c unlimited), then gdb ./buggy core to do post-mortem analysis.

  3. Conditional breakpoint: Set break print_top_players if i == 2 to stop right before the off-by-one hit. Examine the state.

  4. Your own bugs: Write a program with a double-free and a buffer overflow. Practice finding them with GDB and ASan.


Next: 04 - Build a Mini Shell — use fork, exec, and pipes to build a working command interpreter.