C++ Undefined Behavior (UB): Why Release-Only Crashes Happen and How to Catch UB
이 글의 핵심
A practical guide to UB: major patterns, why optimized builds behave differently, UndefinedBehaviorSanitizer, and sanitizer combinations for CI.
UB often surfaces as crashes—see segmentation fault debugging.
Introduction: “Debug is fine; Release crashes”
“With optimizations enabled, behavior changes”
Undefined behavior (UB) means the C++ standard imposes no requirements on what happens. Compilers assume UB does not occur and may optimize aggressively.
int arr[5] = {1, 2, 3, 4, 5};
int x = arr[10]; // UB: out-of-bounds read
This article covers:
- Fifteen common UB patterns (grouped)
- Why Debug and Release differ
- UBSan and complementary tools
- How optimizations interact with UB
- Short case studies
Table of contents
1. What is undefined behavior?
Definition
If a program has undefined behavior, any outcome is allowed: crash, wrong results, or “impossible” optimizations.
UB vs implementation-defined vs unspecified
| Term | Meaning |
|---|---|
| Undefined | No standard guarantee |
| Implementation-defined | Compiler documents behavior |
| Unspecified | One of several allowed outcomes |
2. UB patterns (representative)
- Out-of-bounds access
- Null pointer dereference
- Dangling pointer use
- Reading uninitialized automatic variables
- Signed integer overflow (
int) - Violating strict aliasing rules
- Using an object outside its lifetime
- Data races
- Mismatched allocation/deallocation
- Unsequenced conflicting side effects on the same scalar
- Invalid pointer arithmetic and dereference
- Misaligned access via casts (platform-dependent)
- Dangerous virtual calls during construction/destruction (design-dependent)
- Modifying string literals
- Type punning without
memcpy/std::bit_castwhere required
3. Debug vs Release
Debug often initializes stack slots or uses patterns that mask bugs; Release may leave variables uninitialized and optimize using “UB cannot happen” reasoning.
4. UBSan
g++ -g -fsanitize=undefined -std=c++17 -o myapp main.cpp
./myapp
Combine with -fsanitize=address and -fsanitize=thread where appropriate (separate builds often).
5. Compiler optimization and UB
Examples: null checks that become unreachable after illegal dereference assumptions; x + 1 > x treated as always true for signed int when overflow is assumed impossible.
6. Case studies (short)
- Game server: signed underflow in combat math—use saturating or wider types.
- Image processing: kernel indexing without border checks—clamp or loop interior only.
- Finance: summing
intprices—uselong longor checked accumulation.
Summary
UB prevention checklist
- Initialize before read
- Validate indices and lifetimes
- Avoid signed overflow; use wider types or checks
- Synchronize shared data
- Match
new[]/delete[] - No data races
Sanitizer overview
| Tool | Focus |
|---|---|
| UBSan | Many UB rules |
| ASan | Memory errors |
| TSan | Data races |
Rules
- UB is not “bad luck.”
- Debug passing does not prove Release safety.
- Use sanitizers in CI.
- Treat warnings seriously.
- Prefer RAII and safe abstractions.
Related posts (internal)
- Undefined behavior (longer guide)
- Segmentation fault
- Sanitizers
- Data races
Keywords
undefined behavior, UB, UBSan, release-only crash, signed overflow, data race
Practical tips
- Enable UBSan during development for representative runs.
- Test optimized builds regularly—not only Debug.
- Add sanitizers to CI for main branches.
Closing
Undefined behavior is among the most dangerous C++ issues. Combine warnings, sanitizers, and sound types to ship reliable code.
Next: RAII and smart pointers.
More related posts
- Stack overflow
- Segmentation fault (short)
- Iterator invalidation
- Template errors