Debug vs. Release Builds
This chapter contrasts debug and release builds: their flags, their properties, and why behavior can differ between them.
Why This Matters
"Works in debug, breaks in release" is one of the most common C experiences. The two builds differ in optimization, assertions, and instrumentation, and those differences expose latent undefined behavior.
Prerequisites
c.build.8— compiler flags.
Core Concept
Debug build
-O0or-Og(optimize for debugging).-g(debug symbols).- assertions enabled (
NDEBUGnot defined). - often sanitizers enabled.
Goal: easy stepping, predictable execution, clear diagnostics.
Release build
-O2or-O3(aggressive optimization).-DNDEBUG(assertions compiled out).- no debug symbols (or stripped).
- no sanitizers.
Goal: speed and small size.
Examples
# debug
cc -std=c17 -Wall -Wextra -g -O0 -fsanitize=address,undefined main.c -o app-debug
# release
cc -std=c17 -O2 -DNDEBUG main.c -o app
How It Works
Optimization transforms code aggressively (c.opt.1), which can change timing, reorder operations, and remove "dead" code. NDEBUG removes assert. Sanitizers add runtime checks that slow the program but catch errors. Debug info maps machine code back to source.
Variations
-Og
-Og optimizes just enough to keep debugging pleasant, a middle ground between -O0 and -O2.
Release with symbols
You can keep -g in a release build and strip symbols into a separate file, enabling debugging of optimized code (c.debug.5).
Common Mistakes
- Assuming release and debug behave identically.
- Using
assertfor logic that must run in release. - Not testing at release optimization levels.
Undefined Behavior
- UB may be masked at
-O0and exposed at-O2. The program was always
broken; the optimizer reveals it.
Portability
- The flags are compiler-specific, but the debug/release *concept* is
universal.
Under the Hood
At -O0, the compiler emits straightforward, often redundant code. At -O2, it applies inlining, dead-code elimination, and other passes that rely on the absence of UB (c.opt.2).
Practical Usage
- Develop with a debug build and sanitizers.
- Benchmark and ship with a release build.
- Keep a "release with debug info" configuration for field debugging.
Exercises
1. Build the same program at -O0 and -O2; compare time and binary size. 2. Use assert and observe it vanish with -DNDEBUG. 3. Find a program that behaves differently at -O2 due to UB (or construct a strict-aliasing example) and fix it.
Deep Challenge
Explain, with a concrete strict-aliasing or signed-overflow example, how a release build can produce output that differs from a debug build, and why the program was always undefined. Show the fix.
Related Concepts
c.opt.1— optimization levels.c.debug.5— debugging optimized builds.c.build.8— flags.
References
- GCC/Clang documentation on
-O,-g,-DNDEBUG.
Verification
- Debug/release flag differences are documented 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
- [ ] Debug build flags
- [ ] Release build flags
- [ ] NDEBUG and assert
- [ ] Optimization vs. debugging
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.build.debug | 0 | 5 |
| c.build.release | 0 | 5 |