Your program runs and produces correct output. Then you ship it and it crashes in production after 3 hours. The cause: a memory leak that slowly exhausts RAM, or a use-after-free that corrupts the heap. Valgrind would have caught this in development. Here is how to use it.
Running Valgrind
gcc -g -O0 -o program program.c
valgrind --leak-check=full --show-leak-kinds=all ./programAlways compile with -g -O0 before running Valgrind. Without debug symbols, error messages show hex addresses instead of file and line numbers. Without -O0, inlined functions and register-allocated variables confuse the stack trace.
Memory Leak
#include <stdlib.h>
void leaky_function() {
int *data = malloc(100 * sizeof(int));
/* forgot to free(data) */
}
int main() {
leaky_function();
return 0;
}==12345== HEAP SUMMARY:
==12345== in use at exit: 400 bytes in 1 blocks
==12345== total heap usage: 1 allocs, 0 frees, 400 bytes allocated
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 400 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== 400 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x...: malloc (vg_replace_malloc.c:...)
==12345== at 0x...: leaky_function (leak.c:4)
==12345== at 0x...: main (leak.c:9)Valgrind identifies the allocation site, not just the total leaked bytes. “Definitely lost” means there is no remaining pointer to the allocation — it is truly unrecoverable. “Still reachable” means the pointer exists but was never freed — less urgent but still a bug.
Invalid Read — Use After Free
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p = malloc(sizeof(int));
*p = 42;
free(p);
printf("%dn", *p); /* use-after-free */
return 0;
}==12346== Invalid read of size 4
==12346== at 0x...: main (uaf.c:7)
==12346== Address 0x... is 0 bytes inside a block of size 4 free'd
==12346== at 0x...: free (vg_replace_malloc.c:...)
==12346== at 0x...: main (uaf.c:6)Valgrind pinpoints the read and the earlier free. This is the information you need to find the bug — the line of the read, and the call stack of the free that made it invalid.
Invalid Write — Buffer Overflow
#include <stdlib.h>
int main() {
int *arr = malloc(5 * sizeof(int));
arr[5] = 999; /* one past the end */
free(arr);
return 0;
}==12347== Invalid write of size 4
==12347== at 0x...: main (overflow.c:5)
==12347== Address 0x... is 0 bytes after a block of size 20 alloc'dUninitialized Memory
#include <stdio.h>
#include <stdlib.h>
int main() {
int *arr = malloc(10 * sizeof(int));
/* forgot to initialize */
printf("%dn", arr[3]); /* reading uninitialized heap memory */
free(arr);
return 0;
}==12348== Use of uninitialised value of size 4
==12348== at 0x...: main (uninit.c:6)Valgrind tracks which bytes have been initialized. Reading an uninitialized byte — even if it has a plausible-looking value at runtime — is flagged. This catches bugs that only manifest with certain heap layouts or compilers.
Valgrind Flags Reference
| Flag | What it does |
|---|---|
--leak-check=full | Show details for every leaked block |
--show-leak-kinds=all | Include “still reachable” leaks |
--track-origins=yes | Show where uninitialized values came from (slower) |
--error-exitcode=1 | Return non-zero exit code if errors found — useful in CI |
--gen-suppressions=all | Generate suppression patterns for known false positives |
Valgrind vs AddressSanitizer
Both tools catch memory bugs, but they work differently:
| Valgrind | AddressSanitizer (ASan) | |
|---|---|---|
| Slowdown | 10-30× | 2× |
| Requires recompile | No | Yes (-fsanitize=address) |
| Stack overflow detection | Limited | Yes |
| Uninit memory | Yes (Memcheck) | Partial (MSan) |
| Works on existing binary | Yes | No |
Use ASan for your regular development cycle — it is fast enough to leave enabled. Reserve Valgrind for uninit memory bugs (use --track-origins=yes) and when you cannot recompile. Our guide on malloc, calloc, and realloc covers the allocation patterns that Valgrind most often flags. Use the gcc compiler online to test sanitizer-compatible compilation flags before setting up a local Valgrind run.
TL;DR
- Compile with
-g -O0for readable Valgrind output valgrind --leak-check=full --show-leak-kinds=all ./programis the standard invocation- “Definitely lost” = truly leaked; “Still reachable” = pointer exists but never freed
- Use-after-free and invalid reads show the read site AND the free site — both are needed to fix the bug
- Add
--track-origins=yesfor uninit memory — it slows Valgrind further but names the allocation - Use
--error-exitcode=1in CI to fail builds with memory errors - For fast daily use, prefer AddressSanitizer; use Valgrind for thorough checks and uninit detection