Skip to content

Valgrind Cheat Sheet

From Complete Beginner to Advanced

Terminal window
# Valgrind is a dynamic analysis tool for Linux/macOS programs.
# It detects memory errors, leaks, threading bugs, and performance issues.
# It runs your program inside a virtual CPU β€” no recompilation needed.
# (But compile with -g for useful line numbers in reports)
# Install on Debian/Ubuntu
sudo apt install valgrind
# Install on macOS (limited support β€” Linux preferred)
brew install valgrind
# Verify installation
valgrind --version
# β†’ valgrind-3.22.0

Terminal window
# Always compile with -g (debug symbols) for readable output
# Optionally use -O0 to disable optimizations (more accurate results)
gcc -g -O0 -o myprogram myprogram.c
# For C++
g++ -g -O0 -o myprogram myprogram.cpp

Terminal window
# Simplest usage β€” Memcheck tool runs by default
valgrind ./myprogram
# With command-line arguments for your program
valgrind ./myprogram arg1 arg2

Terminal window
# Every Valgrind line is prefixed with ==PID==
# Example output:
==12345== Memcheck, a memory error detector
==12345== Invalid read of size 4 ← type of error
==12345== at 0x401234: main (foo.c:10) ← where it happened
==12345== Address 0x... is 0 bytes after ← what memory was touched
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 40 bytes in 1 blocks ← real leak
==12345== indirectly lost: 0 bytes in 0 blocks
==12345== possibly lost: 0 bytes in 0 blocks ← maybe leak
==12345== still reachable: 0 bytes in 0 blocks ← pointer exists but not freed
==12345== suppressed: 0 bytes in 0 blocks ← ignored (by suppression rules)

// myprogram.c β€” intentional leak
#include <stdlib.h>
int main() {
int* p = malloc(40); // Allocated but never freed β€” leak!
return 0;
}
Terminal window
# Run with leak checking enabled
valgrind --leak-check=full ./myprogram
# β†’ Reports "definitely lost: 40 bytes in 1 blocks"
# --leak-check=full shows each leak's allocation stack trace
# --leak-check=yes summary only
# --leak-check=no disable leak check

// Reading beyond array bounds
int main() {
int arr[5];
return arr[10]; // Invalid read β€” out of bounds!
}
Terminal window
valgrind ./myprogram
# β†’ "Invalid read of size 4"
# β†’ Shows the exact file and line number with -g

int main() {
int* p = malloc(sizeof(int));
free(p);
*p = 42; // Use after free β€” undefined behavior!
return 0;
}
Terminal window
valgrind ./myprogram
# β†’ "Invalid write of size 4"
# β†’ "Address ... is 0 bytes inside a block of size 4 free'd"

int main() {
int x; // Declared but never initialized
return x + 1; // Using uninitialized value!
}
Terminal window
valgrind --track-origins=yes ./myprogram
# β†’ "Use of uninitialised value of size 4"
# β†’ --track-origins=yes tells you WHERE the uninit value came from

Terminal window
# Show full leak details with allocation stack traces
valgrind --leak-check=full ./myprogram
# Also show reachable and indirectly lost blocks
valgrind --leak-check=full --show-leak-kinds=all ./myprogram
# Track where uninitialized values originate
valgrind --track-origins=yes ./myprogram
# Show error details verbosely
valgrind -v ./myprogram
# Limit number of errors reported (default: 10 per unique spot)
valgrind --error-limit=no ./myprogram
# Combine all useful flags for thorough checking
valgrind --leak-check=full \\
--show-leak-kinds=all \\
--track-origins=yes \\
--error-limit=no \\
-v \\
./myprogram

Terminal window
# Redirect Valgrind output to a log file
valgrind --log-file=valgrind_report.txt ./myprogram
# Append PID to filename (useful for multi-process apps)
valgrind --log-file=report_%p.txt ./myprogram
# β†’ Creates report_12345.txt etc.
# Redirect to a file descriptor (e.g., stderr = 2)
valgrind --log-fd=2 ./myprogram

Terminal window
# Generate a suppression file from current errors
valgrind --gen-suppressions=all ./myprogram 2> my.supp
# β†’ Prints suppression blocks you can copy into a .supp file
# Use a suppression file to ignore known issues
valgrind --suppressions=my.supp ./myprogram
# Use multiple suppression files
valgrind --suppressions=sys.supp --suppressions=my.supp ./myprogram
# Example suppression block in a .supp file:
{
ignore_openssl_leak # Name (can be anything)
Memcheck:Leak # Tool:ErrorType
fun:malloc # Stack frame pattern
fun:CRYPTO_malloc
fun:*openssl*
}

Terminal window
# Run with Callgrind to collect call graph + instruction counts
valgrind --tool=callgrind ./myprogram
# β†’ Creates callgrind.out.<PID> file
# View the report in terminal
callgrind_annotate callgrind.out.12345
# Open visually with KCachegrind (GUI)
kcachegrind callgrind.out.12345
# Focus on specific function only (reduce noise)
valgrind --tool=callgrind --fn-skip=pthread_* ./myprogram
# Start profiling paused β€” toggle with callgrind_control
valgrind --tool=callgrind --instr-atstart=no ./myprogram
callgrind_control -i on # Start collecting
callgrind_control -i off # Stop collecting

Terminal window
# Detect race conditions, lock order violations, pthread API misuse
valgrind --tool=helgrind ./myprogram
# Common errors reported:
# β†’ "Possible data race during read/write of size N"
# β†’ "Lock order violated" (deadlock risk)
# β†’ "Mutex is already locked by this thread"
// Example race condition Helgrind catches:
int counter = 0; // Shared variable β€” no lock!
void* increment(void* arg) {
counter++; // ← Helgrind flags this as a data race
return NULL;
}

Terminal window
# DRD is an alternative to Helgrind β€” sometimes catches different issues
valgrind --tool=drd ./myprogram
# Key differences from Helgrind:
# β†’ Lower memory overhead
# β†’ Better support for custom synchronization primitives
# β†’ Reports "Conflicting accesses" for races
# Show stack trace for both conflicting accesses
valgrind --tool=drd --show-stack-usage=yes ./myprogram

Terminal window
# Profile heap memory usage over time
valgrind --tool=massif ./myprogram
# β†’ Creates massif.out.<PID>
# View the report as a text chart
ms_print massif.out.12345
# β†’ ASCII chart of heap usage over time
# Open visually with Massif-Visualizer (GUI)
massif-visualizer massif.out.12345
# Include stack memory in profiling
valgrind --tool=massif --pages-as-heap=yes ./myprogram
# Take snapshots every N allocations (default: 10)
valgrind --tool=massif --detailed-freq=1 ./myprogram

Terminal window
# Simulate CPU cache behavior β€” find cache misses
valgrind --tool=cachegrind ./myprogram
# β†’ Creates cachegrind.out.<PID>
# View the annotated report
cg_annotate cachegrind.out.12345
# Diff two runs (to compare optimizations)
cg_diff cachegrind.out.111 cachegrind.out.222
# Output includes:
# Ir = Instructions read
# I1mr = L1 instruction cache misses
# D1mr = L1 data cache read misses
# DLmr = Last-level cache read misses

#include <valgrind/valgrind.h>
#include <valgrind/memcheck.h>
int main() {
// Check if we're running under Valgrind
if (RUNNING_ON_VALGRIND) {
printf("Running under Valgrind\\n");
}
char buf[10];
// Mark memory as defined (suppress false positives for custom allocators)
VALGRIND_MAKE_MEM_DEFINED(buf, 10);
// Mark memory as undefined (trigger errors if accessed)
VALGRIND_MAKE_MEM_UNDEFINED(buf, 10);
// Mark memory as inaccessible (like freed memory)
VALGRIND_MAKE_MEM_NOACCESS(buf, 10);
// Print current memory error count to Valgrind log
unsigned errs = VALGRIND_COUNT_ERRORS;
// Force Valgrind to do a full leak check at this point
VALGRIND_DO_LEAK_CHECK;
return 0;
}
Terminal window
# Compile with Valgrind headers
gcc -g -I/usr/include/valgrind -o myprogram myprogram.c

#include <valgrind/memcheck.h>
// Tell Valgrind about a custom memory pool
void* pool = malloc(1024); // Your pool buffer
// Register the pool
VALGRIND_CREATE_MEMPOOL(pool, 0, 0);
// When you hand out a block from your pool:
void* block = pool; // Your allocation logic
VALGRIND_MEMPOOL_ALLOC(pool, block, 64); // Mark 64 bytes as allocated
// When you "free" a block back to your pool:
VALGRIND_MEMPOOL_FREE(pool, block);
// When destroying the whole pool:
VALGRIND_DESTROY_MEMPOOL(pool);
free(pool);

Terminal window
# Also trace child processes spawned by your program
valgrind --trace-children=yes ./myprogram
# Filter which children to trace by executable name
valgrind --trace-children=yes \\
--trace-children-skip=*bash*,*sh \\
./myprogram
# Log each child to its own file (using %p = PID placeholder)
valgrind --trace-children=yes \\
--log-file=vg_%p.txt \\
./myprogram

Terminal window
# Start program under Valgrind with GDB server enabled
valgrind --vgdb=yes --vgdb-error=0 ./myprogram
# β†’ Valgrind pauses and waits for GDB to connect
# In another terminal, attach GDB
gdb ./myprogram
(gdb) target remote | vgdb # Connect to the Valgrind GDB server
# Now you can set breakpoints, inspect memory, etc.
(gdb) monitor leak_check full # Run Valgrind commands via GDB monitor
(gdb) monitor who_points_at 0xADDRESS # Find what points to an address
(gdb) continue

Terminal window
# Output errors as XML β€” great for parsing in CI pipelines
valgrind --xml=yes --xml-file=valgrind_out.xml ./myprogram
# Parse in Python (example)
# import xml.etree.ElementTree as ET
# tree = ET.parse('valgrind_out.xml')
# errors = tree.findall('.//error')
# Use with tools like:
# β†’ valgrind-xml-parser (Python)
# β†’ Jenkins Valgrind Plugin
# β†’ GitLab CI artifact parsing

.github/workflows/valgrind.yml
name: Valgrind Memory Check
on: [push, pull_request]
jobs:
memcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Valgrind
run: sudo apt-get install -y valgrind
- name: Build with debug symbols
run: gcc -g -O0 -o myprogram myprogram.c
- name: Run Valgrind
run: |
valgrind --error-exitcode=1 \\ # Exit with code 1 on any error
--leak-check=full \\
--show-leak-kinds=all \\
./myprogram
# CI fails automatically if Valgrind finds errors

Terminal window
# Exit with a non-zero code if Valgrind finds errors (great for CI)
valgrind --error-exitcode=1 ./myprogram
echo $? # β†’ 1 if errors found, 0 if clean
# Set a specific exit code value
valgrind --error-exitcode=42 ./myprogram

Terminal window
# Valgrind slows programs by 10x–50x β€” reduce with these:
# Reduce detail level (faster but less info)
valgrind --leak-check=summary ./myprogram
# Skip unneeded checks
valgrind --undef-value-errors=no ./myprogram # Skip uninit checks
# Use --fair-sched to improve threading simulation accuracy
valgrind --tool=helgrind --fair-sched=yes ./myprogram
# For large programs β€” limit stack trace depth (faster)
valgrind --num-callers=5 ./myprogram # Default is 12 frames
# Cache results between runs (Cachegrind only)
valgrind --tool=cachegrind --cache-sim=no ./myprogram # Branch only

ToolCommandPurpose
Memcheck--tool=memcheck (default)Memory leaks, invalid access, uninit use
Callgrind--tool=callgrindCPU profiling, call graph
Helgrind--tool=helgrindThread race conditions
DRD--tool=drdThread errors (alternative to Helgrind)
Massif--tool=massifHeap memory profiling
Cachegrind--tool=cachegrindCPU cache & branch prediction
DHAT--tool=dhatDynamic heap analysis
BBV--tool=exp-bbvBasic block vectors for simulation

FlagMeaning
--leak-check=fullFull leak report with stack traces
--show-leak-kinds=allShow all leak types
--track-origins=yesShow origin of uninitialized values
--error-exitcode=NReturn exit code N if errors found
--log-file=file.txtSave output to file
--suppressions=fileLoad suppression rules
--gen-suppressions=allGenerate suppression entries
--trace-children=yesFollow child processes
--vgdb=yesEnable GDB server
--num-callers=NStack trace depth (default: 12)
--error-limit=noReport all errors (no cap)
-vVerbose output

Terminal window
# Clean run (no errors): β†’ returns your program's exit code
# Errors found + --error-exitcode=1 set: β†’ returns 1
# Valgrind internal error: β†’ returns 1
# Program killed by signal: β†’ returns 128 + signal number

Valgrind runs on Linux (best), macOS (limited), and not on Windows (use WSL or AddressSanitizer instead). For Windows memory checking, consider: AddressSanitizer (-fsanitize=address) or Dr. Memory.