Debugging Optimized Builds
This chapter explains how to debug code compiled with optimization, where variables may be optimized away, reordered, or inlined.
Why This Matters
Some bugs only appear at -O2. You cannot always reproduce them in a debug build. Debugging optimized code is harder but learnable, and it is a skill every systems programmer needs.
Prerequisites
c.debug.1— GDB/LLDB.c.build.10— debug vs. release builds.
Core Concept
Optimization transforms the code: variables may live in registers, be eliminated, or be reordered; functions may be inlined. The debugger uses DWARF to map back to source, but the mapping is approximate. You debug the *optimized* machine code, not the naive source.
Examples
Building an optimized-but-debuggable binary
gcc -g -O2 main.c -o app # keep symbols, optimize
GDB in optimized code
(gdb) break main.c:25 # may hit on a different line than expected
(gdb) print x # "optimized out" if x has no location
(gdb) disassemble /m # mixed source + assembly
(gdb) info locals
How It Works
At -O2, the compiler applies inlining, dead-code elimination, and value numbering. DWARF still records the best available location for each variable, but a variable may have no single location (e.g., it is folded into an expression) and shows as "optimized out."
Variations
-Og
-Og optimizes just enough to keep debugging pleasant — a good middle ground when you need optimization but still want reliable stepping.
-fno-omit-frame-pointer
Keeping frame pointers (-fno-omit-frame-pointer) makes backtraces and profiling more reliable at the cost of a register.
Common Mistakes
- Assuming "optimized out" means your variable is gone from the program (it
may just have no debug location).
- Debugging with
-O2and expecting line-by-line stepping to match source. - Not using
disassemble /mto see what the code actually does.
Undefined Behavior
- The very reason a bug "only appears at -O2" is often latent UB being exposed
by optimization (c.opt.2).
Portability
- Debug info for optimized code varies by compiler; the concepts are
universal.
Under the Hood
The compiler records DWARF location lists that can describe a variable's location across instruction ranges. Optimized code makes these lists complex or empty.
Practical Usage
- Reproduce the bug, then rebuild with
-g -O2(or-Og) for investigation. - Use
disassemble /mandinfo registersto see what the machine is doing. - If a variable is optimized out, reconstruct its value from other registers/
memory.
Exercises
1. Compile a program at -O2 -g, set a breakpoint, and observe "optimized out" variables. 2. Use disassemble /m to map source to assembly. 3. Compare debugging at -O0, -Og, and -O2.
Deep Challenge
Take a bug that only manifests at -O2 (e.g., a strict-aliasing violation) and debug it in the optimized build: reproduce, disassemble, identify the transformed code, and fix the root cause. Document the process.
Related Concepts
c.opt.2— UB-based optimization.c.debug.2— DWARF.c.build.10— debug/release.
References
- GDB manual on optimized code, compiler optimization docs.
Verification
- Optimized-debugging behavior is documented tool behavior.
VERIFIED - No example was executed during generation unless noted. `Execution not
verified.`
Progress
- [ ] Read
- [ ] Understand
- [ ] Complete examples
- [ ] Complete exercises
- [ ] Complete deep challenge
Concept checkboxes
- [ ] Debugging with optimization
- [ ] Optimized-out variables
- [ ] disassemble /m
- [ ] -Og
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.debug.opt-build | 0 | 6 |