C++ PROGRAMMING ยท 11 MIN STUDY

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

  1. Reproduce the failure with the smallest input that still triggers it.
  2. Read the first relevant compiler or runtime diagnostic, not just the last line.
  3. Build with debug symbols and warnings.
  4. Use a debugger to inspect the program state at the failing point.
  5. Use sanitizers to detect classes of undefined behavior and memory defects.
  6. 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

  1. What does a backtrace show?
  2. Which sanitizer is useful for use-after-free?
  3. Why should a fixed bug receive a regression test?
Show answers
  1. The active call stack.
  2. AddressSanitizer.
  3. 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.

References