Home C / C++ Mastering C++ Debugging: GDB, Valgrind, and Sanitizers
Advanced 3 min · July 13, 2026

Mastering C++ Debugging: GDB, Valgrind, and Sanitizers

Learn to debug C++ programs like a pro using GDB, Valgrind, and sanitizers.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.

Follow
Production
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of C++ syntax and compilation.
  • Familiarity with command line and terminal.
  • Understanding of pointers and dynamic memory.
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • GDB: Step through code, inspect variables, set breakpoints.
  • Valgrind: Detect memory leaks and invalid memory access.
  • Sanitizers (ASan, UBSan, LSan): Catch undefined behavior and memory errors at runtime.
  • Combine tools for comprehensive debugging.
  • Use compiler flags like -g -fsanitize=address for built-in checks.
✦ Definition~90s read
What is C++ Debugging?

C++ debugging tools like GDB, Valgrind, and sanitizers help you find and fix bugs by inspecting program state, detecting memory errors, and catching undefined behavior.

Debugging is like being a detective.
Plain-English First

Debugging is like being a detective. GDB is your magnifying glass to look at code line by line. Valgrind is a sniffer dog that finds memory leaks (like forgotten trash). Sanitizers are security cameras that catch rule-breaking (like using a broken toy). Together, they keep your program healthy.

Every C++ developer eventually faces a bug that defies logic. A crash that only happens in production, a memory leak that slowly eats server resources, or undefined behavior that works on your machine but fails elsewhere. These are the moments when basic debugging falls short. This article dives into three powerful tools: GDB, Valgrind, and sanitizers. GDB lets you step through code, inspect variables, and understand program flow. Valgrind's Memcheck catches memory leaks and invalid accesses. Sanitizers (AddressSanitizer, UndefinedBehaviorSanitizer, LeakSanitizer) are compiler-integrated tools that detect errors at runtime with minimal overhead. By mastering these, you'll move from guessing to systematic debugging. We'll cover installation, usage, and real-world scenarios, including a production incident where a subtle memory corruption caused intermittent crashes. Whether you're maintaining legacy code or building new systems, these tools are essential for writing robust C++.

Getting Started with GDB

GDB (GNU Debugger) is the Swiss Army knife for debugging C++ programs. To use it, compile with the -g flag to include debug symbols. Basic commands: run (r), break (b), next (n), step (s), print (p), backtrace (bt). Example: debugging a simple program that crashes.

First, compile: g++ -g -o crash crash.cpp. Then run GDB: gdb ./crash. Set a breakpoint at main: b main. Run: r. Step through with n. When it crashes, use bt to see the call stack. You can inspect variables with p var. GDB also supports conditional breakpoints, watchpoints, and reverse debugging (with rr). For multithreaded programs, use thread apply all bt to see all threads.

Pro tip: Use GDB's TUI mode (gdb -tui) for a split-screen view of code and commands.

crash.cppCPP
1
2
3
4
5
6
7
8
9
#include <iostream>
void cause_crash() {
    int* p = nullptr;
    *p = 42; // deliberate null dereference
}
int main() {
    cause_crash();
    return 0;
}
Output
Program received signal SIGSEGV, Segmentation fault.
0x000055555555515e in cause_crash() at crash.cpp:4
4 *p = 42;
💡GDB Shortcuts
📊 Production Insight
In production, core dumps are your friend. Enable them with ulimit -c unlimited and analyze offline with GDB.
🎯 Key Takeaway
GDB allows you to inspect program state at any point, making it indispensable for understanding crashes and logic errors.

Valgrind: Memory Error Detection

Valgrind's Memcheck tool detects memory leaks, invalid reads/writes, and use-after-free. It runs your program in a synthetic CPU, intercepting memory allocations. To use: valgrind --leak-check=full ./program. It reports leaks with stack traces. For invalid accesses, it shows the exact line.

Example: a program that writes past array bounds.

Valgrind can be slow (10-20x slowdown), so use it on test cases, not full production loads. Other tools: Massif (heap profiler), Helgrind (thread race detector). For C++ programs, Valgrind works with STL but may report false positives for custom allocators.

Pro tip: Use --track-origins=yes to track uninitialized values.

buffer_overflow.cppCPP
1
2
3
4
5
6
7
#include <iostream>
int main() {
    int arr[5];
    arr[10] = 42; // out-of-bounds write
    std::cout << arr[10] << std::endl;
    return 0;
}
Output
==12345== Invalid write of size 4
==12345== at 0x1091B6: main (buffer_overflow.cpp:4)
==12345== Address 0x1fff0001c4 is on thread 1's stack
⚠ Valgrind Overhead
📊 Production Insight
Valgrind is too slow for production, but you can run it on a canary instance or during integration tests.
🎯 Key Takeaway
Valgrind catches memory errors that often go unnoticed, preventing crashes and security vulnerabilities.

Sanitizers: Compiler-Integrated Debugging

Sanitizers are built into GCC and Clang. They instrument your code at compile time to detect errors at runtime with low overhead (2x slowdown). Key sanitizers: - AddressSanitizer (ASan): Detects buffer overflows, use-after-free, double free. - UndefinedBehaviorSanitizer (UBSan): Detects integer overflow, null pointer dereference, etc. - LeakSanitizer (LSan): Detects memory leaks (often part of ASan). - ThreadSanitizer (TSan): Detects data races.

Compile with -fsanitize=address -g. Example: use-after-free.

Sanitizers are production-friendly; you can run them in staging. They print detailed error reports with stack traces. Combine with -fno-omit-frame-pointer for better stack traces.

Pro tip: Use -fsanitize=address,undefined for comprehensive checks.

use_after_free.cppCPP
1
2
3
4
5
6
7
#include <iostream>
int main() {
    int* p = new int(42);
    delete p;
    std::cout << *p << std::endl; // use-after-free
    return 0;
}
Output
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010 at pc 0x...
READ of size 4 at 0x602000000010 thread T0
#0 0x... in main use_after_free.cpp:5
🔥Sanitizers vs Valgrind
📊 Production Insight
Enable ASan in staging environments to catch bugs before production. It's safe for moderate loads.
🎯 Key Takeaway
Sanitizers are the first line of defense against memory and undefined behavior bugs, with low enough overhead for testing.

Advanced GDB Techniques

Beyond basic breakpoints, GDB supports conditional breakpoints (b main if x==5), watchpoints (watch x for changes), and reverse debugging (target record-full then reverse-step). For debugging optimized code, use -Og (optimize for debug) instead of -O0. GDB can also debug core dumps: gdb ./app core. Use info registers to see CPU state, and disassemble to view assembly. For multithreaded debugging, thread apply all bt prints backtraces for all threads. You can also attach to a running process: gdb -p PID.

Example: debugging a deadlock. Set breakpoints on mutex lock/unlock and inspect thread states.

Pro tip: Use .gdbinit file to automate setup commands.

deadlock.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <thread>
#include <mutex>
std::mutex m1, m2;
void thread1() {
    std::lock_guard<std::mutex> l1(m1);
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::lock_guard<std::mutex> l2(m2);
}
void thread2() {
    std::lock_guard<std::mutex> l2(m2);
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::lock_guard<std::mutex> l1(m1);
}
int main() {
    std::thread t1(thread1), t2(thread2);
    t1.join(); t2.join();
    return 0;
}
Output
(gdb) thread apply all bt
Thread 2 (Thread 0x...):
#0 __lll_lock_wait ()
#1 ... std::mutex::lock()
#2 ... thread2() at deadlock.cpp:12
Thread 1 (Thread 0x...):
#0 __lll_lock_wait ()
#1 ... std::mutex::lock()
#2 ... thread1() at deadlock.cpp:7
💡Reverse Debugging
📊 Production Insight
For production core dumps, use GDB with debug symbols from the exact build to get accurate line numbers.
🎯 Key Takeaway
Advanced GDB features like watchpoints and reverse debugging help solve complex bugs like deadlocks and data races.

Integrating Sanitizers into Your Build System

To make sanitizers part of your workflow, add them to your CMake or Makefile. For CMake: set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address,undefined -fno-omit-frame-pointer -g"). For Make: CXXFLAGS += -fsanitize=address,undefined -fno-omit-frame-pointer -g. Run tests with ASan enabled to catch errors early. You can also use environment variables like ASAN_OPTIONS=detect_leaks=1. For CI, run a separate build with sanitizers. Note that ASan and TSan cannot be used together. Use LSan standalone with -fsanitize=leak.

Example: a CMakeLists.txt snippet.

Pro tip: Use -DCMAKE_BUILD_TYPE=Debug for debug builds, and add sanitizer flags conditionally.

CMakeLists.txtCPP
1
2
3
4
5
cmake_minimum_required(VERSION 3.10)
project(SanitizerExample)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address,undefined -fno-omit-frame-pointer")
add_executable(app main.cpp)
🔥Sanitizer Compatibility
📊 Production Insight
In CI, run sanitized builds on a subset of tests to balance coverage and performance.
🎯 Key Takeaway
Integrating sanitizers into your build system ensures every test run catches memory and UB errors automatically.

Real-World Debugging Workflow

Combine all three tools for maximum coverage. Workflow: 1. Reproduce the bug with a minimal test case. 2. Compile with -g -fsanitize=address,undefined and run. If it crashes, fix the reported issue. 3. If no crash, run under Valgrind to check for memory leaks and uninitialized values. 4. For intermittent crashes, enable core dumps and analyze with GDB. 5. For performance issues, use Valgrind's Callgrind or Massif.

Example: debugging a memory leak in a server. First, run with ASan to catch any immediate errors. Then Valgrind to find leaks. Finally, use GDB to inspect the code around the leak.

Pro tip: Use a script that runs all tools sequentially on your test suite.

debug_workflow.shCPP
1
2
3
4
5
6
7
#!/bin/bash
g++ -g -fsanitize=address,undefined -o test_app test.cpp
./test_app 2>&1 | tee asan.log
valgrind --leak-check=full ./test_app 2>&1 | tee valgrind.log
# If crash, analyze core dump
ulimit -c unlimited
./test_app || gdb ./test_app core -batch -ex bt
⚠ False Positives
📊 Production Insight
Automate the workflow in CI to catch regressions early. Use separate build configurations for debug and release.
🎯 Key Takeaway
A systematic workflow using all three tools catches a wide range of bugs, from memory leaks to undefined behavior.
● Production incidentPOST-MORTEMseverity: high

The Phantom Crash: A Memory Corruption Mystery

Symptom
Users experienced intermittent 500 errors; logs showed SIGSEGV but no consistent stack trace.
Assumption
Developer assumed a race condition in multithreaded code.
Root cause
A use-after-free bug: a pointer to a deallocated object was still being used in a callback.
Fix
Switched to std::shared_ptr and reviewed all callback lifetimes.
Key lesson
  • Always check object lifetimes in asynchronous code.
  • Use sanitizers in debug builds to catch use-after-free early.
  • Valgrind can be too slow for production; use AddressSanitizer instead.
  • Reproduce crashes with minimal test cases.
  • Log more context (e.g., object IDs) to trace issues.
Production debug guideSymptom to Action4 entries
Symptom · 01
Segmentation fault with no clear pattern
Fix
Enable core dumps and analyze with GDB: gdb ./app core
Symptom · 02
Memory usage grows over time
Fix
Run Valgrind's massif tool: valgrind --tool=massif ./app
Symptom · 03
Undefined behavior (e.g., weird output)
Fix
Compile with -fsanitize=undefined and run tests
Symptom · 04
Double free or invalid free
Fix
Use AddressSanitizer: -fsanitize=address
★ Quick Debug Cheat SheetCommon symptoms and immediate actions
Segfault
Immediate action
Enable core dumps
Commands
ulimit -c unlimited
gdb ./app core
Fix now
Check pointer validity and bounds
Memory leak+
Immediate action
Run Valgrind
Commands
valgrind --leak-check=full ./app
valgrind --tool=massif ./app
Fix now
Use smart pointers
Undefined behavior+
Immediate action
Compile with UBSan
Commands
g++ -fsanitize=undefined -g -o app app.cpp
./app
Fix now
Fix signed overflow, null pointer, etc.
Buffer overflow+
Immediate action
Compile with ASan
Commands
g++ -fsanitize=address -g -o app app.cpp
./app
Fix now
Use std::array or std::vector
ToolTypeSpeedDetectsUse Case
GDBDebuggerFastLogic errors, crashesInteractive debugging, core dump analysis
ValgrindDynamic analysisSlow (10-20x)Memory leaks, invalid access, uninitialized readsThorough memory checking
AddressSanitizerCompiler instrumentationModerate (2x)Buffer overflow, use-after-free, leaksTesting and staging
UndefinedBehaviorSanitizerCompiler instrumentationFastInteger overflow, null deref, etc.Catching UB
ThreadSanitizerCompiler instrumentationModerate (5x)Data racesMultithreaded debugging
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
crash.cppvoid cause_crash() {Getting Started with GDB
buffer_overflow.cppint main() {Valgrind
use_after_free.cppint main() {Sanitizers
deadlock.cppstd::mutex m1, m2;Advanced GDB Techniques
CMakeLists.txtcmake_minimum_required(VERSION 3.10)Integrating Sanitizers into Your Build System
debug_workflow.shg++ -g -fsanitize=address,undefined -o test_app test.cppReal-World Debugging Workflow

Key takeaways

1
GDB is essential for interactive debugging and core dump analysis.
2
Valgrind catches memory leaks and uninitialized reads, but is slow.
3
Sanitizers (ASan, UBSan, LSan) are fast and catch a wide range of errors.
4
Integrate sanitizers into your build system and CI pipeline.
5
Combine all three tools in a systematic workflow for maximum coverage.

Common mistakes to avoid

5 patterns
×

Forgetting to compile with -g

×

Using Valgrind on optimized code

×

Ignoring sanitizer reports in CI

×

Not enabling core dumps

×

Using ASan and TSan together

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain how AddressSanitizer detects buffer overflows.
Q02SENIOR
How would you debug a segmentation fault that only occurs in production?
Q03SENIOR
What are the limitations of Valgrind?
Q04JUNIOR
Compare GDB and LLDB.
Q05JUNIOR
How do you detect memory leaks in C++?
Q01 of 05SENIOR

Explain how AddressSanitizer detects buffer overflows.

ANSWER
ASan replaces malloc/free with instrumented versions that add redzones (poisoned memory) around allocations. On every memory access, it checks if the address is poisoned. If so, it reports an error.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between Valgrind and AddressSanitizer?
02
Can I use sanitizers in production?
03
How do I debug a core dump without source code?
04
What is the best way to debug multithreaded bugs?
05
How do I handle false positives from sanitizers?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.

Follow
Verified
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
🔥

That's C++ Advanced. Mark it forged?

3 min read · try the examples if you haven't

Previous
Undefined Behavior in C++: The Complete Guide
32 / 41 · C++ Advanced
Next
C/C++ Interop and Foreign Function Interface