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 brew install gdb (fiddly)

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 (-fsanitize=address)

gcc/clang, all platforms

Free (2x runtime overhead)

Buffer overflows, use-after-free, double-free

UBSan (-fsanitize=undefined)

gcc/clang

Free (small overhead)

Signed overflow, alignment, null derefs, integer UB

TSan (-fsanitize=thread)

gcc/clang, Linux + Mac

Free (5-15x runtime overhead)

Data races in multithreaded code

MSan (-fsanitize=memory)

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

run / r

Start the program

run < input.txt

Start with stdin redirected

break foo / b foo

Set breakpoint at function foo

break foo.c:42

Set breakpoint at line 42 of foo.c

continue / c

Resume until next breakpoint

next / n

Next line, step over calls

step / s

Next line, step into calls

finish

Run until current function returns

print x / p x

Print value of x

print /x x

Print in hex

print *arr@10

Print first 10 elements of arr

backtrace / bt

Show call stack

frame N / f N

Switch to stack frame N

info locals

Show all local variables

info args

Show current function’s arguments

watch x

Break when x changes

list / l

Show source around current line

layout src

TUI mode with source pane

quit / q

Exit

Modern gdb (v15+) features to know:

  • Python scripting (source myscript.py) — lets you write custom pretty-printers.

  • gdb --tui for split-pane source view (or Ctrl-x a inside gdb).

  • record full for in-gdb reverse execution (small workloads only; use rr for 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

break foo

breakpoint set --name foo (or b foo)

print x

print x or p x

next

next or n

step

step or s

bt

bt

info locals

frame variable

watch x

watchpoint set variable x

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