Skip to content
C++
Debugger
Updated 2025-01-01T00:00:00.000Z

Valgrind Memory Debugging

Valgrind Memcheck, Callgrind, Helgrind, and DRD — detecting memory errors, use-after-free, leaks, data races, and reading Valgrind reports.

TL;DR

Valgrind is a dynamic analysis framework. memcheck (default) finds heap errors and leaks. callgrind profiles function calls. helgrind/drd find data races.

bash
valgrind --tool=memcheck --leak-check=full ./app
valgrind --tool=callgrind ./app && callgrind_annotate callgrind.out.*
valgrind --tool=helgrind ./app

Installation

bash
sudo apt install valgrind       # Debian/Ubuntu
sudo dnf install valgrind       # Fedora/RHEL
brew install valgrind            # macOS (limited support)

Build with debug info for readable reports:

bash
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-g -O0" ..
# or
g++ -g -O0 -o app main.cpp

Memcheck — Memory Error Detection

bash
valgrind --tool=memcheck \
         --leak-check=full \
         --show-leak-kinds=all \
         --track-origins=yes \
         --verbose \
         ./app [args]
OptionPurpose
--leak-check=fullDetailed leak reports (not just totals)
--show-leak-kinds=allShow definite, indirect, possible, and reachable leaks
--track-origins=yesTrack where uninitialized values come from (slower)
--error-exitcode=1Return non-zero exit code on errors (for CI)
--suppressions=fileSuppress known false positives
--gen-suppressions=allGenerate suppression entries for all errors

Reading Memcheck Reports

Invalid read/write

cpp
==12345== Invalid read of size 8
==12345==    at 0x401B2F: use_ptr (main.cpp:42)
==12345==    at 0x401A10: main (main.cpp:15)
==12345==  Address 0x5204080 is 0 bytes after a block of size 24 alloc'd
==12345==    at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck.so)
==12345==    at 0x401A20: allocate (main.cpp:10)
  • Invalid read of size 8 — reading 8 bytes out of bounds
  • The stack trace shows where the bad access happened
  • The second block shows where the buffer was allocated

Use after free

cpp
==12345== Invalid read of size 4
==12345==  Address 0x5204e80 is 4 bytes inside a block of size 40 free'd
==12345==    at 0x4C30D3B: free (in .../vgpreload_memcheck.so)
==12345==    at 0x401B10: cleanup (main.cpp:55)
==12345==  Block was alloc'd at
==12345==    at 0x4C2FB0F: malloc (in .../vgpreload_memcheck.so)

Memory leak summary

cpp
==12345== LEAK SUMMARY:
==12345==    definitely lost: 1,024 bytes in 1 blocks
==12345==    indirectly lost: 512 bytes in 4 blocks
==12345==      possibly lost: 0 bytes in 0 blocks
==12345==    still reachable: 8,192 bytes in 2 blocks
  • Definitely lost — no pointer to this memory exists (real leak)
  • Indirectly lost — reachable only through definitely-lost memory
  • Possibly lost — pointer exists but points to middle of block
  • Still reachable — pointer exists at exit (often fine: global/static)

Callgrind — Call Graph Profiler

bash
# Profile the application
valgrind --tool=callgrind \
         --callgrind-out-file=cg.out \
         ./app

# Text report
callgrind_annotate cg.out

# GUI visualization (install kcachegrind)
kcachegrind cg.out
bash
# Profile only part of execution
valgrind --tool=callgrind --instr-atstart=no ./app
# In code:
#include <valgrind/callgrind.h>
CALLGRIND_START_INSTRUMENTATION;
hot_function();
CALLGRIND_STOP_INSTRUMENTATION;
CALLGRIND_DUMP_STATS;

Helgrind — Data Race Detector

bash
valgrind --tool=helgrind \
         --history-level=approx \
         ./threaded_app

Sample output:

cpp
==12345== Possible data race during read of size 4 at 0x601060
==12345==    at 0x4008E4: worker (race.cpp:15)
==12345==  This conflicts with a previous write of size 4
==12345==    at 0x400911: main (race.cpp:30)

Helgrind also detects:

  • Lock order violations (potential deadlocks)
  • pthread API misuse
  • Unlocked variable accesses

DRD — Data Race Detector (Alternative)

bash
valgrind --tool=drd ./threaded_app

DRD uses less memory than Helgrind and is faster on programs with many threads. Helgrind gives more detailed "happens-before" analysis.


Suppression Files

Suppress known false positives or third-party library noise:

cpp
# myapp.supp
{
   openssl_init_leak
   Memcheck:Leak
   match-leak-kinds: reachable
   fun:malloc
   ...
   fun:OPENSSL_init_ssl
}
bash
valgrind --suppressions=myapp.supp ./app

Generate suppressions automatically:

bash
valgrind --gen-suppressions=all ./app 2>&1 | grep -A 20 "^{" > new.supp

CMake Integration

cmake
find_program(VALGRIND valgrind)
if(VALGRIND)
    add_custom_target(memcheck
        COMMAND ${VALGRIND}
            --leak-check=full
            --error-exitcode=1
            $<TARGET_FILE:myapp>
        DEPENDS myapp
    )
endif()

Valgrind vs Address Sanitizer

Valgrind MemcheckAddressSanitizer
Slowdown10–50x2x
Requires recompileNoYes (-fsanitize=address)
Stack buffer overflowsNoYes
Use-after-returnNoYes (with flag)
Leak detectionYesYes
Initialization errorsYesPartial (MSan)
Thread errorsHelgrind/DRDThreadSanitizer
CI overheadHigh (slow)Low
Best forLegacy binaries, thorough leak huntFast CI integration

Rule of thumb: use ASan in CI (fast), Valgrind for deep investigations or when you can't recompile.

Edit on GitHubUpdated 2025-01-01T00:00:00.000Z