Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 4 of 6

Valgrind runs your program on a simulated CPU, watching every memory access. Unlike the sanitizers, it needs no special build — it works on the binary you already have.

valgrind --leak-check=full ./your_program

It is Linux-first. On macOS support is unreliable on recent versions; use ASan there.

A real run

The same off-by-one program from lesson 11.2, built normally with just -g:

#include <iostream>

int sum_range(const int* values, int count) {
    int total = 0;
    for (int i = 0; i <= count; ++i) {   // BUG: <= reads one past the end
        total += values[i];
    }
    return total;
}

int main() {
    int* data = new int[5];
    for (int i = 0; i < 5; ++i) {
        data[i] = i + 1;
    }

    std::cout << "sum: " << sum_range(data, 5) << "\n";

    delete[] data;
    return 0;
}

Running it under Valgrind produces:

==1653== Memcheck, a memory error detector
==1653== Command: ./buggy
==1653== 
==1653== Invalid read of size 4
==1653==    at 0x1091DC: sum_range(int const*, int) (buggy.cpp:6)
==1653==    by 0x109268: main (buggy.cpp:17)
==1653==  Address 0x4e21094 is 0 bytes after a block of size 20 alloc'd
==1653==    at 0x48485C3: operator new[](unsigned long)
==1653==    by 0x109208: main (buggy.cpp:12)
==1653== 
sum: 15
==1653== 
==1653== HEAP SUMMARY:
==1653==     in use at exit: 0 bytes in 0 blocks
==1653==   total heap usage: 3 allocs, 3 frees, 77,844 bytes allocated
==1653== 
==1653== All heap blocks were freed -- no leaks are possible
==1653== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

The same four facts as ASan, in a different layout: an Invalid read of size 4 at line 6, reached from line 17, 0 bytes after a block of size 20 allocated at line 12.

Note sum: 15 in the middle. Valgrind reports and lets the program continue, so you see the correct-looking output alongside the error that produced it.

The ==1653== prefix is the process ID, on every line so Valgrind's output can be separated from your program's.

Reading the leak summary

==1653== HEAP SUMMARY:
==1653==     in use at exit: 0 bytes in 0 blocks
==1653==   total heap usage: 3 allocs, 3 frees, 77,844 bytes allocated
==1653== All heap blocks were freed -- no leaks are possible

3 allocs, 3 frees is the line to check. When they differ, the difference is what leaked — and with --leak-check=full Valgrind prints the allocation stack for each one.

Valgrind sorts leaks into categories, and the distinction is useful:

  • Definitely lost — no pointer to it anywhere. A real leak; fix it.
  • Indirectly lost — reachable only through something definitely lost, such as the children of a leaked tree node. Fixing the parent usually fixes these.
  • Possibly lost — a pointer exists but points into the middle of the block, not its start. Sometimes a real leak, sometimes an interior pointer by design.
  • Still reachable — a pointer still exists at exit. Often a global or a cache; not usually a bug, but worth a glance.

Start with definitely lost.

Valgrind versus AddressSanitizer

Both find overlapping bugs. The differences matter when choosing:

| | Valgrind | ASan | |---|---|---| | Rebuild needed | no | yes | | Slowdown | 10–50× | ~2× | | Uninitialised reads | yes | no | | Stack overflows | limited | yes | | Platform | Linux mainly | Linux, macOS, Windows | | Works on third-party binaries | yes | no |

Two rows decide most cases.

ASan is far faster, so it is what you run in CI on every commit.

Valgrind finds uninitialised reads, which ASan does not. That is a real gap — lesson 3.1's garbage variable is invisible to ASan and caught by Valgrind:

#include <iostream>

int main() {
    int uninitialised;                       // never assigned

    // Reading it is undefined behaviour. ASan does not check this;
    // Valgrind reports "Conditional jump or move depends on uninitialised value".
    if (uninitialised > 0) {
        std::cout << "positive\n";
    } else {
        std::cout << "not positive\n";
    }
    return 0;
}

Note the program compiles, runs, and prints one of the two branches. Which one is not defined.

(Clang has -fsanitize=memory for this, but it requires every library to be rebuilt with it, which is rarely practical.)

The practical answer: ASan for continuous testing, Valgrind when you are stuck — especially when a bug smells like uninitialised data, or when you cannot rebuild the binary.

Other Valgrind tools

Memcheck is the default. Valgrind is a framework:

valgrind --tool=callgrind ./program   # call counts and cost per function
valgrind --tool=cachegrind ./program  # cache hit and miss simulation
valgrind --tool=helgrind ./program    # data races in threaded code
valgrind --tool=massif ./program      # heap usage over time

Callgrind and Cachegrind reappear in Module 22 on performance; Helgrind in Module 21 on concurrency.

Useful flags

valgrind --leak-check=full \
         --show-leak-kinds=all \
         --track-origins=yes \
         --error-exitcode=1 \
         ./program
  • --leak-check=full — a stack for every leak, not just a count.
  • --show-leak-kinds=all — include still reachable, which is hidden by default.
  • --track-origins=yes — for uninitialised values, say where the bad value came from. Slower, and usually worth it.
  • --error-exitcode=1 — exit non-zero on any finding, so CI fails.

That last one is what makes Valgrind usable in an automated build: without it, Valgrind exits 0 even when it reported errors, and the pipeline goes green.

Try it

Write a program with three separate problems: a leak, an invalid read past an array, and a read of an uninitialised variable. Run it under Valgrind and then under ASan, and note which finds which.

Checkpoint

  • Valgrind needs no rebuild — it runs the binary you have.
  • valgrind --leak-check=full ./program is the everyday command.
  • Reports give the operation, the line, and the allocation the memory belongs to.
  • Leaks are sorted: definitely lost is the category to fix first.
  • Valgrind is 10–50× slower than native; ASan is roughly 2×.
  • Valgrind finds uninitialised reads, which ASan does not.
  • Use ASan in CI, Valgrind when stuck or when you cannot rebuild.
  • Add --error-exitcode=1 or an automated build will pass despite findings.

Questions about this lesson

Log in to ask a question.