pointers-memory Lesson 9 20 min read

How Do I Debug Memory Errors with Valgrind?

Memory leaks, invalid reads, use-after-free — Valgrind finds bugs you can't see

Reading: Valgrind documentation (valgrind.org); C Text: Ch. 13 §2 (dynamic memory errors)

After this lesson, you will be able to:

  • Compile with -g and run under valgrind --leak-check=full to detect memory errors
  • Interpret Valgrind output for leaks, invalid reads/writes, use-after-free, and double-free
  • Map a Valgrind error to the specific source code line
  • Identify clean Valgrind output (0 errors, all blocks freed)

The Bugs You Can’t See

Your program compiles. It runs. It produces the right output. Everything looks fine.

But it’s leaking memory. Every time the user adds a record, your program consumes more RAM that it never gives back. After running for an hour, it’s using 2 GB. After a day, it crashes.

Memory bugs are invisible at the surface. The program “works” — until it doesn’t. That’s where Valgrind comes in. It watches every memory access your program makes and reports anything suspicious.


Using Valgrind

Basic Usage

First, compile with debug symbols (-g):

gcc -Wall -g -o myprogram myprogram.c

Then run through Valgrind:

valgrind --leak-check=full ./myprogram

Valgrind runs your program normally but instruments every memory operation. It’s slower (10-20x), but it catches bugs that no other tool can.

Reading Valgrind Output

Clean output (no errors):

==12345== HEAP SUMMARY:
==12345==     in use at exit: 0 bytes in 0 blocks
==12345==   total heap usage: 3 allocs, 3 frees, 1,200 bytes allocated
==12345==
==12345== All heap blocks were freed -- no leaks are possible
==12345==
==12345== ERROR SUMMARY: 0 errors from 0 contexts

This is what you want: 0 bytes in use at exit, all blocks freed, 0 errors.

Memory Leak

int *arr = calloc(100, sizeof(int));
// ... forgot to free(arr) ...

Valgrind reports:

==12345== LEAK SUMMARY:
==12345==    definitely lost: 400 bytes in 1 blocks

Fix: Add free(arr) before the program exits.

Invalid Read

int *arr = calloc(5, sizeof(int));
printf("%d\n", arr[10]);     // Reading past the end
free(arr);
==12345== Invalid read of size 4
==12345==    at 0x40069C: main (buggy.c:5)
==12345==  Address 0x5205068 is 20 bytes after a block of size 20 alloc'd

Valgrind tells you exactly which line caused the problem and how far past the allocation you read.

Check Your Understanding
Valgrind reports: Invalid read of size 4 ... Address is 20 bytes after a block of size 20 alloc'd. What happened?
A You freed the memory and then tried to read it
B You forgot to call free() — memory leak
C You passed an uninitialized pointer to a function
D You read one element past the end of a 5-int array (off-by-one)
Answer: D. "20 bytes after a block of size 20" means you allocated 20 bytes (5 ints * 4 bytes) and read at the exact byte where the block ends — that's index 5 of a 5-element array. This is a classic off-by-one error, like using <= instead of < in a loop. Option A would say "inside a block ... free'd." Option B would appear in the LEAK SUMMARY, not as an invalid read.

Use After Free

int *arr = calloc(5, sizeof(int));
free(arr);
arr[0] = 42;                 // Writing to freed memory!
==12345== Invalid write of size 4
==12345==    at 0x40069C: main (buggy.c:4)
==12345==  Address 0x5205040 is 0 bytes inside a block of size 20 free'd

Fix: Set arr = NULL after free(arr) — then this would be a clean segfault instead of silent corruption.

Uninitialized Value

int *arr = malloc(5 * sizeof(int));     // Not calloc — uninitialized!
if (arr[0] > 10) { ... }               // Reading garbage
==12345== Conditional jump or move depends on uninitialised value(s)
==12345==    at 0x40069C: main (buggy.c:3)

Fix: Use calloc instead of malloc, or initialize values before reading them.

Double Free

int *arr = calloc(5, sizeof(int));
free(arr);
free(arr);              // Freeing the same block twice!
==12345== Invalid free() / delete / delete[] / realloc()
==12345==    at 0x4C2EDEB: free (vg_replace_malloc.c:530)
==12345==    by 0x40069C: main (buggy.c:4)
==12345==  Address 0x5205040 is 0 bytes inside a block of size 20 free'd

Fix: Set arr = NULL after free(arr). Calling free(NULL) is safe and does nothing — it’s a no-op by the C standard.

Deep dive: Valgrind key phrases cheat sheet

When reading Valgrind output, look for these phrases:

Valgrind says… What it means Common cause
Invalid read of size N Reading memory you don’t own Array out of bounds, use-after-free
Invalid write of size N Writing memory you don’t own Buffer overflow, use-after-free
Conditional jump depends on uninitialised value Using uninitialized memory in an if or loop malloc without initializing, reading before writing
Invalid free() Freeing memory that was already freed or never allocated Double free, freeing stack memory
definitely lost: N bytes Memory leaked — no pointer to it exists at exit Forgot free(), overwrote pointer before freeing
indirectly lost: N bytes Memory reachable only through a leaked pointer Leaked a struct that contains pointers to other allocations
still reachable: N bytes Memory still pointed to at exit but not freed Usually not a bug — but clean it up anyway
All heap blocks were freed No leaks at all This is the goal

The number after == (like ==12345==) is just the process ID — ignore it. Focus on the error description and the source file/line number (only available if you compiled with -g).

The Valgrind Checklist

Before submitting any lab with dynamic memory:

gcc -Wall -g -o program program.c
valgrind --leak-check=full ./program

Look for:

  • All heap blocks were freed -- no leaks are possible
  • ERROR SUMMARY: 0 errors from 0 contexts

Key Insight: Valgrind is your best friend for memory bugs. Java would never let you make these mistakes (garbage collection, bounds checking, no pointer arithmetic). C lets you make all of them — and Valgrind catches them after the fact. Run it on every program that uses calloc, malloc, or free.

Check Your Understanding
Your program produces correct output but Valgrind shows definitely lost: 800 bytes in 2 blocks. What should you do?
A Nothing — correct output means the program is fine
B Find the 2 allocations that are missing free() calls and add them
C Add NULL checks after every calloc
D Switch from malloc to calloc to fix the leak
Answer: B. "Definitely lost" means you allocated memory and lost all pointers to it without freeing. You have 2 allocations totaling 800 bytes that need free() calls. Option A is wrong — correct output doesn't mean correct memory management. In a long-running program, these leaks accumulate. Option D is wrong — calloc vs malloc has nothing to do with freeing.
Why does this matter?

In industry, memory leaks kill servers. A web server leaking 800 bytes per request will consume gigabytes in hours. Valgrind is the standard tool for catching these bugs before deployment. Getting clean Valgrind output is a grading requirement for labs with dynamic memory — and a professional skill that transfers directly to C/C++ jobs.

From Java: Java throws ArrayIndexOutOfBoundsException, NullPointerException, and handles memory automatically. C gives you none of that. Valgrind is the closest thing C has to Java’s runtime safety checks — but you have to run it yourself, and it only catches bugs that actually execute during your test.

Quick Check: What does "definitely lost: 400 bytes in 1 blocks" mean?

You allocated 400 bytes (probably calloc(100, sizeof(int))) and never freed them. The memory is leaked — no pointer to it exists at program exit, so it can never be freed. Add a free() call.

Quick Check: Why compile with -g for Valgrind?

The -g flag includes debug information (source file names and line numbers) in the executable. Without it, Valgrind can still detect errors but can’t tell you which line of source code caused them — you’d only see memory addresses.

Quick Check: Can Valgrind catch a bug that doesn't happen during your test run?

No. Valgrind only catches errors that actually occur during the specific run you test. If your test doesn’t trigger a particular code path (e.g., the realloc branch), Valgrind can’t check it. That’s why thorough test cases matter.


Trust but Verify

Valgrind doesn’t prevent bugs — it detects them. The real fix is writing correct code: matching every calloc with free, checking every allocation for NULL, and never accessing out-of-bounds memory. But Valgrind catches the mistakes you miss.

Next: putting it all together with a complete program that uses dynamic memory, menus, and input validation. This is the capstone for Series 3.

Big Picture: Valgrind is the safety net that makes manual memory management survivable. Without it, C programmers would be debugging invisible memory corruption by staring at hex dumps. With it, you get precise error reports that point to the exact line of code. Every lab with dynamic memory from here on requires clean Valgrind output — treat it as part of the compilation step.