Undefined Behavior and Debugging
7 pages · ~5 min✓ Reviewed
Builds on Concurrency and Threads.
Undefined Behavior: What It Is and Why It Bites
1. What UB Is
- Undefined behavior: the standard sets no requirements on the program
- The compiler may assume UB never happens and optimize on that
- UB is not a crash guarantee
- It can seem fine, fail only at
-O2, or break after a compiler upgrade
2. Three Kinds of Unpredictable
| Kind | Result | Example |
|---|---|---|
| Unspecified | one of several valid results | argument evaluation order |
| Implementation-defined | valid, documented by compiler | sizeof(int) |
| Undefined | no valid result | INT_MAX + 1 |
3. How the Optimizer Bites
cpp
int f(Node* p) {
if (p->x) return 1; // p dereferenced
if (!p) return -1; // check may be dropped
return 0;
}p was already dereferenced, so the compiler assumes p != nullptr
- UB belongs to the whole execution, not one line
- One bad input path poisons the entire run
- Passing tests prove nothing
Remember
UB has no valid result. "It works on my machine" is not evidence of correctness.
The Four Common UB Families
1. Memory UB
- Out of bounds:
a[n]onint a[n]reads or writes past the end - Even forming a pointer more than one past the end is UB
- Use after free, double free, returning a pointer to a local
- Freed memory may still look valid until it is reused
- Locals and
mallocmemory are indeterminate callocor= {0}initializes;mallocdoes not
2. Integer Overflow
| Signed | Unsigned | |
|---|---|---|
INT_MAX + 1 | UB | wraps mod 2^N |
- Compiler may fold
x + 1 > xto true
3. Other Frequent UB
- Null pointer dereference
- Shift by >= width or a negative amount
Sanitizers: ASan, UBSan, TSan
1. What They Do
- Compiler instruments the binary; the report comes at runtime with a stack trace
- Build with
-g -O1 -fno-omit-frame-pointerfor readable traces
2. Which Sanitizer
| Tool | Flag | Catches | Cost |
|---|---|---|---|
| ASan | -fsanitize=address | out-of-bounds, use-after-free, double free, leaks | ~2x time, 2-3x memory |
| UBSan | -fsanitize=undefined | signed overflow, bad shifts, null deref, misaligned | – |
| TSan | -fsanitize=thread | data races | 5-15x slower |
| MSan | -fsanitize=memory | uninitialized reads (Clang only) | all code and libs instrumented |
3. Build and Run
bash
g++ -g -O1 -fno-omit-frame-pointer \ -fsanitize=address,undefined \ -fno-sanitize-recover=all prog.cpp
recover flag aborts on the first error
Valgrind, gdb and Core Dumps
1. Valgrind memcheck
- No recompile; build with
-gfor line numbers - Reports invalid reads/writes, uninitialized use, leaks; 10-50x slower
- definitely lost = real leak; still reachable = usually fine
--track-origins=yesshows where an uninitialized value came from
bash
valgrind --leak-check=full --track-origins=yes ./prog| Valgrind | ASan | |
|---|---|---|
| Rebuild | No | Yes |
| Speed | slower | faster |
| Stack/global overflow | missed | caught |
2. gdb basics
bash
gdb ./prog break file.c:42 run next / step / continue bt frame N print var / info locals watch var
3. Crash without a debugger
- ulimit -c unlimited
- Run, crashcore file
- gdb ./prog core
- btbacktrace
Warnings as Errors and Static Analysis
1. Warning Flags
- Fail the build on any warning, so warnings never accumulate
| Set | Flags |
|---|---|
| GCC / Clang | -Wall -Wextra -Wpedantic -Werror |
| MSVC | /W4 /WX |
| Extras | -Wshadow -Wconversion -Wsign-compare -Wnull-dereference -Wformat=2 |
Need -O1 or higher | -Wuninitialized -Wmaybe-uninitialized |
2. Static Analyzers
- Find bugs without running the code
- Static covers all paths; dynamic tools see only executed ones
bash
clang-tidy file.cpp -- -std=c++20 cppcheck --enable=all file.cpp gcc -fanalyzer file.c clang --analyze file.c
| Static | Dynamic | |
|---|---|---|
| Paths | All | Only executed |
Cheat Sheet: Flags and Commands
1. Build Commands
bash
g++ -g -O1 -fno-omit-frame-pointer -Wall -Wextra -Werror -fsanitize=address,undefined prog.cpp g++ -g -O1 -fsanitize=thread prog.cpp ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 ./prog
debug build, thread build, runtime options
- ASan + UBSan share one build
- TSan is a separate build: never combined with ASan
- Env options like
ASAN_OPTIONStune the run, no rebuild
2. Valgrind and gdb
bash
valgrind --leak-check=full --track-origins=yes --error-exitcode=1 ./prog gdb -ex run --args ./prog ulimit -c unlimited
after a crash: gdb ./prog core
| gdb | Does |
|---|---|
bt | backtrace |
frame N, up, down | move between frames |
print x, info locals | inspect values |
break f, watch x | stop at function / on change |
3. Which Tool for Which Bug
Check yourself
Compiled with -O2, what can f(INT_MAX) return, and why?
- Signed overflow is UB, so it may return 1 (folded to true) or 0 (wraps at -O0)
- Fix: check
x < INT_MAXfirst, or use unsigned
int f(int x) { return x + 1 > x; }Two threads increment a plain int. Tests pass. Which build finds the bug, and can it share a binary with ASan?
- TSan (
-fsanitize=thread) reports the data race - No: TSan and ASan need separate builds
Valgrind reports 'definitely lost: 40 bytes' and 'still reachable: 72 bytes'. Which one do you fix first?
- Definitely lost: a real leak with no pointer left
- Still reachable is usually a global still held at exit