04 — Debuggers and Sanitizers¶
In C, the debugger and the sanitizer are not “nice to haves.” They are the difference between shipping code and shipping segfaults. By M6 you must be able to gdb a running process, set watchpoints, and read a stack trace. By M4 you must never compile without -fsanitize=address,undefined unless you’re benchmarking.
The 2026 landscape¶
Tool |
Platform |
Cost |
Primary use |
|---|---|---|---|
gdb 16+ |
Linux (best); Mac via |
Free |
Interactive debugging on Linux |
lldb |
macOS (native); Linux |
Free |
Interactive debugging on macOS, and increasingly used on Linux |
rr |
Linux x86_64 only (Intel/AMD with perf counters) |
Free |
Record-and-replay; time-travel debugging for nondeterministic bugs |
ASan ( |
gcc/clang, all platforms |
Free (2x runtime overhead) |
Buffer overflows, use-after-free, double-free |
UBSan ( |
gcc/clang |
Free (small overhead) |
Signed overflow, alignment, null derefs, integer UB |
TSan ( |
gcc/clang, Linux + Mac |
Free (5-15x runtime overhead) |
Data races in multithreaded code |
MSan ( |
Clang only, Linux only |
Free (3x overhead) |
Uninitialized reads. Requires all deps built with MSan — painful. |
valgrind (memcheck) |
Linux only (no Apple Silicon port) |
Free (20x overhead) |
Same class as ASan but doesn’t need recompile |
gdb — the survival subset¶
gdb has hundreds of commands; you need about 20. Print this and tape it near your keyboard:
Command |
Effect |
|---|---|
|
Start the program |
|
Start with stdin redirected |
|
Set breakpoint at function |
|
Set breakpoint at line 42 of |
|
Resume until next breakpoint |
|
Next line, step over calls |
|
Next line, step into calls |
|
Run until current function returns |
|
Print value of |
|
Print in hex |
|
Print first 10 elements of |
|
Show call stack |
|
Switch to stack frame N |
|
Show all local variables |
|
Show current function’s arguments |
|
Break when |
|
Show source around current line |
|
TUI mode with source pane |
|
Exit |
Modern gdb (v15+) features to know:
Python scripting (
source myscript.py) — lets you write custom pretty-printers.gdb --tuifor split-pane source view (orCtrl-x ainside gdb).record fullfor in-gdb reverse execution (small workloads only; userrfor real).
On macOS: gdb requires code-signing to attach to processes; lldb is the path of less resistance. Use lldb on Mac unless you have a specific reason.
lldb — for macOS users¶
Same conceptual model as gdb, different command names. Cheat sheet:
gdb |
lldb |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lldb ships with Xcode Command Line Tools on macOS. No install needed.
Sanitizers — your daily hygiene¶
Always develop with:
gcc -std=c17 -Wall -Wextra -Wpedantic -O1 -g -fsanitize=address,undefined foo.c -o foo
-O1 (not -O0) because ASan needs some optimization to give clean stack traces. -fno-omit-frame-pointer helps too.
Trade-offs by sanitizer:
ASan+UBSan together: ~2-3x runtime overhead. Enable by default in development.
TSan: cannot be combined with ASan. Run separately when working on threaded code.
MSan: clang-only, requires MSan-instrumented libc/deps. Skip until year two.
valgrind’s memcheck: slower than ASan (~20x vs 2x) but doesn’t need recompile. Useful for third-party binaries you can’t rebuild.
ASan vs valgrind decision tree:
Can you recompile? → ASan (10x faster, cleaner reports).
Can’t recompile (binary blob)? → valgrind (Linux only).
On Apple Silicon and want the valgrind experience? → ASan — there is no valgrind for you.
rr — the debugging superpower most C engineers don’t use¶
Mozilla’s rr (https://rr-project.org) records a program execution, then replays it deterministically with reverse execution:
sudo apt install rr
rr record ./prog input.txt
rr replay
(rr) continue # runs to first crash
(rr) reverse-continue # runs BACKWARD to previous breakpoint
(rr) reverse-next # step backward
Limitations:
Linux only.
x86_64 Intel or AMD only (needs performance counters). Apple Silicon: nope.
Some kernels need
/proc/sys/kernel/perf_event_paranoid = 1.
When to use it: nondeterministic bugs, race conditions, “it segfaults only sometimes.” A CppNorth 2025 lightning talk covered exactly this pattern. Once you use it on a real bug, you never want to debug without it.
Static analysis — free finds¶
Run these periodically (not every build):
gcc -fanalyzer— gcc 10+ static analyzer. Rerun with-Wanalyzer-*warnings.scan-build(clang) —scan-build make; produces an HTML report.clang-tidy— checks style + bug patterns. Configure via.clang-tidy.cppcheck— old but still useful.apt install cppcheck.
None of these replaces the sanitizers, which run actual code and actual inputs. Static analysis catches a different class of bugs; use both.
What most people get wrong about debugging¶
They printf-debug for two years, then wonder why senior engineers solve bugs 10x faster. The answer: senior engineers use gdb, watchpoints, and rr. Learn one debugger deeply in M2-M3; you will save hundreds of hours over the 13 months.
Return to README.md · Next: 05_build_and_package.md