C++ debugging GDB Valgrind breakpoints memory analysis
|

C++ Debugging with GDB & Valgrind: Find & Fix Bugs Guide 2026

Why Debugging Skills Matter

Writing C++ code that compiles is one thing. Writing code that works correctly is another entirely. C++ gives you direct memory access, pointer arithmetic, and manual resource management — powerful features that produce subtle, hard-to-find bugs when used incorrectly. Memory leaks, buffer overflows, dangling pointers, and data races don’t always crash immediately — they corrupt data silently and fail unpredictably.

Professional C++ developers spend significant time debugging. The tools in this lesson — GDB for interactive debugging, Valgrind for memory analysis, and sanitizers for catching undefined behavior — are non-negotiable skills. They turn impossible “it works on my machine” bugs into clear, reproducible, fixable problems.

Compiling for Debugging

# Always compile with -g for debug symbols
g++ -g -O0 main.cpp -o myapp

# -g   : Include debug information (line numbers, variable names)
# -O0  : No optimization (code matches source exactly)
# -Wall -Wextra : Enable warnings (catch bugs at compile time)

# With CMake:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build

# Debug builds are bigger and slower but debuggable
# Release builds (-O2 or -O3) optimize away variables and reorder code

GDB Basics

# Start GDB with your program
gdb ./myapp

# GDB commands:
(gdb) run                    # Run the program
(gdb) run arg1 arg2          # Run with arguments
(gdb) quit                   # Exit GDB
(gdb) help                   # Show help
(gdb) help breakpoints       # Help on specific topic

# Run with input redirection
(gdb) run < input.txt

# Attach to running process
gdb -p 12345                 # Attach to PID 12345

Breakpoints

# Set breakpoints (pause execution at specific points)
(gdb) break main              # Break at function main()
(gdb) break main.cpp:42       # Break at line 42 of main.cpp
(gdb) break MyClass::method   # Break at class method
(gdb) break 42 if x > 10      # Conditional breakpoint

# List, enable, disable, delete breakpoints
(gdb) info breakpoints        # List all breakpoints
(gdb) disable 2               # Disable breakpoint #2
(gdb) enable 2                # Re-enable it
(gdb) delete 2                # Remove breakpoint #2
(gdb) delete                  # Remove all breakpoints

# Temporary breakpoint (auto-deletes after hit)
(gdb) tbreak main.cpp:50

Stepping Through Code

# After hitting a breakpoint:
(gdb) next           # Execute current line, step OVER function calls
(gdb) step           # Execute current line, step INTO function calls
(gdb) finish         # Run until current function returns
(gdb) continue       # Resume execution until next breakpoint
(gdb) until 60       # Run until line 60

# next vs step:
# If current line is: result = compute(x);
# next → executes compute() entirely, stops at next line
# step → enters compute(), stops at its first line

# Reverse debugging (if supported)
(gdb) reverse-step
(gdb) reverse-next
(gdb) reverse-continue

Inspecting Variables and Memory

# Print variables
(gdb) print x                 # Print variable x
(gdb) print *ptr               # Dereference pointer
(gdb) print array[5]           # Array element
(gdb) print obj.member         # Object member
(gdb) print sizeof(x)          # Size of variable

# Print expressions
(gdb) print x + y * 2
(gdb) print (double)x / y

# Print with format
(gdb) print/x val              # Hexadecimal
(gdb) print/t val              # Binary
(gdb) print/d val              # Decimal

# Display: auto-print at every stop
(gdb) display x                # Show x after every step
(gdb) undisplay 1              # Stop showing display #1

# Examine memory
(gdb) x/10d ptr                # 10 decimal ints at ptr
(gdb) x/20xb ptr               # 20 hex bytes
(gdb) x/s str_ptr              # String at pointer

# Stack trace
(gdb) backtrace                # Show call stack (bt for short)
(gdb) frame 3                  # Switch to frame #3
(gdb) info locals              # All local variables in current frame
(gdb) info args                # Function arguments

Watchpoints

# Watch: break when a variable changes
(gdb) watch x                  # Break when x is modified
(gdb) watch *ptr               # Break when value at ptr changes
(gdb) rwatch x                 # Break when x is READ
(gdb) awatch x                 # Break on read OR write

# Watch with condition
(gdb) watch x if x > 100

# Watchpoints are slow (hardware-limited to ~4) but invaluable
# for finding "who changed this variable?"

Debugging Crashes

# When program crashes (segfault), GDB stops at the crash point
(gdb) run
# Program received signal SIGSEGV, Segmentation fault.
(gdb) backtrace                # See where it crashed
(gdb) frame 0                  # Go to crash frame
(gdb) print ptr                # Often the culprit is a null/dangling pointer
(gdb) info registers           # CPU register state

# Core dumps: debug a crash after the fact
# Enable core dumps:
ulimit -c unlimited
./myapp                        # Crashes, creates core file

# Debug the core dump:
gdb ./myapp core
(gdb) backtrace                # See crash location

Valgrind — Memory Error Detection

# Valgrind runs your program in a virtual CPU and tracks every
# memory access. It finds bugs that don't crash.

# Basic usage:
valgrind ./myapp

# Detailed leak check:
valgrind --leak-check=full --show-leak-kinds=all ./myapp

# Track origins of uninitialized values:
valgrind --track-origins=yes ./myapp
// What Valgrind catches:

// 1. Memory leaks
void leak() {
    int* p = new int[100];
    // Never deleted — Valgrind reports: "definitely lost: 400 bytes"
}

// 2. Use after free
void use_after_free() {
    int* p = new int(42);
    delete p;
    *p = 10;  // Valgrind: "Invalid write of size 4"
}

// 3. Buffer overflow
void overflow() {
    int arr[10];
    arr[10] = 42;  // Valgrind: "Invalid write of size 4"
}

// 4. Uninitialized reads
void uninitialized() {
    int x;
    if (x > 0) {}  // Valgrind: "Conditional jump depends on uninitialised value"
}

// 5. Double free
void double_free() {
    int* p = new int(42);
    delete p;
    delete p;  // Valgrind: "Invalid free"
}

Address Sanitizer (ASan)

# Compile with ASan (faster than Valgrind, ~2x slowdown vs 20x)
g++ -g -fsanitize=address -fno-omit-frame-pointer main.cpp -o myapp
./myapp

# ASan catches:
# - Buffer overflows (stack, heap, global)
# - Use after free
# - Use after return (with flag)
# - Double free
# - Memory leaks (with ASAN_OPTIONS=detect_leaks=1)

# Environment options:
ASAN_OPTIONS=detect_leaks=1:halt_on_error=0 ./myapp

Undefined Behavior Sanitizer (UBSan)

# Compile with UBSan
g++ -g -fsanitize=undefined main.cpp -o myapp
./myapp

# Combine with ASan:
g++ -g -fsanitize=address,undefined main.cpp -o myapp
// UBSan catches:
int x = INT_MAX;
x + 1;              // Signed integer overflow

int arr[10];
arr[15];             // Out of bounds

int* p = nullptr;
*p;                  // Null pointer dereference

int x = 1;
x << 33;             // Shift by too much

double d = 1e308;
(int)d;              // Float-to-int overflow

Thread Sanitizer (TSan)

# Compile with TSan (cannot combine with ASan)
g++ -g -fsanitize=thread main.cpp -o myapp -pthread
./myapp

# TSan detects data races in multithreaded code
# Reports: which threads, which variable, read vs write

Debugging Techniques

// 1. Assert: catch bugs early
#include <cassert>
void process(int* ptr, int size) {
    assert(ptr != nullptr && "Null pointer passed to process");
    assert(size > 0 && "Size must be positive");
    // Asserts are removed in release builds (NDEBUG defined)
}

// 2. Static assert: catch bugs at compile time
static_assert(sizeof(int) == 4, "This code requires 32-bit ints");

// 3. Debug logging
#ifdef DEBUG_BUILD
    #define LOG(msg) std::cerr << __FILE__ << ":" << __LINE__ << " " << msg << "
"
#else
    #define LOG(msg)
#endif

// 4. Narrow down with binary search
// Comment out half the code. Does the bug persist?
// If yes, it's in the remaining half. Repeat.

// 5. Rubber duck debugging
// Explain the code line-by-line to someone (or something).
// You'll often find the bug while explaining.

IDE Debugging

Most developers use IDE debuggers rather than command-line GDB. VS Code with the C/C++ extension provides a graphical debugger that uses GDB or LLDB underneath. CLion has an integrated debugger. Visual Studio on Windows has the best-in-class MSVC debugger. All of them support breakpoints, variable inspection, call stacks, and memory views — same concepts as GDB, friendlier interface.

To set up VS Code debugging, create a .vscode/launch.json file pointing to your debug build. Set breakpoints by clicking the gutter. Press F5 to start debugging. The debug console accepts GDB commands for advanced operations.

Practice Exercises

Exercise 1: Write a program with a deliberate memory leak, buffer overflow, and use-after-free. Run it through Valgrind and ASan. Compare the error reports.

Exercise 2: Create a program with a subtle bug (e.g., off-by-one in a loop, uninitialized variable used in a condition). Use GDB to step through and find the bug.

Exercise 3: Write a multithreaded program with a data race. Compile with Thread Sanitizer and fix the race it detects.

Exercise 4: Set up a CMake project with a sanitizer preset. Add ASan and UBSan build options that can be enabled via a CMake flag.

Debugging is a skill that separates professional developers from beginners. The tools are free and available on every platform — there's no excuse not to use them. Combined with unit testing (next lesson), debugging tools ensure your code works correctly and stays correct as it evolves.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *