C++ Debugging
systematic-debugging gives you the language-neutral method. This skill adds the
native C++ toolchain and runtime techniques that matter once the bug involves
crashes, corrupted memory, UB, or platform-specific debugger state.
When to Use
- A C++ process crashes with a segmentation fault, access violation, or abort
- The bug depends on object lifetime, ownership, or move/copy behavior
- Behavior changes between debug and release builds
- AddressSanitizer, UndefinedBehaviorSanitizer, or a debugger is needed to see the failure clearly
- A native test hangs, deadlocks, or corrupts memory without a clear stack trace
When NOT to Use
| Instead of cpp-debugging |
Use |
| You still do not have a stable reproduction or root-cause workflow |
systematic-debugging |
| The failure is a build, compile, or link error |
fix-build-errors |
| The issue is performance-only, not correctness |
performance-optimization |
Prerequisites
- A reproducible failing command, test, or executable
- A debug-capable build with symbols
- Access to the relevant debugger or sanitizer-enabled toolchain
- Enough context to tell whether the bug is runtime, lifetime, concurrency, or build-related
Workflow
1. Rebuild for diagnosis, not speed
Start with a debuggable build before chasing the crash:
# CMake
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build --config Debug
# Clang/GCC direct build example
clang++ -std=c++20 -g -O0 -Wall -Wextra -Wpedantic main.cpp -o app.exe
Guidelines:
- enable symbols (
-g, /Zi)
- reduce or disable optimization while reproducing (
-O0, /Od)
- keep warnings enabled
- reproduce with the same inputs that fail in CI or production-like runs
2. Capture the exact failing surface
# Run the failing binary or test under the same conditions each time
ctest --test-dir build --output-on-failure
# Or run the specific executable directly
.\build\app.exe
Record:
- exact command line
- failing input or fixture
- crash signal or exit code
- first bad log line, assertion, or stack frame
3. Inspect runtime state with a native debugger
Use the platform-native debugger your team standardizes on. Common flows:
# GDB
gdb --args .\build\app.exe <args>
# (gdb) run
# (gdb) bt
# (gdb) frame 0
# (gdb) info locals
# (gdb) p suspiciousVariable
# LLDB
lldb -- .\build\app.exe <args>
# (lldb) run
# (lldb) bt
# (lldb) frame variable
Focus on:
- the first frame where state becomes invalid
- moved-from or already-destroyed objects
- container bounds and iterator validity
- null or dangling pointers
- cross-thread ordering when the state only fails under load
4. Turn on sanitizers early
Sanitizers catch the class of bugs that ordinary logging often hides.
# Clang/GCC
cmake -S . -B build-asan `
-DCMAKE_BUILD_TYPE=Debug `
-DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer"
cmake --build build-asan
ctest --test-dir build-asan --output-on-failure
If your toolchain supports it, enable the equivalent native flags on Windows as well
(for example MSVC AddressSanitizer in supported versions).
Use sanitizer output to answer:
- which access was invalid
- which allocation/free site owned the memory
- whether UB happened before the visible crash
5. Use core dumps or crash artifacts when the bug is post-mortem
If the crash only appears outside the current shell, collect the artifact and inspect it:
- preserve the crashing binary and symbols
- keep the exact build that produced the dump
- load the dump in the matching debugger
- compare the crashing frame with the last known good run
Do not "fix" a crash dump by guessing from the top frame alone. Trace ownership and state backwards.
6. Fix the root cause and harden the boundary
Typical C++ root causes:
- lifetime mismatch between owner and borrower
- invalid iterator or reference after container mutation
- double free or use-after-free
- missing synchronization around shared mutable state
- undefined behavior surfaced by a newer compiler or optimizer
After the fix:
- rerun the failing reproduction
- rerun sanitizer-enabled tests
- add a regression test where practical
- check nearby code for the same lifetime or ownership pattern
Common Rationalizations
| Rationalization |
Reality |
| "The debugger changes timing, so it is useless" |
Timing-sensitive bugs still need debugger or sanitizer evidence. Use them to narrow the class of failure, then confirm in a normal run. |
| "Release-only crash means the optimizer is broken" |
Most release-only failures are UB, data races, or lifetime bugs that debug mode accidentally masks. |
| "The stack trace is enough" |
Native crashes often happen far from the real cause. Ownership history matters more than the final frame. |
| "I'll just add null checks" |
Null checks do not fix dangling references, iterator invalidation, or races. |
Red Flags
- The crash disappears when you add logging but no root cause is identified
- The only "fix" is changing optimization level
- You cannot say which object owns the failing memory
- The code mixes raw ownership, smart pointers, and borrowed references without clear boundaries
- No regression test or sanitizer rerun happened after the fix
Verification
Tips
- Prefer smaller reproductions: isolate the failing target or test before debugging the whole system
- If the stack is noisy, break on the first thrown exception, failed assertion, or allocator error
- Compare debug and release compile flags when behavior diverges
- If the bug looks build-related after all, hand off to
fix-build-errors
See Also
1---2name: cpp-debugging3description: Use when a C++ failure involves memory lifetime, undefined behavior, native crashes, or debugger-only state — debug with symbols, sanitizers, and platform-native debuggers before patching symptoms4---56# C++ Debugging78`systematic-debugging` gives you the language-neutral method. This skill adds the9native C++ toolchain and runtime techniques that matter once the bug involves10crashes, corrupted memory, UB, or platform-specific debugger state.1112## When to Use1314- A C++ process crashes with a segmentation fault, access violation, or abort15- The bug depends on object lifetime, ownership, or move/copy behavior16- Behavior changes between debug and release builds17- AddressSanitizer, UndefinedBehaviorSanitizer, or a debugger is needed to see the failure clearly18- A native test hangs, deadlocks, or corrupts memory without a clear stack trace1920## When NOT to Use2122| Instead of cpp-debugging | Use |23|--------------------------|-----|24| You still do not have a stable reproduction or root-cause workflow | `systematic-debugging` |25| The failure is a build, compile, or link error | `fix-build-errors` |26| The issue is performance-only, not correctness | `performance-optimization` |2728## Prerequisites2930- A reproducible failing command, test, or executable31- A debug-capable build with symbols32- Access to the relevant debugger or sanitizer-enabled toolchain33- Enough context to tell whether the bug is runtime, lifetime, concurrency, or build-related3435## Workflow3637### 1. Rebuild for diagnosis, not speed3839Start with a debuggable build before chasing the crash:4041```powershell42# CMake43cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug44cmake --build build --config Debug4546# Clang/GCC direct build example47clang++ -std=c++20 -g -O0 -Wall -Wextra -Wpedantic main.cpp -o app.exe48```4950Guidelines:5152- enable symbols (`-g`, `/Zi`)53- reduce or disable optimization while reproducing (`-O0`, `/Od`)54- keep warnings enabled55- reproduce with the same inputs that fail in CI or production-like runs5657### 2. Capture the exact failing surface5859```powershell60# Run the failing binary or test under the same conditions each time61ctest --test-dir build --output-on-failure6263# Or run the specific executable directly64.\build\app.exe65```6667Record:68691. exact command line702. failing input or fixture713. crash signal or exit code724. first bad log line, assertion, or stack frame7374### 3. Inspect runtime state with a native debugger7576Use the platform-native debugger your team standardizes on. Common flows:7778```powershell79# GDB80gdb --args .\build\app.exe <args>81# (gdb) run82# (gdb) bt83# (gdb) frame 084# (gdb) info locals85# (gdb) p suspiciousVariable8687# LLDB88lldb -- .\build\app.exe <args>89# (lldb) run90# (lldb) bt91# (lldb) frame variable92```9394Focus on:9596- the first frame where state becomes invalid97- moved-from or already-destroyed objects98- container bounds and iterator validity99- null or dangling pointers100- cross-thread ordering when the state only fails under load101102### 4. Turn on sanitizers early103104Sanitizers catch the class of bugs that ordinary logging often hides.105106```powershell107# Clang/GCC108cmake -S . -B build-asan `109 -DCMAKE_BUILD_TYPE=Debug `110 -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer"111cmake --build build-asan112ctest --test-dir build-asan --output-on-failure113```114115If your toolchain supports it, enable the equivalent native flags on Windows as well116(for example MSVC AddressSanitizer in supported versions).117118Use sanitizer output to answer:119120- which access was invalid121- which allocation/free site owned the memory122- whether UB happened before the visible crash123124### 5. Use core dumps or crash artifacts when the bug is post-mortem125126If the crash only appears outside the current shell, collect the artifact and inspect it:127128```text129- preserve the crashing binary and symbols130- keep the exact build that produced the dump131- load the dump in the matching debugger132- compare the crashing frame with the last known good run133```134135Do not "fix" a crash dump by guessing from the top frame alone. Trace ownership and state backwards.136137### 6. Fix the root cause and harden the boundary138139Typical C++ root causes:140141- lifetime mismatch between owner and borrower142- invalid iterator or reference after container mutation143- double free or use-after-free144- missing synchronization around shared mutable state145- undefined behavior surfaced by a newer compiler or optimizer146147After the fix:148149- rerun the failing reproduction150- rerun sanitizer-enabled tests151- add a regression test where practical152- check nearby code for the same lifetime or ownership pattern153154## Common Rationalizations155156| Rationalization | Reality |157|----------------|---------|158| "The debugger changes timing, so it is useless" | Timing-sensitive bugs still need debugger or sanitizer evidence. Use them to narrow the class of failure, then confirm in a normal run. |159| "Release-only crash means the optimizer is broken" | Most release-only failures are UB, data races, or lifetime bugs that debug mode accidentally masks. |160| "The stack trace is enough" | Native crashes often happen far from the real cause. Ownership history matters more than the final frame. |161| "I'll just add null checks" | Null checks do not fix dangling references, iterator invalidation, or races. |162163## Red Flags164165- The crash disappears when you add logging but no root cause is identified166- The only "fix" is changing optimization level167- You cannot say which object owns the failing memory168- The code mixes raw ownership, smart pointers, and borrowed references without clear boundaries169- No regression test or sanitizer rerun happened after the fix170171## Verification172173- [ ] A reproducible failing command or test exists174- [ ] A debug-symbol build was used during diagnosis175- [ ] Debugger or sanitizer output identifies the real failure class176- [ ] The final fix addresses ownership, bounds, synchronization, or UB at the source177- [ ] The targeted reproduction and relevant tests pass after the fix178179## Tips180181- Prefer smaller reproductions: isolate the failing target or test before debugging the whole system182- If the stack is noisy, break on the first thrown exception, failed assertion, or allocator error183- Compare debug and release compile flags when behavior diverges184- If the bug looks build-related after all, hand off to `fix-build-errors`185186## See Also187188- [`systematic-debugging`](../systematic-debugging/SKILL.md) — root-cause method before tool-specific work189- [`fix-build-errors`](../fix-build-errors/SKILL.md) — compile, link, and configuration failures190- [`tdd-workflow`](../tdd-workflow/SKILL.md) — add regression tests after the runtime bug is fixed