Debugging & Tooling
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
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
| Tool | Category | Recompile needed? |
|---|---|---|
| valgrind | Memory bugs (c2-3) | no |
| AddressSanitizer | Memory bugs (c2-3) | yes — fast, precise |
| UBSan | Undefined behavior (c3-4) | yes |
| gdb | Interactive step-through + post-mortem | no (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 it — clang-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.
pipelines1's own CI/CD material — not something reached for only after a bug report arrives.
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
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 solutionExplain 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 solutionExplain 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 solutionChapter 5 Quick Reference
gdb—break/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