Debugging Memory Leaks with Valgrind and AddressSanitizer

DebuggingC++

Memory leaks are silent killers. Your program runs fine in development, then crashes in production after running for days. Here’s how to catch them early with Valgrind and AddressSanitizer.

The Problem

void processData() {
    int* data = new int[1000];
    // ... process data
    // Oops, forgot to delete[]
}

// After 10,000 calls: 40 MB leaked

Tool 1: Valgrind (Runtime Analysis)

Install:

sudo apt-get install valgrind  # Linux
brew install valgrind          # macOS

Basic Usage:

valgrind --leak-check=full ./myprogram

Output:

==12345== HEAP SUMMARY:
==12345==     in use at exit: 4,000 bytes in 1 blocks
==12345==   total heap usage: 1 allocs, 0 frees, 4,000 bytes allocated
==12345==
==12345== 4,000 bytes in 1 blocks are definitely lost
==12345==    at 0x4C2E0EF: operator new[](unsigned long)
==12345==    by 0x400A3C: processData() (main.cpp:15)
==12345==    by 0x400A5D: main (main.cpp:20)

Key Metrics:

  • Definitely lost: Memory you leaked
  • Indirectly lost: Leaked because parent was leaked
  • Possibly lost: Might be a leak (investigate)
  • Still reachable: Not freed but still accessible (usually OK)

Tool 2: AddressSanitizer (Compile-Time Instrumentation)

Compile with ASan:

g++ -fsanitize=address -g main.cpp -o myprogram
clang++ -fsanitize=address -g main.cpp -o myprogram

Run:

./myprogram

Output:

=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 4000 byte(s) in 1 object(s) allocated from:
    #0 0x7f8a in operator new[](unsigned long)
    #1 0x400a3c in processData() main.cpp:15
    #2 0x400a5d in main main.cpp:20

SUMMARY: AddressSanitizer: 4000 byte(s) leaked in 1 allocation(s).

Valgrind vs AddressSanitizer

Feature Valgrind AddressSanitizer
Speed 10-50x slower 2x slower
Accuracy Very high Very high
Setup No recompilation Requires recompilation
Platform Linux, macOS Linux, macOS, Windows
Use Case Production debugging Development

Recommendation: Use ASan during development, Valgrind for production issues.

Common Leak Patterns

1. Forgetting delete/delete[]

// Leak
int* arr = new int[100];

// Fix
int* arr = new int[100];
delete[] arr;

// Better: Use smart pointers
std::unique_ptr<int[]> arr(new int[100]);
// Or even better
std::vector<int> arr(100);

2. Exception Safety

// Leaks if processData throws
void badFunction() {
    Resource* res = new Resource();
    processData();  // Throws exception
    delete res;     // Never reached
}

// Fix with RAII
void goodFunction() {
    std::unique_ptr<Resource> res(new Resource());
    processData();  // Exception safe
}  // Automatically deleted

3. Circular References

class Node {
    std::shared_ptr<Node> next;  // Circular reference = leak
};

// Fix: Use weak_ptr
class Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // Breaks cycle
};

Advanced Valgrind Options

# Suppress known leaks
valgrind --leak-check=full --suppressions=myapp.supp ./myprogram

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

# Generate suppression file
valgrind --gen-suppressions=all ./myprogram 2>&1 | grep -A 5 "insert_a_suppression_name_here"

AddressSanitizer Options

# Detect use-after-free
ASAN_OPTIONS=detect_leaks=1:halt_on_error=0 ./myprogram

# Symbolize stack traces
ASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer ./myprogram

Integrating into CI/CD

# GitHub Actions
- name: Run with AddressSanitizer
  run: |
    cmake -DCMAKE_CXX_FLAGS="-fsanitize=address" .
    make
    ./tests

Conclusion

Use AddressSanitizer for fast feedback during development. Use Valgrind for thorough analysis and production debugging. Use smart pointers to avoid manual memory management entirely.


What memory bugs have you caught with these tools? Share your stories!