Valgrind basics
Use valgrind memcheck to detect memory errors — invalid reads/writes, use-after-free, memory leaks, and uninitialised value use.
- 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 valgrindCompile with debug information (so valgrind can report file names and line numbers):
gcc -g -O0 program.c -o program
valgrind ./programThe -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:
mainatbuggy.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
| Feature | Valgrind | AddressSanitizer |
|---|---|---|
| No recompilation needed | Yes | No (add -fsanitize=address) |
| Slowdown | 10–50× | 2× |
| Detects heap errors | Yes | Yes |
| Detects stack errors | Limited | Yes |
| Detects leaks | Yes | Yes (with -fsanitize=leak) |
| Detects uninitialised | Yes | With MSan only |
| Works on all binaries | Yes | Instrumented 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
- Reproduce the crash or unexpected behaviour.
- Compile with
gcc -g -O0 -fsanitize=address. - Run — ASan will likely report the error with a stack trace.
- Fix the error.
- Run under valgrind to catch any remaining leaks.
- 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.
Buffer overflows
Understand buffer overflows — how they happen, why they are security vulnerabilities, and the defensive practices that prevent them.
Lab: Dynamic array (growable buffer)
Implement a growable dynamic array in C using malloc and realloc — the foundation of every dynamic collection in systems programming.