Debugging C++ with GDB, AddressSanitizer, and UBSan
Find runtime defects using debug symbols, breakpoints, compiler sanitizers, and a repeatable debugging workflow.
Learning objectives
- Compile a debug build with symbols and useful warnings.
- Set a GDB breakpoint, step through code, inspect a variable, and read a backtrace.
- Choose AddressSanitizer or UndefinedBehaviorSanitizer for suitable classes of defects.
- Turn a reproduced defect into a regression test after fixing its cause.
Prerequisites: compiling a C++ program from the command line and reading basic compiler diagnostics. Commands shown target GCC/Clang toolchains; sanitizer availability varies by compiler and platform.
A repeatable debugging workflow
- Reproduce the failure with the smallest input that still triggers it.
- Read the first relevant compiler or runtime diagnostic, not just the last line.
- Build with debug symbols and warnings.
- Use a debugger to inspect the program state at the failing point.
- Use sanitizers to detect classes of undefined behavior and memory defects.
- Fix the cause, add a regression test, and rerun the full test suite.
Compile a debug build
g++ -std=c++17 -Wall -Wextra -Wpedantic -g main.cpp -o app
The -g option adds debug information. Warnings help identify suspicious code before runtime. Keep optimization low or disabled while learning to debug; optimized code can make source-level stepping less direct.
GDB essentials
gdb ./app (gdb) break main (gdb) run (gdb) next (gdb) step (gdb) print variable_name (gdb) backtrace (gdb) continue
A breakpoint pauses execution. next executes the next source line without entering a called function; step enters it. backtrace shows the call stack.
AddressSanitizer and UndefinedBehaviorSanitizer
g++ -std=c++17 -g -O1 -fsanitize=address,undefined -fno-omit-frame-pointer main.cpp -o app ./app
AddressSanitizer can detect many out-of-bounds accesses, use-after-free errors, and related memory problems. UndefinedBehaviorSanitizer detects selected forms of undefined behavior. These tools add runtime overhead and are primarily development tools; they do not prove a program is defect-free.
Example: out-of-bounds access
int main() {
int values[]{10, 20, 30};
return values[3]; // invalid: valid indexes are 0, 1, 2
}AddressSanitizer can report this raw-array access as a stack-buffer-overflow. Sanitizers only detect defects that are exercised and within the tool's instrumentation; a clean run is not proof of correctness. Prefer checked access with at() when runtime bounds checking is appropriate, and design loops around container size.
Common mistakes
- Assuming a clean sanitizer run proves the program is correct.
- Ignoring the first relevant diagnostic and focusing only on the final crash.
- Fixing the visible symptom without adding a regression test.
- Assuming every compiler/platform supports the same sanitizer flags.
Practice
- What does a backtrace show?
- Which sanitizer is useful for use-after-free?
- Why should a fixed bug receive a regression test?
Show answers
- The active call stack.
- AddressSanitizer.
- To ensure future changes do not reintroduce the same defect.
Summary
Debug systematically: reproduce, inspect, instrument, fix, and test. Debug symbols, GDB, warnings, and sanitizers complement one another.