valgrind Command: Tutorial & Examples

Run your program on a synthetic CPU that watches every byte it touches — and catches the bugs a normal run hides.

What It Is

valgrind is the tool you reach for when your program does something impossible — crashes once a week, returns a number that's right on your laptop but garbage in production, or slowly eats memory until the box falls over. These are the bugs that survive every test you wrote, because the broken code usually gets away with it. valgrind is how you stop guessing. It runs your program and watches every single memory access it makes — every read, every write, every allocation — and the instant something touches memory it shouldn't, it stops and tells you the exact line, with a stack trace, in the act.

If you write C or C++ and you've never used it, this page is going to change how you debug. And here's the part that sounds like science fiction until you see it: valgrind doesn't just watch your program run — it runs your program on a synthetic CPU it builds in software, a complete pretend processor that exists only to narrate everything the real one would have done silently. That's the magic, and we'll come back to how it pulls that off, because it's one of the most quietly audacious tricks in all of systems programming. For now: this is x-ray vision for memory bugs, it needs no special build, and it's installed-or-one-apt-away on every Linux box. Let's learn to read what it sees.

Your First Look

Say you've got a tiny C program with a classic bug — it reads one element past the end of an array and then forgets to free what it allocated:

#include <stdlib.h>
int main(void) {
    int *a = malloc(5 * sizeof(int));   // room for indices 0..4
    a[5] = 42;                          // oops — one past the end
    return a[5];                        // and reading it back
}

It compiles clean, runs without complaint, and on most machines returns 42 like nothing happened. That's the whole horror of memory bugs — they hide. Now build it with debug symbols (-g, so valgrind can name your lines) and run it under the tool:

gcc -g -O0 -o buggy buggy.c
valgrind ./buggy
==12345== Memcheck, a memory error detector
==12345== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==12345== Using Valgrind-3.24.0 and LibVEX; rerun with -h for copyright info
==12345== Command: ./buggy
==12345==
==12345== Invalid write of size 4
==12345==    at 0x10915E: main (buggy.c:4)
==12345==  Address 0x4a8e054 is 0 bytes after a block of size 20 alloc'd
==12345==    at 0x484880F: malloc (vg_replace_malloc.c:446)
==12345==    by 0x109155: main (buggy.c:3)
==12345==
==12345== Invalid read of size 4
==12345==    at 0x109168: main (buggy.c:5)
==12345==  Address 0x4a8e054 is 0 bytes after a block of size 20 alloc'd
==12345==    at 0x484880F: malloc (vg_replace_malloc.c:446)
==12345==    by 0x109155: main (buggy.c:3)
==12345==
==12345== HEAP SUMMARY:
==12345==     in use at exit: 20 bytes in 1 blocks
==12345==   total heap usage: 1 allocs, 0 frees, 20 bytes allocated
==12345==
==12345== LEAK SUMMARY:
==12345==    definitely lost: 20 bytes in 1 blocks
==12345==      indirectly lost: 0 bytes in 0 blocks
==12345==        possibly lost: 0 bytes in 0 blocks
==12345==      still reachable: 0 bytes in 0 blocks
==12345==           suppressed: 0 bytes in 0 blocks
==12345==
==12345== For lists of detected and suppressed errors, rerun with: -s
==12345== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0 suppressed)

Two bugs, both pinpointed to the exact line, in a program that "worked." That ==12345== prefix on every line is just the PID of the process being watched — it's there so that when your program forks children, you can tell whose output is whose. Everything else here is the whole craft of reading valgrind, so let's learn it.

How I Read It

When valgrind dumps a wall of text, I read it in a fixed order, and almost always from the bottom up.

First I jump to the last line — ERROR SUMMARY. That number is the verdict. 0 errors and I exhale; my memory handling is clean on this run. Anything above zero and I scroll up to read them one at a time, oldest first (they print in the order they happened, which usually means the first error is the root cause and the rest are aftershocks).

Second, for each error, I read the headline and the top stack frame. The headline names the crimeInvalid write of size 4, Use of uninitialised value, Invalid free(). The line right under it, the one starting at, is where it happened in my code. That's the line to go fix. Everything below at is the call stack that led there — read it top-to-bottom like a gdb backtrace to see how the program arrived.

Third, I read the "Address ..." line, because that's the diagnosis. 0 bytes after a block of size 20 alloc'd tells me I overran a heap buffer by exactly the start — a fence-post error. is on thread 1's stack means I scribbled past a local array. is 0 bytes inside a block of size 20 free'd is the dreaded use-after-free — I touched memory I already handed back, the single nastiest class of bug in C, because the allocator may have reused that slot for something else entirely. The "Address" line is valgrind doing the forensics for you: not just that you misfired, but what you hit.

Last, I read the LEAK SUMMARY — but only the definitely lost line, at first. That's memory I allocated and then dropped every pointer to: gone, unreclaimable, a true leak. The other three buckets are a more subtle story we'll get to. The move that took me years to trust: a leak at program exit often doesn't matter (the OS reclaims everything when the process dies), but a leak in a loop — in a server that runs for months — is the thing that pages you at 3am. valgrind can't tell which is which; you have to.

That's the rhythm: summary line for the verdict, headline + at for the location, the Address line for the diagnosis, leaks last. Now let's name every kind of error it can throw.

The Errors, Explained

Memcheck — the default tool, and the one people mean when they say "valgrind" — reports a fixed cast of error types. Knowing all of them means you never stare at one wondering what it's even telling you:

  • Invalid read / Invalid write of size N — you touched memory you don't own: past the end of an array, before the start, or a wild pointer. The Address line says exactly which block and by how much.
  • Use of uninitialised value — you read a variable (or a malloc'd byte) before writing anything to it, and that garbage influenced a branch, a syscall, or output. This is the one that explains "works on my machine" — uninitialised memory happens to be zero on your laptop and 0xDEADBEEF in production.
  • Conditional jump or move depends on uninitialised value(s) — the same disease, caught at the moment it mattered: an if/while whose outcome hinged on a value you never set. Add --track-origins=yes and valgrind will even tell you where that uninitialised value was born.
  • Invalid free() / delete — you freed something that wasn't a live heap block: a stack pointer, a global, or — most often — a double free, releasing the same block twice.
  • Mismatched free() / delete / delete [] — C++ only: you allocated with new[] and freed with delete (or any other mix). Different allocators, undefined behaviour.
  • Source and destination overlap — you passed overlapping buffers to memcpy/strcpy, which forbid it (you wanted memmove).
  • Invalid read/write ... after free — the use-after-free, called out explicitly. The Address line will say inside a block of size N free'd, and a second stack trace shows where it was freed. Two stack traces for one bug — allocation site and free site, both — is valgrind at its most generous.

Pro Tip

Always compile with -g and turn optimisation off (-O0). -g gives valgrind your file names and line numbers; -O0 stops the compiler from reshuffling and inlining your code so the line numbers actually point where you'd expect. Debugging optimised code under valgrind is reading a map someone folded wrong.

The Leak Summary, Explained

The four leak buckets confuse everyone the first time, and they're worth getting straight once and for all. At exit, valgrind walks every byte still allocated and sorts each block by a single question: can the program still reach it?

  • definitely lost — no pointer anywhere in the program points to this block. You leaked it, full stop. Fix these first.
  • indirectly lost — this block was only reachable through a definitely-lost block. Leak the head of a linked list and every node behind it is "indirectly lost." Fix the definite leak and these usually vanish with it.
  • possibly lost — a pointer exists, but it points into the middle of the block, not its start. Sometimes that's a legitimate trick (interior pointers); often it's a real leak in disguise. Treat with suspicion.
  • still reachable — you never freed it, but a valid pointer still reaches it at exit (a global cache, a one-time buffer). The OS reclaims it instantly when the process dies, so for a short-lived program this is usually harmless. It is not harmless in a long-running loop — same bytes, very different verdict, and only you know which you're in.

To see the actual leaked blocks instead of just totals, add --leak-check=full and --show-leak-kinds=all, and valgrind prints the allocation stack trace for every leaked block — the malloc that you forgot to match with a free, named to the line.

How You'll Actually Use It

Ninety percent of the time it's just this — full leak detail, origins of uninitialised values, and your program's own arguments on the end:

valgrind --leak-check=full --track-origins=yes ./myprogram --some-arg

That ./myprogram --some-arg matters: valgrind runs your program, so anything after the binary is your program's own argv, passed straight through. Everything before it (the --leak-check flags) is for valgrind itself.

When the output scrolls off the screen — and it will — send it to a file so you can read it at leisure:

valgrind --leak-check=full --log-file=vg.log ./myprogram

And the move that turns valgrind from "report generator" into "time machine": pair it with gdb. Start with the remote-debugger gate open, and valgrind will freeze your program at the exact instruction of the first error and wait for a debugger to attach:

valgrind --vgdb=yes --vgdb-error=1 ./myprogram
# then, in another terminal:
gdb ./myprogram
(gdb) target remote | vgdb

Now you're sitting inside the moment of the crash, at the offending line, able to print every variable and walk the stack — not at the SIGSEGV downstream where the corruption finally became fatal, but at the write that caused it. The first time you catch a use-after-free this way, frozen mid-crime with every register laid out, you'll wonder how you ever debugged without it.

Cheat Sheet

The flags worth knowing, grouped by what they're for:

  • --leak-check=full — list every leaked block with its allocation stack (the one you'll use most)
  • --show-leak-kinds=all — include still reachable and possibly lost, not just definite leaks
  • --track-origins=yes — for uninitialised-value errors, report where the bad value was born (costs speed; worth it)
  • --log-file=FILE — write the report to a file instead of stderr (use %p in the name for the PID)
  • --vgdb=yes --vgdb-error=N — freeze and wait for gdb on the Nth error
  • -s / --show-error-list=yes — print the full list of distinct errors at the end
  • --error-exitcode=N — exit non-zero if any error was found (so CI can fail the build)
  • --trace-children=yes — follow into child processes across fork/exec
  • --suppressions=FILE — silence known-harmless errors (e.g. from a third-party library)
  • --gen-suppressions=yes — print ready-to-paste suppression blocks for the errors it finds

Picking a different tool (Memcheck is the default; you select others with --tool=):

  • --tool=memcheck — memory errors + leaks (the default)
  • --tool=helgrind — data races and lock-ordering bugs in threaded code
  • --tool=drd — a second, lighter thread-error detector
  • --tool=cachegrind — CPU cache-miss and branch-misprediction profiler
  • --tool=callgrind — call-graph profiler; visualise with kcachegrind
  • --tool=massif — heap profiler: what is using your memory over time, not just whether you leaked

The Other Tools in the Box

This is the thing most people never learn: valgrind isn't one tool, it's a framework, and Memcheck is just the one it runs by default. The whole machinery — that synthetic CPU we keep hinting at — is a platform for watching programs, and several completely different tools are built on top of it. You switch with --tool=:

  • Helgrind finds the threading bugs that are even worse than memory bugs: two threads touching the same data with no lock between them (a data race), or locks taken in an order that can deadlock. These bugs are non-deterministic — they happen one run in a thousand — so catching them by testing is hopeless. Helgrind catches them by reasoning about the accesses.
  • Cachegrind and Callgrind are profilers: instead of asking "is this wrong," they ask "why is this slow." Callgrind builds a full call graph with instruction counts, and kcachegrind draws it as a beautiful clickable map of where your cycles go.
  • Massif answers "what's eating my RAM?" — it samples the heap over time and shows you which call paths are responsible for the growth, which is exactly what you want when memory climbs but nothing is technically leaked.

One framework, six lenses. Learn Memcheck first; know the others exist so that when "it's slow" or "it deadlocks sometimes" lands on you, you remember you already have the tool.

How It Really Works (the magic)

Here's the part worth slowing down for, because it's genuinely one of the cleverest things in systems software.

A debugger like gdb watches your program from the outside, occasionally pausing it to peek. valgrind does something far stranger: it doesn't run your program on the real CPU at all. Instead it reads your program's machine code, block by block, and translates it — through an intermediate language called VEX — into new machine code that does everything the original did plus a running commentary: before every memory read, "is this address valid and initialised?"; on every malloc, "remember this block's bounds"; on every free, "mark these bytes poisoned." Then it runs that instrumented version on a software model of the processor. Your program never touches the bare metal; it runs inside a simulation that narrates its every move.

This is why valgrind sees what a normal run can't. The real CPU happily reads one int past your array — there's no hardware tripwire at the array's edge, that's just the next bytes in RAM. But valgrind's synthetic CPU knows where your block ends, because it watched the malloc, so it catches the overrun the silicon shrugs at. It tracks the "defined-ness" of memory down to the individual bit — that's how it knows you branched on an uninitialised value three function calls after the byte was allocated.

There's a price, and it's the one thing everyone should know going in: running on a simulated CPU is slow — figure 10 to 50× slower than native, often around 20×. A program that runs in a second takes the better part of a minute. That's the trade: you give up speed and get omniscience. You don't run valgrind in production; you run it in your test suite, on the input that reproduces the bug, and you let it take its time. Slow x-ray beats fast blindness every single time.

Why

valgrind is Norwegian, from Old Norse: valgrind, "the gate of the slain" — the gate to Valhalla in Norse myth, which only the worthy may pass. Julian Seward picked it because every instruction in your program has to pass through valgrind's gate to run. A debugger that's also a doorway to the hall of the honoured dead is, frankly, the most metal name in all of Unix tooling, and now you know it.

Gotchas

  • No -g means no line numbers. If your stack traces say in /lib/x86_64-linux-gnu/libc.so.6 instead of main (buggy.c:4), you forgot to compile with -g. valgrind can only name what the debug symbols name.
  • It only sees the path you actually run. valgrind is dynamic analysis — it watches the program execute. A bug down a branch your test never takes is a bug valgrind never sees. Feed it the input that triggers the problem.
  • Leaks at exit are often fine; leaks in a loop are not. Don't burn an afternoon chasing a still reachable global that the OS reclaims in a millisecond. Chase the block that grows every iteration.
  • It catches heap overruns, not most stack ones. Memcheck guards malloc'd memory tightly, but it can miss overruns within a stack frame or a global array. For those, the compiler's AddressSanitizer (gcc -fsanitize=address) often does better — and runs far faster, because it instruments at compile time instead of simulating the CPU. The two are complementary, not rivals: reach for the sanitizers when you can rebuild, valgrind when you can't (it needs no recompile and no source changes at all).
  • Third-party libraries throw noise. Some libraries (older glibc, graphics drivers) trip Memcheck for harmless reasons. Don't chase those — generate a suppression with --gen-suppressions=yes, save it, and pass it back with --suppressions=. Silence the known-noise so the real signal stands out.

Installation

Not installed by default. On Debian or Ubuntu:

sudo apt install valgrind

On RHEL/Fedora it's dnf install valgrind. The current release is 3.24.0 (late 2024). One tip that saves head-scratching: also install your libc's debug symbols (libc6-dbg on Debian, pulled in automatically) so the stack traces that pass through system libraries stay readable instead of dissolving into hex.

History & Philosophy

valgrind began life in 2000 as a project by Julian Seward — the same person who wrote bzip2, BTW, so he has form for tools that quietly become infrastructure. The original idea was narrower than what we have now (it grew out of an earlier checker called Heimdall), but the killer move was making it a framework: a synthetic CPU that any analysis tool could be built on top of. That decision is why one download gives you a memory checker, three profilers, and two race detectors — they all share the same engine that translates and watches your code.

And here's the lovely connection that ties it back to everything else you run on a server. valgrind proved that you could take a program written for the bare metal and quietly slip a translation layer underneath it — rewriting its instructions on the fly, watching everything — without the program ever noticing. That same idea, "dynamic binary translation," is the beating heart of how Apple's Rosetta runs Intel apps on ARM chips, how QEMU emulates one processor on another, and how a great deal of virtualization works under the hood. The next time valgrind catches a use-after-free that would've taken you a week, remember you're watching one of the most influential tricks in computing do its day job: a fake CPU, faithfully pretending to be a real one, and narrating the truth as it goes.

See Also

  • gdb — the debugger; pair it with valgrind to freeze at the exact error
  • top — watch a leaking program's RES climb in real time
  • process — threads, memory, and the things valgrind instruments
  • virtualization — the dynamic binary translation valgrind is built on
  • memory leak — the diagnose-and-fix walkthrough
  • out of memory — what an unchecked leak eventually causes

Found the leak in testing — but is one already growing on the live box right now?

CleverUptime watches each server's memory and swap every minute and flags the slow, steady climb of a leak in production — the one your test suite never ran long enough to catch — in plain language, before it turns into an OOM kill at 3am.

Want to see your own server's health right now? One command, no signup, no install.

Check your server →