5.1.19 · D1C Programming

Foundations — Valgrind — detecting memory errors

1,610 words7 min readBack to topic

Before you can read the parent note Valgrind — detecting memory errors, you need to earn every word it throws at you. Below, each idea is built from nothing: plain words → a picture → why the topic needs it. Read top to bottom; each rung stands on the one below.


1. A byte and an address — the numbered row of boxes

Picture a long row of lockers. Each locker is a byte. The little number painted on each locker door is its address. When C "accesses memory," it walks up to a locker (address), and either reads what's inside or writes a new value into it.

Figure — Valgrind — detecting memory errors

2. A pointer — a box that stores a box-number

The notation:

  • int *p — read as "p is a pointer to an int." The * in the declaration means "this thing points at something."
  • *p — read as "the value at the locker p points to." Here * means "go there and look inside." This is called dereferencing.
  • p (alone) — the locker number itself.
Figure — Valgrind — detecting memory errors

Why the topic needs it: the parent note writes *p == 0 and a[5] = 42. Both are pointer operations. a[5] is secretly *(a + 5) — "go 5 slots past where a points, then look inside." More in Pointers in C.


3. Stack vs Heap — two neighbourhoods of memory

Picture two neighbourhoods:

  • Stack — a stack of trays. Each function call adds a tray on top; when it returns, its tray is thrown away automatically. You never call free here.
  • Heap — a warehouse where you ask for storage space ("give me 20 bytes") and you are responsible for handing it back.

Valgrind's whole job is watching the heap, because the heap is where you hold the responsibility — and therefore where you make mistakes. See Stack vs Heap.


4. malloc and free — renting and returning heap boxes

Two crucial facts the parent leans on heavily:

  1. A fresh malloc block contains garbage — leftover junk from whoever used those lockers before. C does not clear it.
  2. After free(p), the pointer p still holds the old address, but you are no longer allowed to use it. Touching it = use-after-free.

More detail: malloc and free.


5. sizeof and array indexing — turning counts into byte offsets

sizeof(int) is usually 4 (four bytes per integer). So in the parent's overflow example, malloc(5 * sizeof(int)) rents bytes: lockers through (relative to the block's start).

Then a[5] means byte — the first locker past the block. That is exactly why Valgrind says "0 bytes after a block of size 20": your write starts precisely where the rented block ends.

Figure — Valgrind — detecting memory errors
Recall Quick check: which byte does

a[3] write for an int array? Bytes 12, 13, 14, 15 — that is, up to . Inside a 20-byte block, this is safe.


6. The two shadow labels — A-bits and V-bits

Now the payoff. Valgrind keeps, for every real byte, two hidden tags:

Two different questions:

Situation A-bit V-bit Valgrind error
Read a freed box 0 (not yours) Invalid read (use-after-free)
Branch on fresh malloc value 1 (yours) undefined "Conditional jump depends on uninitialised value"
Write one past the array 0 (past the block) Invalid write

The parent formalises the A-bit rule as: an access of n bytes at address is OK only if every byte has its A-bit set to accessible. If even one is 0, it's an error.


7. Undefined Behaviour — why "it worked" proves nothing

The picture: UB is a coin that lands heads (works) most days purely by luck of the heap layout. Change the compiler, the input, or the moon phase, and it lands tails (crash / corruption). Valgrind exists precisely to catch UB before it flips to tails on a customer. See Undefined Behaviour in C.


Prerequisite map

Byte and Address

Pointer

Stack vs Heap

malloc and free

sizeof and array index

A-bits and V-bits

Undefined Behaviour

Valgrind Memcheck

Each foundation on the left feeds the shadow-memory idea on the right. Valgrind is only meaningful once addresses, pointers, the heap, and UB all sit in place. Related tooling worth a glance later: AddressSanitizer (ASan) and Debugging with GDB.


Equipment checklist

An address is what, physically?
A box (byte) number — the label on a locker door telling you which byte you mean.
What does int *p declare vs what does *p do?
int *p declares p as a pointer to an int; *p dereferences it — goes to the box p points at and reads/writes its contents.
Which memory region does Valgrind mainly watch, and why?
The heap — because you manually manage it with malloc/free, so that's where your mistakes live.
What is inside a fresh malloc block?
Garbage — whatever leftover bytes were there before; malloc does not clear it.
How many bytes does malloc(5 * sizeof(int)) rent on a typical machine?
20 bytes, since sizeof(int) is 4 and 5 × 4 = 20.
a[5] for an int array touches which byte offset?
Byte 20 (5 × 4) — exactly one slot past a 20-byte block, hence "0 bytes after a block of size 20".
What does the A-bit answer?
"Am I allowed to touch this byte right now?" — 1 = accessible, 0 = off-limits.
What does the V-bit answer?
"Does this byte hold a defined (real) value or undefined garbage?"
Reading freed memory is an A-bit or V-bit error?
A-bit error (not yours to touch) — use-after-free / invalid read.
Why does "the program ran fine" not prove there is no bug?
Because the bug may be undefined behaviour, which can appear to work by luck and break with any change.