Debugging Memory with Valgrind
Find leaks, invalid reads, and use-after-free bugs before they find you
Quick check before you start: Have you ever had a segfault and no idea where it came from? If yes, Valgrind is about to become your best friend. If you already use Valgrind, skip to Reading the Output.
Practice this topic: Valgrind skill drill
After this lesson, you will be able to:
- Compile with
-gfor debug symbols - Run Valgrind with
--leak-check=full - Interpret “definitely lost,” “indirectly lost,” and “invalid read” messages
- Map Valgrind errors back to specific source lines
Step 1: Compile with Debug Symbols
Valgrind can only show you file names and line numbers if your binary contains debug info. Add -g to your gcc command:
gcc -Wall -Wextra -g -o program program.c
Without -g, Valgrind still detects errors, but the output says things like at 0x4005F2 instead of at program.c:17. That is useless for debugging.
Step 2: Run Valgrind
valgrind --leak-check=full ./program
Valgrind runs your program inside a virtual CPU. It is about 20x slower than normal execution, but it tracks every byte of memory.
Your program runs normally — you type input, see output. But after your program exits, Valgrind prints a report.
Reading the Output
A clean report looks like this:
==12345== HEAP SUMMARY:
==12345== in use at exit: 0 bytes in 0 blocks
==12345== All heap blocks were freed -- no leaks are possible
==12345== ERROR SUMMARY: 0 errors from 0 contexts
Zero bytes in use at exit. Zero errors. That is your goal for every program.
Leak Categories
When leaks exist, Valgrind classifies them:
| Category | Meaning |
|---|---|
| Definitely lost | You allocated memory and lost all pointers to it. This is a real leak. Fix it. |
| Indirectly lost | Memory reachable only through a “definitely lost” block. Fix the parent leak and this goes away. |
| Possibly lost | Valgrind is not sure. Rare in simple programs. Investigate. |
| Still reachable | You still have a pointer to it at exit, but never freed it. Usually a minor cleanup issue. |
Invalid Reads and Writes
==12345== Invalid read of size 4
==12345== at 0x4005F2: main (program.c:17)
==12345== Address 0x5205048 is 8 bytes inside a block of size 4 free'd
==12345== at 0x4C2ACBD: free (vg_replace_malloc.c:530)
==12345== by 0x4005E8: main (program.c:15)
This tells you: on line 17, you read 4 bytes from memory that was freed on line 15. That is a use-after-free bug.
Mapping Errors to Source Lines
Valgrind gives you a call stack for every error. Read it bottom to top:
- The top frame is where the error happened
- Lower frames show how you got there
- The file name and line number point you directly to the bug
==12345== 40 bytes in 1 blocks are definitely lost
==12345== at 0x4C2BBAF: calloc (vg_replace_malloc.c:711)
==12345== by 0x400597: build_array (program.c:8)
==12345== by 0x4005C1: main (program.c:22)
Translation: main called build_array on line 22. build_array called calloc on line 8. That allocation was never freed.
A Complete Example
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *arr = calloc(10, sizeof(int));
if (arr == NULL) { return 1; }
arr[0] = 42;
printf("%d\n", arr[0]);
free(arr);
arr = NULL;
return 0;
}
Compile and run:
gcc -Wall -Wextra -g -o demo demo.c
valgrind --leak-check=full ./demo
Result: zero leaks, zero errors.
What Comes Next
You have the tools to manage memory correctly. Next week, you will learn to group related data together using structs — C’s answer to Java classes.