Code of the Day
IntermediateMemory management

Valgrind basics

Use valgrind memcheck to detect memory errors — invalid reads/writes, use-after-free, memory leaks, and uninitialised value use.

CIntermediate9 min read
Recommended first
By the end of this lesson you will be able to:
  • Install and run valgrind on a C program
  • Interpret valgrind error messages to locate the bug
  • Use valgrind --leak-check=full to find memory leaks
  • Explain the difference between valgrind and AddressSanitizer in terms of trade-offs

Writing correct memory management is hard. Even experienced C programmers make mistakes. Valgrind's memcheck tool runs your program under a virtual machine that intercepts every memory access and reports errors as they happen. It is one of the most valuable tools in a C developer's toolkit.

Installing and running

# Debian/Ubuntu
sudo apt install valgrind

# Fedora
sudo dnf install valgrind

# macOS (limited support -- use ASan instead)
brew install valgrind

Compile with debug information (so valgrind can report file names and line numbers):

gcc -g -O0 program.c -o program
valgrind ./program

The -O0 flag disables optimisations, which can cause valgrind to report less accurate line numbers.

Reading valgrind output

Invalid read/write

/* buggy.c */
#include <stdlib.h>

int main(void) {
    int *arr = malloc(5 * sizeof(int));
    arr[5] = 42; /* one past the end */
    free(arr);
    return 0;
}

Valgrind output:

==12345== Invalid write of size 4
==12345==    at 0x109161: main (buggy.c:5)
==12345==  Address 0x5204054 is 0 bytes after a block of size 20 alloc'd
==12345==    at 0x4C2FB0F: malloc (in /usr/lib/valgrind/...)
==12345==    at 0x109148: main (buggy.c:4)

The output shows:

  • The type of error: invalid write (accessing memory not allocated to you).
  • Where it happened: main at buggy.c:5.
  • What memory was involved: 0 bytes after a 20-byte block allocated at line 4.

Use-after-free

int *p = malloc(sizeof(int));
free(p);
*p = 42; /* use after free */
==12345== Invalid write of size 4
==12345==  Address 0x5204040 is 0 bytes inside a block of size 4 free'd
==12345==    at 0x4C30D3B: free (in /usr/lib/valgrind/...)
==12345==    at 0x109162: main (buggy.c:3)
==12345==  Block was alloc'd at
==12345==    at 0x4C2FB0F: malloc ...

Valgrind shows both where the memory was freed and where it was originally allocated.

Uninitialised value use

int x;
if (x > 0) { /* reading uninitialised x */
    printf("positive\n");
}
==12345== Conditional jump or move depends on uninitialised value(s)
==12345==    at 0x10914A: main (buggy.c:3)

Memory leak summary

Run with --leak-check=full to see where each leaked block was allocated:

valgrind --leak-check=full --show-leak-kinds=all ./program
==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4C2FB0F: malloc
==12345==    at 0x109148: main (buggy.c:4)

"Definitely lost" = unreachable at program exit. "Still reachable" = there is a valid pointer, but it was never freed. Fix both.

Valgrind vs. AddressSanitizer

FeatureValgrindAddressSanitizer
No recompilation neededYesNo (add -fsanitize=address)
Slowdown10–50×
Detects heap errorsYesYes
Detects stack errorsLimitedYes
Detects leaksYesYes (with -fsanitize=leak)
Detects uninitialisedYesWith MSan only
Works on all binariesYesInstrumented only

Use both: ASan during development (fast, catches stack bugs), valgrind for final memory audit and for third-party binaries you cannot recompile.

Run your test suite under valgrind in CI. Any CI pipeline for a C project should include a valgrind run. Add it as a Makefile target: make memcheck. A clean valgrind run should be a release requirement.

A workflow for debugging memory errors

  1. Reproduce the crash or unexpected behaviour.
  2. Compile with gcc -g -O0 -fsanitize=address.
  3. Run — ASan will likely report the error with a stack trace.
  4. Fix the error.
  5. Run under valgrind to catch any remaining leaks.
  6. Add the test case to your test suite to prevent regression.

Where to go next

Next: the dynamic array lab — you will implement a growable array (vec-style container) that doubles its capacity with realloc, practising everything from this module.

Finished reading? Mark it complete to track your progress.

On this page