Why this matters
Every bug so far in this course has been small enough to spot by reading the code. That stops being true fast –
real bugs hide in state that changes over many steps, and “stare at the source until it’s obvious” stops working.
The tools in this lesson exist for exactly that gap: a debugger lets you watch a program’s state as it runs
instead of just imagining it; assert gives you an always-available, zero-setup way to make a wrong assumption
fail loudly, immediately, at its source; and memory checkers like ASan and valgrind catch an entire category of bug
– memory misuse – that can execute perfectly normally for a while before it corrupts something.
What a debugger session looks like
A debugger (gdb on Linux, lldb on macOS – same ideas, different command names) lets you pause a running
program at a specific line and inspect it. You can’t run one inside this browser, so here’s an annotated, read-only
transcript of a real session – diagnosing a function that’s supposed to sum an array but returns a total that’s
consistently too small:
(gdb) break main # pause execution as soon as main starts
(gdb) run 4 # start the program; it stops immediately, before line 1 of main runs
(gdb) next # step over one line at a time (into the loop)
(gdb) next
(gdb) print i # $1 = 0 <- loop variable, as expected
(gdb) print total # $2 = 0 <- nothing added yet, as expected
(gdb) next
(gdb) next
(gdb) print i # $3 = 1
(gdb) print total # $4 = 10 <- values[0] was added; looks right so far
...
(gdb) print i # $7 = 3 <- but n was 4 -- the loop already stopped
(gdb) print total # $8 = 60 <- values[3] was never added
Nothing here is exotic: break sets a stopping point, run starts the program, next executes one line and
stops again, and print shows a variable’s current value. What makes it powerful is watching i and total
together, step by step, until the exact moment they diverge from what you expected – here, the loop stopping at
i == 3 instead of running through i == 3 reveals a < that should have been <=, or an off-by-one in the
bound, without needing to guess from the wrong final answer alone. This is the second exercise below, framed
exactly this way: given the “trace,” find and fix the bound.
A few more commands worth recognizing on sight
The transcript above only needed break, run, next, and print, but a few more commands come up constantly
enough to be worth knowing, even without a live session to practice them in:
stepvs.next– both execute one line and stop, but they differ the moment that line is a function call:nextruns the entire called function and stops at the line after it (treating the call as one opaque step);stepfollows execution into the called function itself, stopping at its first line. Usenextwhen you trust a function and just want to keep moving past it; usestepwhen the bug might be inside that call.continue– resume running at full speed until the next breakpoint (or the program ends). This is how you get out of single-stepping once you’ve confirmed everything’s fine for a while.backtrace(often shortened tobt) – print every currently-paused stack frame, from the one you’re stopped in back tomain. This is the debugger equivalent of the frame-by-frame call traces from Module 2’s recursion lesson – if you’re stopped deep inside a recursive call and want to see the full chain of calls that got you there,backtraceis how you’d actually look, rather than reasoning it out by hand.watch <variable>– instead of a breakpoint tied to a line, this pauses execution the instant a specific variable’s value changes, no matter which line does it. Useful exactly when you know what is going wrong (a variable ending up with the wrong value) but not where in a large function it happens.
assert: a debugger you don’t have to attach
assert(condition), from <assert.h>, does one thing: if condition is false when execution reaches it, the
program prints a message naming the failed condition, the file, and the line, and stops immediately via abort().
If it’s true, assert does nothing at all – no output, no cost beyond the check itself.
The worked example uses it to state an assumption out loud: safe_divide requires a nonzero denominator, and
assert makes that requirement enforced, not just implied by a comment. The value isn’t stylistic. Compare two
failure modes: without the assert, dividing by zero produces a garbage or infinite result that gets used, printed,
and possibly fed into more computation before anyone notices something’s wrong, far from where the actual mistake
was made. With the assert, the program stops at the exact call that violated the assumption, with a message naming
exactly which assumption failed. A loud failure at the source of the bug is always easier to debug than a silent
one that surfaces somewhere else entirely – this is the same principle a debugger breakpoint gives you
manually, built into the language for free.
What ASan and valgrind catch that reading code doesn’t
Module 2 covered the rules for heap memory by hand: free exactly once, never use a pointer after freeing it, always
check malloc’s result. Breaking those rules is frequently invisible when you test a program normally – reading
freed memory often still returns the old bytes, unchanged, until something else happens to reuse that address. The
bug is real the instant you break the rule; the visible symptom might not appear until much later, in a completely
different part of the program, which makes it one of the hardest bug classes to trace back by reading code alone.
AddressSanitizer (ASan) and valgrind are tools that watch every memory access while the program runs and
flag a violation the instant it happens, rather than however much later a symptom might appear – a
use-after-free, a double-free, a heap write past the end of an allocation, or memory that was malloc’d and never
free’d. Neither can run inside this browser (they need to instrument a real running process on your machine, not
a remote one-shot compile-and-run call), but they’re standard tools once you’re compiling locally:
gcc -fsanitize=address,undefined -g program.c -o program && ./program
valgrind --leak-check=full ./program
Not every bug is a memory bug, though – some of the strangest ones live in state that quietly outlives the call that set it, which no memory checker flags because nothing about it is memory-unsafe. The hardest exercise below is exactly that kind: a function whose output for one call secretly depends on a previous, unrelated call. Tracing it is a job for the same tool as the sum-loop bug above – watching a variable’s actual value over time, by hand or with a debugger, until it stops matching what you expected.