Debugging C is an art. Unlike managed languages where the runtime catches null references and out-of-bounds accesses with clear exceptions, C will silently corrupt memory, produce wrong results, or crash with nothing but “Segmentation fault (core dumped).” The bug that crashes your program at line 500 may have been caused by a buffer overflow at line 50. This chapter arms you with the tools and techniques to systematically hunt down even the most devious bugs.
# Compile with debug symbols (-g) and NO optimization (-O0).# -O0 is important: with optimization, the compiler reorders, inlines, and# eliminates code. Variables you want to inspect may be "optimized out,"# and stepping through code will jump around unpredictably. Always use -O0# for debugging sessions.gcc -g -O0 program.c -o program# Start GDBgdb ./program# Start with argumentsgdb --args ./program arg1 arg2# Attach to running processgdb -p <pid># Load core dumpgdb ./program core
# Runningrun # Start programrun arg1 arg2 # With argumentsstart # Run and stop at maincontinue (c) # Continue executionnext (n) # Step over (don't enter functions)step (s) # Step intofinish # Run until current function returnsuntil <line> # Run until line# Breakpointsbreak main # Break at functionbreak file.c:42 # Break at linebreak *0x400520 # Break at addressinfo breakpoints # List breakpointsdelete 1 # Delete breakpoint 1disable 1 # Disable breakpoint 1enable 1 # Enable breakpoint 1clear main # Clear breakpoints at main# Conditional breakpointsbreak file.c:42 if i == 100condition 1 x > 5 # Add condition to breakpoint 1# Watchpoints (break when value changes)watch x # Break when x changesrwatch x # Break when x is readawatch x # Break on read or write# Examiningprint x (p) # Print variableprint *ptr # Dereference pointerprint arr[5] # Array elementprint (int*)0x7fff... # Cast and printprint/x val # Print in hexprint/t val # Print in binaryprint/d val # Print as decimaldisplay x # Print x every step# Memory examinationx/10xw 0x7fff... # 10 words in hexx/s str # Stringx/i $pc # Instruction at PCx/20i main # 20 instructions at main# Stackbacktrace (bt) # Show call stackframe 2 # Select frame 2up / down # Move through framesinfo locals # Local variablesinfo args # Function argumentsinfo registers # CPU registers# Codelist # Show sourcelist function # Show function sourcedisassemble # Show assemblydisassemble /m # Mixed source and assembly
# ~/.gdbinitset history save onset history size 10000set print pretty onset print array onset print elements 0set confirm off# Custom promptset prompt (gdb) # Useful macrosdefine phead print *($arg0)enddocument pheadPrint the head of a linked list nodeend
# Load core dumpgdb ./program core# In GDB(gdb) bt # Where did it crash?(gdb) frame 0 # Go to crash frame(gdb) info locals # What were the values?(gdb) print *ptr # Examine suspicious pointers
Valgrind runs your program on a synthetic CPU, intercepting every memory access. This gives it superpowers (detecting reads of uninitialized memory, off-by-one errors, leaks) but makes your program 10-50x slower. Always test with Valgrind before shipping, but do not use it for performance benchmarks.
==12345== Invalid read of size 4==12345== at 0x400520: main (example.c:10)==12345== Address 0x5203040 is 0 bytes after a block of size 16 alloc'd==12345== at 0x4C2AB80: malloc (vg_replace_malloc.c:299)==12345== at 0x400510: main (example.c:9)
This means: reading 4 bytes right after a 16-byte allocation (buffer overflow).
AddressSanitizer (ASan) is a compile-time instrumentation tool that catches memory errors at runtime with only 2x slowdown (much faster than Valgrind). It detects heap/stack/global buffer overflows, use-after-free, use-after-return, double-free, and memory leaks. In practice, run your test suite with ASan enabled as part of CI — it catches bugs that would take days to track down with GDB.
# Compile with ASan -- it instruments every memory access at compile timegcc -fsanitize=address -g program.c -o program# Run./program
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000014READ of size 4 at 0x602000000014 thread T0 #0 0x400520 in main /path/example.c:10 #1 0x7f123... in __libc_start_main0x602000000014 is located 0 bytes to the right of 16-byte region
These are the seven deadly sins of C memory management. Every one of them is undefined behavior — meaning the program might crash, produce wrong results, appear to work fine, or even format your hard drive (in theory). The terrifying part: “appears to work fine” is the most common outcome in development, and “crashes in production” is when you find out.
// 1. Use after free -- the dangling pointer problem// After free(), the memory may be reused for something else. Writing to it// corrupts unrelated data. Reading from it returns garbage. Both are UB.char *ptr = malloc(10);free(ptr);ptr[0] = 'x'; // ERROR! ptr now points to freed memory// 2. Buffer overflow -- the #1 security vulnerability in C// Writing past the end of a buffer overwrites adjacent memory. On the stack,// this can overwrite the return address (enabling code injection attacks).char buf[10];buf[10] = 'x'; // ERROR! Valid indices are 0-9. This writes past the buffer.// 3. Memory leak -- death by a thousand allocations// The original pointer is overwritten, so you can never free those 10 bytes.// In a long-running server, leaks accumulate until the process runs out of memory.char *ptr = malloc(10);ptr = malloc(20); // Original 10 bytes leaked forever!// 4. Double free -- heap corruption// The allocator's internal bookkeeping is corrupted. A later malloc may return// a pointer that overlaps with another live allocation, leading to silent data// corruption that manifests far from the actual bug.char *ptr = malloc(10);free(ptr);free(ptr); // ERROR! Heap metadata is now corrupted// 5. Invalid free -- freeing a non-heap address// The allocator tries to update metadata that does not exist at that address.char buf[10];free(buf); // ERROR! buf is on the stack, not from malloc// 6. Uninitialized read -- using garbage values// Stack memory contains whatever was left there by previous function calls.// The value is indeterminate and may differ between runs, compilers, and// optimization levels -- making bugs extremely hard to reproduce.int x;printf("%d\n", x); // ERROR! Could print anything. UB.// 7. Stack buffer overflow -- strcpy's classic trap// strcpy copies until it finds '\0'. If the source is longer than the// destination, it silently writes past the buffer. Use strncpy or snprintf.char buf[10];strcpy(buf, "This is too long!"); // ERROR! Writes 18 bytes into 10-byte buffer
Find the minimal input/steps to trigger the crash. Randomness = harder debugging.
2
Get the Stack Trace
Run in GDB or analyze core dump. Know exactly where it crashed.
3
Examine State
Check variable values, pointer validity, array bounds at crash site.
4
Find the Root Cause
The crash site is almost never the bug itself. A segfault in free() usually means someone corrupted the heap much earlier. A wrong value in a calculation often traces back to an uninitialized variable set 50 lines ago. Work backwards from the crash: when did the state first become invalid? Use watchpoints in GDB (watch variable) to catch the exact moment a value changes unexpectedly.
5
Verify Fix
Run the same reproduction steps. Then run your test suite.