Debugging & Tooling

Course 3 · Ch 5
Debugging & Tooling
Every tool this course has mentioned, plus the interactive debugger — brought together into one real workflow

c2-3 introduced valgrind and AddressSanitizer; c3-4 added UBSan. This chapter adds the one genuinely interactive tool — gdb — and shows how a real project uses all of them together.

gdb — The GNU Debugger

Compile with -g to include debug symbols. Core commands: break (set a breakpoint), run, next/step (advance a line, stepping into or over calls), print (inspect a variable), backtrace (the call stack), continue.

A genuinely rare moment of overlap rather than contrast: Rust compiles to native code too, and debugs through the same underlying mechanism (breakpoints, stepping, variable inspection) via gdb or lldb. Most of this course has been about differences — this is a real, shared skill.

A Worked gdb Session

// buggy.c — an off-by-one loop int arr[5] = {1, 2, 3, 4, 5}; int sum = 0; for (int i = 0; i <= 5; i++) { // <= is the bug — should be < sum += arr[i]; }
$ gcc -g buggy.c -o buggy $ gdb ./buggy (gdb) break buggy.c:5 (gdb) run (gdb) print i $1 = 0 (gdb) next (gdb) print sum $2 = 1 ... continue stepping to watch i reach 5, past the array's real bound

Post-Mortem Debugging With Core Dumps

A crashed program can leave behind a core dump — a snapshot of its memory at the moment of the crash. gdb ./program core inspects that exact state after the fact, genuinely useful for bugs that are hard to reproduce interactively on demand.

Bringing the Whole Toolkit Together

ToolCategoryRecompile needed?
valgrindMemory bugs (c2-3)no
AddressSanitizerMemory bugs (c2-3)yes — fast, precise
UBSanUndefined behavior (c3-4)yes
gdbInteractive step-through + post-mortemno (with -g symbols)

These are complementary, not competing. A real workflow often compiles with ASan and UBSan together for routine testing, then reaches for gdb once a bug is narrowed down for deep, interactive investigation.

Static Analysis

A genuinely different category: tools that examine source code without ever running itclang-tidy, cppcheck. They catch uninitialized variables, some overflow patterns, and suspicious casts before the program executes at all, complementing every runtime tool covered so far. Rust's own compiler performs far more of this kind of analysis by default, as a mandatory part of compilation — borrow checking and type checking aren't optional add-ons. In C, static analysis is a separate, optional tool a team has to deliberately choose to run; nothing about the compiler itself enforces it.

Routine practice, not a one-time investigation aid
A mature C project typically runs several of these tools together as everyday development and CI practice — mirroring pipelines1's own CI/CD material — not something reached for only after a bug report arrives.
No tool here guarantees catching every bug
valgrind, ASan, and UBSan only catch bugs actually exercised by the specific code path run during that particular execution. A bug hiding in an untested branch remains completely invisible to all of them — these tools verify what they observe, not what could theoretically happen.

Coding Challenges

Challenge 1

Compile the chapter's buggy.c with -g, run it under gdb, set a breakpoint inside the loop, and step through enough iterations to observe i reaching 5 — one past the array's valid indices. Report what print arr[i] shows at that point.

📄 View solution
Challenge 2

Explain why valgrind and AddressSanitizer, run against the exact same test input, might catch a bug on one execution but not on a different one — and what this implies about test coverage more broadly.

📄 View solution
Challenge 3

Explain the key difference between static analysis tools (like clang-tidy) and the dynamic tools (valgrind, ASan, UBSan, gdb) covered earlier in the chapter, and why Rust's compiler performing similar analysis by default is a meaningfully different guarantee than C teams choosing to run a static analyzer.

📄 View solution

Chapter 5 Quick Reference

  • gdbbreak/run/next/step/print/backtrace, compiled with -g
  • Debugging via breakpoints/stepping is a genuinely shared skill with Rust, not a C-only technique
  • Core dumps enable post-mortem inspection of a crash's exact state after the fact
  • valgrind/ASan/UBSan/gdb are complementary — a real project uses several together
  • Static analysis (clang-tidy, cppcheck) examines source without running it — optional in C, mandatory-by-default in Rust's own compiler
  • No tool here catches bugs in code paths that were never actually exercised
  • Next chapter: the capstone — building a real CLI tool combining everything from this entire track