Video summary
Building a Debugger • Sy Brand & Tim Misiak • GOTO 2025
Main summary
Key takeaways
Main ideas, concepts, and lessons
-
Debuggers teach core system truths by forcing implementation
- The speaker argues that learning debuggers by building them is more effective than studying theory alone.
- Building a debugger reveals how hardware and operating systems actually behave.
-
The book is useful beyond “people who want to build a debugger”
- Understanding debuggers improves systems programming and helps developers:
- Grasp hardware/OS interaction and low-level “under the hood” behavior
- Diagnose why debugging workflows fail (e.g., stepping/variables not behaving as expected)
- Use debugger features more effectively (once you understand what they’re doing)
- Understanding debuggers improves systems programming and helps developers:
-
Debugging concepts highlighted
-
Memory access breakpoints (“memory breakpoints”)
- Breakpoints can be set not only on code locations but also on reads/writes to a specific memory address.
- Purpose: stop execution at the moment memory is corrupted/modified and identify the cause.
-
Stack unwinding (and stack walking) is far more complex than “frame pointers” suggest
- A simplified approach: when compiled with frame pointers, you can walk the call stack by reading known memory relative to the current stack pointer.
- The reality: without frame pointers, debuggers must interpret stack unwinding tables, which include complicated, versioned, and edge-case-heavy logic.
- Both Linux and Windows have “horrific but fascinating” complexity:
- Inline frames, interrupt handlers, CPU-specific edge cases
- “Unwinding languages” / encodings embedded in metadata
- Documentation is not enough: specs are inconsistent, and source code (e.g., GCC behavior) often becomes the true reference.
-
Windows vs. Linux differences are philosophical more than fundamental
- Major debugging concepts transfer across platforms, but interfaces differ:
- Linux system call interface vs. Windows’s more “ergonomic” approach to low-level debug features
- Threading model differs:
- Linux process/thread distinction is thin; both are kernel “tasks,” enabling debuggers to sometimes treat threading as secondary early on.
- Windows more strongly centers register context around threads, not processes.
- Major debugging concepts transfer across platforms, but interfaces differ:
-
Old interfaces create ongoing complexity
- Linux
ptraceis described as a single, overloaded interface whose arguments and error handling vary by request type. - This “one function to do many things” approach accumulates complexity over time.
- Linux
-
DWARF is compact but hard to consume
- DWARF debug info uses multiple bytecode interpreters and prioritizes small memory footprint by encoding information as programs that must be interpreted.
- Backward/forward compatibility is limited:
- Parsers must handle multiple DWARF versions, and versions may be incompatible.
- Modern needs (bigger binaries, new language features) motivate reconsideration of compact-but-complex formats.
-
-
Language evolution forces debugging toolchain evolution
- New language features (C++20 concepts, coroutines/“async”-like behavior, anonymous functions, closures, etc.) require:
- New kinds of debug information
- Updates across compiler, linker, debugger, and runtime/library boundaries
- Debugging becomes a cross-layer problem: symbol formats, unwind data, expression evaluation, and execution models all interact.
- New language features (C++20 concepts, coroutines/“async”-like behavior, anonymous functions, closures, etc.) require:
-
Stepping through code is surprisingly complex
- Even though stepping seems like it should be easy (“go to the next line”), the “next line” is hard to define:
- Line mappings depend on compiler-generated line tables
- Stepping semantics differ for:
- Step into vs. step over
- Inline functions (hard because there may be no program-counter change)
- Breakpoints encountered mid-step
- Branches that jump into/out of functions
- Conditions spanning multiple source lines
- Stepping complexity grows as you encounter more real-world compilation behavior and language constructs.
- Even though stepping seems like it should be easy (“go to the next line”), the “next line” is hard to define:
-
A practical methodology for stepping: compose “thread plans”
- The debugger design approach discussed (inspired by LLDB/LLVM):
- Treat stepping as a set of goals/actions, represented by a stack of “thread plans.”
- Plans can handle “what to do when a breakpoint occurs,” without mixing that logic into every stepping action.
- Benefit: separation of concerns
- Each plan encodes what the debugger is trying to accomplish.
- When execution stops, the debugger asks the plans which one explains the stop and what to report.
- The debugger design approach discussed (inspired by LLDB/LLVM):
-
Teaching/debugger implementation realism
- While writing the book, some chapters were expected to be hard/easy, but the actual experience differed:
- Stack unwinding ended up harder than expected.
- Expression evaluation was also a major challenge.
- The author mapped prerequisites early (compilation, hardware, OS basics), then re-ordered chapters as dependencies became clearer.
- While writing the book, some chapters were expected to be hard/easy, but the actual experience differed:
-
Future directions in debugging
-
Time travel debugging
- Lets developers go backward to understand why a bug happened, especially useful for:
- Multi-threaded programs
- Non-deterministic behavior
- Enables reversing past corrupted state (e.g., stack/memory overwrites).
- Lets developers go backward to understand why a bug happened, especially useful for:
-
Debugging optimized code
- Still under-served due to:
- Variables optimized out
- Stepping jumping unexpectedly
- Inlining and other transformations
- Presents a major tooling opportunity.
- Still under-served due to:
-
Methodology / instruction-style details (as presented)
Memory breakpoints (conceptual “how it works” steps)
- Set a breakpoint on a memory address for:
- Reads or writes
- When the program accesses that address (e.g., corrupt write occurs):
- The debugger halts execution immediately
- Inspect state at the stop point to identify:
- What code path/cause is responsible for the corruption
Stack unwinding (practical reality compared to simplified method)
- If compiled with frame pointers:
- Unwinding can be done by walking up the stack via known offsets from the current stack pointer.
- If frame pointers are not available:
- The debugger must interpret unwinding tables/encodings:
- Potentially multiple metadata formats
- Multiple versions
- Complex edge cases (inline frames, interrupts, CPU differences)
- The debugger must interpret unwinding tables/encodings:
Stepping design approach (thread plans / separation of concerns)
- Model stepping as:
- A stack of “thread plans” representing goals like:
- step over a code range
- step into a function
- step out of a function
- A stack of “thread plans” representing goals like:
- As execution progresses:
- If a breakpoint occurs during a planned step:
- Stop execution
- Ask each thread plan in the stack whether it can explain/handle the stop
- If a breakpoint occurs during a planned step:
- Report to the user based on the plan that matches the stop reason
- Keep stepping logic modular:
- Plans encode intent and stopping behavior
- Other concerns (like breakpoint handling relevance) are delegated to the plan framework
Speakers / sources featured (and roles)
- Tim Mishack — interviewer; engineer at Datadog (formerly at Microsoft working on debuggers)
- Sai Brand (spelled “Saibrand” in subtitles) — author of Building a Debugger; works on C++ standards and toolchains; background in GPU debuggers and compilers; Microsoft C++ developer advocate
- LLDB / LLVM debugger — referenced as an example stepping implementation style (thread plans)
- GCC — referenced regarding how its source behavior can contradict unwind specifications (Linux context)
- DWARF — referenced as a Linux debug information format (and its versioning/bytecode interpreter complexity)
ptrace— referenced as the Linux low-level debugger interface that accumulates complexity over time
Rate this summary
Your feedback will help improve summaries.
Improve this summary
Reprocess with a stronger model when the summary feels incomplete or inaccurate.
Translate summary in another language
Ask questions to this video
Chat for follow-up questions, clarifications, and source-backed answers.