← All 139 books Valgrind, one concept per page
One concept per page

Valgrind, one concept per page

Twenty-eight lessons on Valgrind, one concept per page: valgrind as a framework with seven tools, not only a leak checker; a first memcheck run with no flags; an error report read apart: kind, stack, address, summary; the four leak kinds in one quadrant; why -g matters and how optimization hides the bug; the shadow bits; the conditional jump that reads garbage; --track-origins; invalid reads and writes; free errors; --leak-check=full and loss records; possibly lost; errors into exit codes; the suppression file anatomy; a first generated suppression; memcheck in CI; helgrind and data races; happens-before; callgrind; cachegrind; massif and ms_print; --vgdb; the honest cost measured; when AddressSanitizer wins; a leaky cache diagnosed end to end; and teaching a teammate the one good run.


Steve Hodgkiss 7 lessons

One concept, one page, one real capture. That's the rhythm.

Valgrind, one concept per page

Twenty-eight lessons on Valgrind, one concept per page: valgrind as a framework with seven tools, not only a leak checker; a first memcheck run with no flags; an error report read apart: kind, stack, address, summary; the four leak kinds in one quadrant; why -g matters and how optimization hides the bug; the shadow bits; the conditional jump that reads garbage; --track-origins; invalid reads and writes; free errors; --leak-check=full and loss records; possibly lost; errors into exit codes; the suppression file anatomy; a first generated suppression; memcheck in CI; helgrind and data races; happens-before; callgrind; cachegrind; massif and ms_print; --vgdb; the honest cost measured; when AddressSanitizer wins; a leaky cache diagnosed end to end; and teaching a teammate the one good run.


Set in Space Grotesk, Inter and JetBrains Mono (SIL Open Font License).

Every fact in this book is as the official source states it, fetched and read during this build: the Valgrind user manual chapters from valgrind.org, the definition of truth for this book (core, Memcheck, Helgrind, Callgrind, Cachegrind, Massif and the vgdb chapter), the valgrind.org overview page, and the Wikipedia articles Valgrind, Shadow memory, Memory leak, Race condition, Uninitialized variable, Code sanitizer and Dynamic program analysis. The Wikipedia AddressSanitizer URL caches as a redirect stub to Code sanitizer, recorded honestly; the comparison rests on those articles and this machine's own run. No facts are sourced from Reddit, Stack Overflow or vendor blogs. Every command and output pair was run on this machine (valgrind 3.22.0 at /usr/bin/valgrind, gcc 13.3.0, Ubuntu 24.04) and confirmed before printing; every run lived in a throwaway directory under a scratch tree. Process ids differ per run, so the ==pid== prefix is described, one long path is elided as stated, and gdb's remote-attach word never appears; this book's word scan forbids it. An independent guide, not affiliated with or endorsed by the Valgrind Developers, or the Wikimedia Foundation.

Your purchase is for personal use only. You do not have redistribution rights: please do not share, resell, or republish this book or its pages.

© 2026 Steve Hodgkiss. All rights reserved. Personal use only; no redistribution rights.

Edition 1.0 · stevehodgkiss.net

Contents

Contents


Part 1 · First contact
A first run4
Part 2 · Built for it
Conditional jump5
Part 3 · Errors and leaks
--leak-check=full6
Part 4 · Steering the run
Suppression files7
Part 5 · The other tools
Data races8
Part 6 · Deep and honest
massif9
Part 7 · A real program
The confirmed fix10

Per the Valgrind core manual, with no options the core runs the default tool, memcheck, writes a commentary to the standard error stream in which every line carries the ==process id== prefix, and reports both heap totals and an ERROR SUMMARY line at exit. This machine's capture: a hello-world program under a bare valgrind ./hello printed HEAP SUMMARY with in use at exit: 0 bytes in 0 blocks and total heap usage: 1 allocs, 1 frees, 4,096 bytes allocated, then All heap blocks were freed -- no leaks are possible and ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0).

Valgrind companion · No. 01
first run

A first run

No flags, and the run tells all

no flags, and the run tells all$ valgrind ./hellox is 42HEAP SUMMARY:in use at exit: 0 bytes in 0 blocks1 allocs, 1 frees, 4,096 bytesAll heap blocks were freed --no leaks are possibleERROR SUMMARY: 0 errorsfrom 0 contextsevery commentary line startswith ==pid==, the process id,so tool output can never bemistaken for program outputcommentary to stderr by default; two lines to memorize: all blocks freed, zero errors

One word does it: valgrind ./hello. No flags, no build changes. The program runs, roughly an order of magnitude slower, and the commentary lands on stderr.

A clean run says two things worth memorizing: All heap blocks were freed -- no leaks are possible and ERROR SUMMARY: 0 errors. Ours printed both, with 1 alloc and 1 free in the heap totals.

Every commentary line starts with ==pid==, the process id, so tool output can never be mistaken for program output.

RUN IT ON YOURS

Run one small program of yours under a bare valgrind; read the four summary lines.

Per the Memcheck manual, an uninitialized-value use error is reported when the program uses a value which has not been initialised, in other words is undefined; the program can copy junk around as much as it likes and memcheck tracks it without complaint, issuing a complaint only when the use might affect externally visible behaviour, such as a conditional branch, with sources tending to be uninitialized locals and the contents of heap blocks before anything is written there. Per the Wikipedia Uninitialized variable article, such a variable has some value but not a predictable one, a programming error and a common source of bugs. This machine's captures: int x; if (x == 42) reported Conditional jump or move depends on uninitialised value(s) at main (uninit.c:4); the same value fed to printf printed x = 31 and cascaded through the C library to 6 errors from 4 contexts.

Valgrind companion · No. 02
uninitialized

Conditional jump

The branch that reads garbage

the branch that reads garbageConditional jump or movedepends on uninitialisedvalue(s)at 0x109135: main (uninit.c:4)printf run: x = 31 printed6 errors from 4 contextscopying junk is silent:memcheck tracks it andsays nothing; the complaintcomes only when the junkcould change what the runshows the world, and abranch does exactly thatan uninitialized variable has some value, never a predictable one; here it was 31

Conditional jump or move depends on uninitialised value(s) is the complaint to learn first. The rule: your program may copy junk around freely, memcheck watching silently; it objects only when the junk could change visible behaviour.

Our captures: if (x == 42) on a bare local fired once at uninit.c:4. Printing x fired six times in four contexts inside printf, and printed x = 31: real garbage, faithfully formatted.

An uninitialized variable has some value, never a predictable one; it passes tests.

RUN IT ON YOURS

Branch on one uninitialized variable under valgrind, then print it.

Per the Memcheck manual, if --leak-check=full is specified memcheck gives details for each definitely lost or possibly lost block, including where it was allocated, merging blocks with the same leak kind and sufficiently similar stack traces into a single loss record; it cannot tell you when or how or why the pointer to the leaked block was lost, that has to be worked out for yourself; the default set of leak kinds shown is definite,possible. This machine's capture: a 16-byte single malloc never freed reported 16 bytes in 1 blocks are definitely lost in loss record 1 of 1, with the allocation stack by main (leak1.c:2), and the summary closed with ERROR SUMMARY: 1 errors from 1 contexts.

Valgrind companion · No. 03
full

--leak-check=full

Loss records name the birth

the loss record names the birth$ valgrind --leak-check=full ./leak116 bytes in 1 blocks aredefinitely lost in lossrecord 1 of 1by 0x10915E: main (leak1.c:2)ERROR SUMMARY: 1 errorsrecords merge when thekind and stack match, soone bug in a loop prints onceit cannot say when, how orwhy the pointer was lost;that half is your workdefault shows definite and possible records only; the count is the verdict

The bare summary counts; --leak-check=full testifies. Each lost block becomes a loss record: 16 bytes in 1 blocks are definitely lost in loss record 1 of 1, followed by the stack of its allocation, ours by main (leak1.c:2).

Records merge when kind and stack match, so one bug leaking in a loop prints once. By default only definite and possible records show.

The one thing the record cannot say: when, how or why the pointer was lost. That half of the story is yours to reconstruct from the code between allocation and exit.

RUN IT ON YOURS

Run one leaky program with --leak-check=full; say why the lost pointer died.

Per the Valgrind core manual, each suppression has a name on its first line, a second line naming the tool or tools it is for and the suppression type separated by a colon with no spaces, optional extra information lines for some types, and up to 24 remaining lines of the calling context, each beginning obj:, fun: or src:, with the wildcard characters star and question mark allowed and a three-dots line matching zero or more frames; the whole entry sits between curly braces, each brace the first character on its line, and a suppression only suppresses an error when the error matches all the details in it. Per the Memcheck manual, memcheck's suppression types include Cond for an uninitialized CPU condition code, Value1 through Value16 for uninitialized values, Addr1 through Addr16 for invalid addresses, Free, Overlap, Param and Leak.

Valgrind companion · No. 04
anatomy

Suppression files

Name, type, calling context

five lines, three working parts{the opening brace, alone on its linemyfirstcondthe name: any identifying stringMemcheck:Condtool and type, colon-joined, no spacesfun:mainthe calling context, innermost first,}the closing brace, first on its lineup to 24 context lines; ... matches any framesan error dies only when it matches every detail; a wide suppression is a chosen blind spot

A suppression is a small filter with three parts. A name, any string. Tool and type, colon-joined: Memcheck:Cond says uninitialized condition codes. The calling context, up to 24 lines of fun: or obj:.

Frames match innermost first, a bare ... swallows any number of frames, and the entry lives between braces.

One law: an error dies only if it matches every detail. Suppressions are narrow by design; a wide one is a blind spot you chose.

RUN IT ON YOURS

Copy the five-line anatomy into a file from memory, then check it against the manual.

Per the Helgrind manual, a data race happens, or could happen, when two threads access a shared memory location without using suitable locks or other synchronisation to ensure single-threaded access, and ensuring race-free programs is one of the central difficulties of threaded programming; helgrind's simple example is an unprotected shared counter incremented by a child thread and main; for each error helgrind shows two stack traces, not one, the read trace and the conflicting previous write, with the set of locks held at each access. Per the Wikipedia Race condition article, a race condition is where the system's substantive behavior depends on the sequence or timing of uncontrollable events. This machine's capture: the manual's own example, compiled, reported Possible data race during read of size 4 at 0x10C014 by thread #1 with Locks held: none, conflicting with a previous write by thread #2 in child_fn, and Address 0x10c014 is 0 bytes inside data symbol "var".

Valgrind companion · No. 05
helgrind

Data races

Two stacks, one variable

two threads, one unprotected variablethread #1 mainthread #2 child_fnvarvar++Possible data race duringread of size 4 by thread #1Locks held: noneconflicts with a previouswrite of size 4 by thread #2data symbol "var"2 errors from 2 contextstwo stacks, not one: the read and the conflicting write, each with its locks held

Switch tools, same frame: valgrind --tool=helgrind ./race. A race is two threads touching one location without locks or other synchronisation, and the behavior then depends on timing you cannot control.

Helgrind's signature is two stacks, not one: our read by thread #1 at race.c:12, the conflicting previous write by thread #2, and Locks held: none on both sides.

It even names the variable: 0 bytes inside data symbol "var". The manual's own example, run here.

RUN IT ON YOURS

Compile the two-thread counter without a lock, run helgrind, find both Locks held lines.

Per the Massif manual, massif is a heap profiler that measures how much heap memory a program uses, both useful space and the extra bytes for book-keeping and alignment; it tells not only how much heap is used but which parts of the program are responsible for allocating it, and can find space leaks that traditional leak-checkers miss because the memory is not lost, a pointer remains, but it is not in use; run massif to gather the information, then run ms_print to present it, which produces a graph showing memory consumption over time. This machine's captures: a program growing a block array to a peak and freeing it produced massif.out with 82 snapshots, and ms_print drew the graph rising to a 3.203 MB peak labelled on the MB axis across roughly 966 kiloinstructions before falling to zero.

Valgrind companion · No. 06
massif

massif

The heap, drawn over time

the heap, drawn over timeMB3.203 MB peakkithe frees run,heap falls to 082 snapshotsms_print drawsmassif.out.NNNNstaircase up:40 growing blocks;book-keeping toomassif's specialty is the space leak: never lost, still pointed at, never used again

Memcheck finds lost memory; massif draws the heap over time, useful bytes and book-keeping alike, and names which code allocated it.

Two steps: run under --tool=massif, then ms_print massif.out.NNNN. Ours rose stairwise to a 3.203 MB peak and fell to zero when the frees ran, across 82 snapshots.

Massif's specialty is the leak memcheck cannot name: a space leak, memory never lost, still pointed at, never used again. The pointer survives; the bill keeps growing.

RUN IT ON YOURS

Grow one array of blocks to a peak and free it; let ms_print draw the rise and fall.

Per the Memcheck manual, the aim is that programs do not exit with definitely lost or possibly lost blocks; --leak-check=full with --show-leak-kinds=all shows every kind, and a run with every block freed reports All heap blocks were freed -- no leaks are possible. Per this book's plan, the fix frees the old slot's value and entry before storing the new one and adds a free_all at exit. This machine's capture: cache_fixed printed the same slot0 key 2 and exited with total heap usage: 5 allocs, 5 frees, 4,141 bytes allocated, in use at exit: 0 bytes in 0 blocks, All heap blocks were freed -- no leaks are possible, ERROR SUMMARY: 0 errors from 0 contexts.

Valgrind companion · No. 07
fix

The confirmed fix

Free the old, then the rest

free the old, then free the restif (slots[slot]) {  free the old value;  free the old entry;}free_all() walks theslots at exit5 allocs, 5 frees4,141 bytes, both waysin use at exit: 0 bytesin 0 blocksAll heap blocks were freedERROR SUMMARY: 0 errorssame output, slot0 key 2; behavior identical, debt zero

The fix is two moves: in put, free the old entry and its string before the overwrite; at exit, a free_all walk over the slots.

The proof is arithmetic before it is prose: 5 allocs, 5 frees, the same 4,141 bytes both ways. Then the lines worth framing: in use at exit: 0 bytes in 0 blocks, All heap blocks were freed, 0 errors.

Same output, slot0 key 2. Behavior identical, debt zero: that is what a fixed leak looks like from the outside, nothing.

RUN IT ON YOURS

Apply the two moves to your own cache or list; rerun full and read the frees.

Index

Index


--leak-check=full6
A first run4
Conditional jump5
Data races8
massif9
Suppression files7
The confirmed fix10