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.
valgrind --tool=memcheck --leak-check=full ./app
valgrind --tool=callgrind ./app && callgrind_annotate callgrind.out.*
valgrind --tool=helgrind ./appInstallation
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:
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-g -O0" ..
# or
g++ -g -O0 -o app main.cppMemcheck — Memory Error Detection
valgrind --tool=memcheck \
--leak-check=full \
--show-leak-kinds=all \
--track-origins=yes \
--verbose \
./app [args]| Option | Purpose |
|---|---|
--leak-check=full | Detailed leak reports (not just totals) |
--show-leak-kinds=all | Show definite, indirect, possible, and reachable leaks |
--track-origins=yes | Track where uninitialized values come from (slower) |
--error-exitcode=1 | Return non-zero exit code on errors (for CI) |
--suppressions=file | Suppress known false positives |
--gen-suppressions=all | Generate suppression entries for all errors |
Reading Memcheck Reports
Invalid read/write
==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
==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
==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
# 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# 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
valgrind --tool=helgrind \
--history-level=approx \
./threaded_appSample output:
==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)
valgrind --tool=drd ./threaded_appDRD 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:
# myapp.supp
{
openssl_init_leak
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
...
fun:OPENSSL_init_ssl
}valgrind --suppressions=myapp.supp ./appGenerate suppressions automatically:
valgrind --gen-suppressions=all ./app 2>&1 | grep -A 20 "^{" > new.suppCMake Integration
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 Memcheck | AddressSanitizer | |
|---|---|---|
| Slowdown | 10–50x | 2x |
| Requires recompile | No | Yes (-fsanitize=address) |
| Stack buffer overflows | No | Yes |
| Use-after-return | No | Yes (with flag) |
| Leak detection | Yes | Yes |
| Initialization errors | Yes | Partial (MSan) |
| Thread errors | Helgrind/DRD | ThreadSanitizer |
| CI overhead | High (slow) | Low |
| Best for | Legacy binaries, thorough leak hunt | Fast CI integration |
Rule of thumb: use ASan in CI (fast), Valgrind for deep investigations or when you can't recompile.
Massif — Heap Memory Profiler
Massif shows not just how much memory leaks, but how much memory your program consumes over its lifetime. Use --time-unit=B (bytes allocated) instead of the default wall-clock time — it produces stable, reproducible snapshots unaffected by CPU speed or system load.
valgrind --tool=massif \
--time-unit=B \
--pages-as-heap=yes \
--stacks=yes \
./app| Option | Effect |
|---|---|
--time-unit=B | X-axis = cumulative bytes allocated (recommended) |
--pages-as-heap=yes | Profile all pages (RSS), not just malloc/new heap |
--stacks=yes | Include stack memory in measurements |
--detailed-freq=N | Take a detailed snapshot every N snapshots (default 10) |
Massif writes massif.out.<pid>. Two ways to read it:
# Text output with heap tree
ms_print massif.out.*
# GUI (install separately)
massif-visualizer massif.out.*A typical ms_print snapshot looks like this:
n time(B) total(B) useful-heap(B) extra-heap(B) stacks(B)
--------------------------------------------------------------------------------
10 1,048,576 1,049,600 1,024,000 25,600 0
99.56% (1,024,000B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc.
->79.10% (811,008B) 0x402C1A: std::vector<int>::push_back (stl_vector.h:1198)
| ->79.10% (811,008B) 0x401B4E: fill_data (main.cpp:34)
| ->79.10% (811,008B) 0x401A5F: main (main.cpp:18)
->20.46% (212,992B) 0x402D8F: operator new (main.cpp:61)The tree format directly tells you which call chain is responsible for each allocation. massif-visualizer renders the same data as an interactive area chart, which makes it easier to spot the moment peak memory was reached.
DHAT — Dynamic Heap Analysis Tool
DHAT answers a different question than Massif: it finds memory that is allocated but barely used, revealing over-allocation and short-lived churn. Where Massif tracks peak consumption over time, DHAT tracks access frequency per allocation site.
valgrind --tool=dhat ./appDHAT writes a JSON file (dhat.out.<pid>) and prints its location on exit. Open it in the browser-based viewer:
# Open the viewer (ships with Valgrind >= 3.15)
firefox $(dpkg -L valgrind | grep dh_view.html)
# Or point your browser at:
# /usr/lib/valgrind/dh_view.html (Debian/Ubuntu)
# /usr/share/valgrind/dh_view.html (Fedora)The viewer shows each allocation site with three key columns:
| Column | Meaning |
|---|---|
Total bytes | Cumulative bytes allocated at this site |
Max bytes live | Peak simultaneous live bytes |
Total lifetimes | Sum of lifetimes (bytes × instructions alive) |
A large gap between Total bytes and Max bytes live means objects are allocated and freed rapidly — a candidate for object pooling or pre-allocation. A high Total lifetimes with low access counts means memory is being kept alive longer than it needs to be.
# Copy profiler: track which memcpy/memmove calls are hottest
valgrind --tool=dhat --mode=copy ./appCachegrind — Cache Simulation
Cachegrind simulates the CPU cache hierarchy and counts every instruction fetch and data access, letting you find cache-thrashing functions without hardware counter access. Unlike perf, Cachegrind runs on any machine and produces deterministic, reproducible output — at the cost of being a simulation of a fixed cache model rather than your actual hardware.
valgrind --tool=cachegrind \
--cache-sim=yes \
--branch-sim=yes \
./app
cg_annotate cachegrind.out.* --auto=yes--auto=yes annotates every source line that was executed. The key output columns:
| Column | Meaning |
|---|---|
Ir | Instruction reads (total instructions executed) |
I1mr / ILmr | L1 / last-level instruction cache misses |
Dr / Dw | Data reads / data writes |
D1mr / DLmr | L1 / LL data read misses |
D1mw / DLmw | L1 / LL data write misses |
Bc / Bcm | Conditional branches / mispredictions |
A function with high DLmr (last-level read misses) is causing DRAM traffic — the most expensive memory access. Focus optimization there first: restructure data for sequential access, reduce struct sizes, or switch from pointer-chasing (linked lists, std::map) to flat arrays (std::vector, std::unordered_map).
# Compare two runs (e.g. before/after optimization)
cg_diff cachegrind.out.1234 cachegrind.out.5678Cachegrind vs perf: Cachegrind uses a fixed simulated cache (portable, repeatable, no root needed). perf reads hardware performance counters (accurate to your physical CPU, faster to run, requires perf_event_paranoid ≤ 1). Use Cachegrind when you need portability or CI reproducibility; use perf when you need accuracy on a known machine.
Running Tests Under Valgrind with CTest
CTest has built-in Memcheck integration — one flag turns your test suite into a Valgrind run without touching any test code. The results appear in the same DartConfiguration XML format that CI dashboards consume.
# Run all tests under Valgrind memcheck
ctest -T memcheck --test-dir build -j4Set the Valgrind binary and options via environment or CTestCustom.cmake:
# CTestCustom.cmake (place in build dir or project root)
set(CTEST_MEMORYCHECK_COMMAND "/usr/bin/valgrind")
set(CTEST_MEMORYCHECK_TYPE "Valgrind")
set(CTEST_MEMORYCHECK_COMMAND_OPTIONS
"--leak-check=full --show-leak-kinds=all --error-exitcode=1 --track-origins=yes"
)
set(CTEST_MEMORYCHECK_SUPPRESSIONS_FILE
"${CMAKE_SOURCE_DIR}/valgrind.supp"
)
# Tests that are known-flaky under Valgrind (e.g. due to sanitizer conflicts)
list(APPEND CTEST_CUSTOM_MEMCHECK_IGNORE
"MyFlakeyTest"
)CTest picks up CTestCustom.cmake automatically from the build directory. A minimal CMakeLists addition to make it available:
configure_file(
${CMAKE_SOURCE_DIR}/CTestCustom.cmake
${CMAKE_BINARY_DIR}/CTestCustom.cmake
COPYONLY
)In CI, read the XML results from build/Testing/:
# GitHub Actions / generic CI step
cmake -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-g -O0"
cmake --build build
# Run memcheck; exit code is non-zero if any test fails Valgrind checks
ctest --test-dir build -T memcheck --output-on-failure 2>&1 | tee valgrind-ci.log
# DartConfiguration.tcl in build/ records test metadata
# Detailed XML is under build/Testing/<timestamp>/DynamicAnalysis.xmlThe DynamicAnalysis.xml file lists each test with its Valgrind defect counts. Parse it or upload it to a CDash dashboard for trend tracking across commits.
Suppression File Generation Workflow
Every real codebase eventually hits false positives from system libraries — a repeatable workflow prevents suppression files from growing into unreviewed noise.
Step 1 — Capture raw suppression candidates:
valgrind --leak-check=full \
--gen-suppressions=all \
./app 2>&1 | tee raw.txtStep 2 — Extract the suppression blocks:
grep -A 20 "^{" raw.txt > candidate.suppStep 3 — Review manually. Open candidate.supp and keep only genuine false positives. Delete blocks that correspond to real bugs. For each remaining block, add a descriptive name and a comment explaining why it is suppressed:
{
openssl_crypto_init_reachable
# OpenSSL 3.x keeps internal state alive intentionally; not a real leak.
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
...
fun:CRYPTO_THREAD_run_once
fun:OPENSSL_init_crypto
}Step 4 — Apply the suppression file:
valgrind --suppressions=myapp.supp --leak-check=full ./appScoping suppressions — broad fun: wildcards suppress the error anywhere in the call stack. Tighten them by adding more fun: frames to match a specific call path. Use obj: to suppress all errors originating from a shared library when you have no source:
{
libpython_startup
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
obj:*/libpython3*.so*
}Well-known public suppression files — rather than writing from scratch, start from community-maintained files:
| Library | Suppression file location |
|---|---|
| OpenSSL | contrib/valgrind.supp in OpenSSL source tree |
| glib / GLib | /usr/share/doc/valgrind/valgrind.supp (Debian) or glib source glib.supp |
| Python (embedding) | Misc/valgrind-python.supp in CPython source |
| Qt | mkspecs/common/valgrind.supp in Qt source |
Chain multiple suppression files with repeated --suppressions= flags:
valgrind \
--suppressions=openssl.supp \
--suppressions=glib.supp \
--suppressions=myapp.supp \
./appSee Also
- Address Sanitizer — faster alternative for CI; catches stack overflows and use-after-return
- Heaptrack — lower-overhead heap profiler with GUI; complements Massif
- perf — hardware counter profiling; pairs with Cachegrind for cache analysis
- GDB — step through errors that Valgrind's stack trace points to