C++ Debugging

GDB Basics

Compile with debug info and run under gdb
g++ -g -O0 -o myapp main.cpp
gdb ./myapp
Essential GDB commands
run                     # start execution
run arg1 arg2           # start with arguments
break main              # breakpoint at function
break main.cpp:42       # breakpoint at line
break Host::address     # breakpoint at method
next                    # step over (don't enter functions)
step                    # step into (enter functions)
continue                # resume execution
print var               # print variable value
print *ptr              # dereference pointer
print vec.size()        # call member function
display var             # print var after every step
backtrace               # show call stack
frame 3                 # switch to stack frame 3
info locals             # show local variables
info breakpoints        # list breakpoints
delete 1                # delete breakpoint 1
watch var               # break when var changes
quit                    # exit

Valgrind

Detect memory errors
# Memory leak detection
valgrind --leak-check=full ./myapp

# Track origins of uninitialized values
valgrind --track-origins=yes ./myapp

# Output:
# ==12345== LEAK SUMMARY:
# ==12345==    definitely lost: 48 bytes in 1 blocks
# ==12345==    indirectly lost: 0 bytes in 0 blocks

Valgrind runs the binary in a virtual machine — ~10-20x slowdown but catches use-after-free, double-free, buffer overflows, and leaks.

Assertions

Runtime checks for invariants
#include <cassert>

void process(int* data, size_t size) {
    assert(data != nullptr && "data must not be null");
    assert(size > 0 && "size must be positive");
    // process...
}

// static_assert — compile-time check
static_assert(sizeof(int) == 4, "int must be 4 bytes");
static_assert(std::is_move_constructible_v<Host>, "Host must be movable");

assert is removed in release builds (-DNDEBUG). static_assert runs at compile time — no runtime cost.

Preprocessor Debugging

Conditional compilation for debug output
#ifdef DEBUG
    #define LOG(msg) std::cerr << "[DEBUG] " << msg << std::endl
#else
    #define LOG(msg)
#endif

LOG("entering function");  // only in debug builds

// Compile with: g++ -DDEBUG -o app main.cpp

Prefer constexpr if or logging libraries over preprocessor macros in modern C++. Macros bypass the type system and are harder to debug.

Core Dumps

Analyze crashes post-mortem
# Enable core dumps
ulimit -c unlimited

# Run the crashing program
./myapp
# Segmentation fault (core dumped)

# Analyze
gdb ./myapp core
# In gdb:
# backtrace          # see where it crashed
# info registers     # register state at crash
# print *ptr         # examine the offending pointer

Core dumps capture the entire process state at the time of crash. backtrace shows the call stack leading to the fault.