5.1.19C Programming

Valgrind — detecting memory errors

1,910 words9 min readdifficulty · medium

WHY does this tool exist?

C gives you raw pointers and no automatic memory management. The CPU happily reads/writes any address; if you malloc(10) and write byte 11, nothing crashes immediately — you corrupt the heap and the program dies "randomly" 5000 lines later. These bugs are:

  • Non-deterministic (depend on heap layout, timing).
  • Silent (no compiler warning, often no crash).

WHAT it tracks (the core trick — shadow memory): For every byte of real memory, Memcheck keeps two bits of info:

  • A-bits (Addressability): is this byte allowed to be accessed right now?
  • V-bits (Validity/defined-ness): does this byte hold a defined value, or garbage from malloc?

HOW Memcheck reasons (derivation from first principles)

We don't have a formula here — we have a decision procedure. Let's build it.

Let address aa and access size nn. For a read or write of nn bytes starting at aa:

Access OK    i[0,n):A[a+i]=accessible\text{Access OK} \iff \forall i \in [0, n) : A[a+i] = \texttt{accessible}

If any byte is not accessible ⇒ Invalid read/write error.

For defined-ness, V-bits propagate through operations. Starting rules:

malloc(n):A[a..a+n)=1,V[a..a+n)=undefined\text{malloc}(n): A[a..a{+}n) = 1,\quad V[a..a{+}n) = \texttt{undefined} free(p):A[block]=0  (also queued, not reused immediately)\text{free}(p): A[\text{block}] = 0 \;(\text{also queued, not reused immediately})

The leak rule (mark-and-sweep at exit): at program end, Memcheck does a GC-style scan. A heap block is:

  • Still reachable: a pointer to it exists somewhere → not a "lost" leak (but still unfreed).
  • Definitely lost: no pointer anywhere points into it → you leaked it.
  • Indirectly lost: only reachable through another lost block.
  • Possibly lost: only an interior pointer points to it.
leaked bytes=blocks b:b unreachable at exitsize(b)\text{leaked bytes} = \sum_{\text{blocks } b \,:\, b\text{ unreachable at exit}} \text{size}(b)
Figure — Valgrind — detecting memory errors

Worked Example 1 — Heap buffer overflow

#include <stdlib.h>
int main(void) {
    int *a = malloc(5 * sizeof(int)); // 20 bytes, A-bits=1, V-bits=undef
    a[5] = 42;                         // writes bytes 20..23 -> A-bits=0!
    free(a);
    return 0;
}

Run: gcc -g -O0 prog.c && valgrind --leak-check=full ./a.out

Valgrind says:

Invalid write of size 4
   at main (prog.c:4)
 Address 0x... is 0 bytes after a block of size 20 alloc'd

Worked Example 2 — Use of uninitialised value

#include <stdio.h>
#include <stdlib.h>
int main(void) {
    int *p = malloc(sizeof(int)); // V-bits = undefined
    if (*p == 0)                  // branching on undefined value!
        printf("zero\n");
    free(p);
    return 0;
}
Conditional jump or move depends on uninitialised value(s)

Worked Example 3 — Memory leak

#include <stdlib.h>
int main(void) {
    char *buf = malloc(100);
    buf = malloc(200);   // first 100-byte block now unreachable
    free(buf);           // frees the 200, leaks the 100
    return 0;
}
LEAK SUMMARY:
   definitely lost: 100 bytes in 1 blocks


Recall Explain to a 12-year-old (hidden)

Imagine you're given a row of 5 lockers to keep your stuff. C lets you stuff things into locker number 6 — which isn't yours — and nobody stops you. Valgrind is a strict guard standing behind you. Every time you open a locker, the guard checks: "Is this one yours?" (addressability) and "Did you actually put something in here, or is it empty junk?" (defined-ness). At the end of the day the guard also checks: "Did you leave any of your bags lying around without telling anyone where they are?" — those are leaks. The program runs slower because the guard checks everything, but you finally catch all your sneaky mistakes.


Flashcards

What virtual machine technique lets Valgrind see every memory access?
It runs the program on a synthetic/virtual CPU (JIT-instrumented), inserting checks around every load/store.
What are A-bits and V-bits in Memcheck?
A-bits = addressability (may this byte be accessed?); V-bits = validity/defined-ness (does it hold a defined value?).
A read of freed memory is which kind of error?
An A-bit (addressability) error → "Invalid read", use-after-free.
Branching on a malloc'd-but-unwritten variable triggers what?
A V-bit error: "Conditional jump or move depends on uninitialised value(s)".
Why does Valgrind keep freed blocks in a quarantine freelist?
So their A-bits stay 0 and aren't immediately reused, allowing use-after-free to be detected.
Difference between "definitely lost" and "still reachable"?
Definitely lost = no pointer points to the block (true leak); still reachable = a pointer still exists at exit (often harmless).
Why compile with -g -O0 before running Valgrind?
-g gives source line/variable names in the report; -O0 stops the optimiser from reordering/eliding the buggy access.
What does --track-origins=yes add?
It reports where an uninitialised value originated, at the cost of slower execution.
Why can a program "run fine" yet have a Valgrind error?
The bug is undefined behaviour; correct output depends on heap layout/luck, not on the code being correct.
What flag actually shows full leak details?
--leak-check=full (plus --show-leak-kinds=all for every category).

Connections

  • malloc and free — the allocations Valgrind shadows
  • Pointers in C — invalid/dangling pointers are what A-bits guard
  • Undefined Behaviour in C — why these bugs are silent
  • Stack vs Heap — Memcheck mainly polices the heap
  • AddressSanitizer (ASan) — compiler-based alternative, faster, different trade-offs
  • Debugging with GDB — pair with Valgrind for root-cause analysis

Concept Map

causes

motivates

default tool

uses

tracks

tracks

detects

detects

quarantines freed blocks

enables catching

scan at exit

classifies

C raw pointers, no auto memory mgmt

Silent non-deterministic memory bugs

Valgrind instrumentation framework

Memcheck

Shadow memory metadata

A-bits Addressability

V-bits Validity

Invalid read/write errors

Use of uninitialised values

Freelist quarantine

Use-after-free

Mark-and-sweep leak check

Definitely / indirectly / possibly lost

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Dekho, C mein aap pointers se kuch bhi memory address access kar sakte ho, aur CPU bina sawaal kiye allow kar deta hai. Agar aapne malloc(20 bytes) kiya aur galti se byte 21 par likh diya, toh program abhi crash nahi karega — par heap corrupt ho jayega aur 5000 line baad "randomly" crash karega. Yahi C ke sabse tricky bugs hain: silent aur non-deterministic. Valgrind in bugs ka guard hai.

Valgrind aapke program ko ek virtual CPU par chalata hai aur har memory access ko watch karta hai. Iska main trick hai shadow memory — har byte ke liye do cheezein yaad rakhta hai: A-bits (kya yeh byte access karne ki permission hai?) aur V-bits (jo value padi hai woh defined hai ya malloc ka garbage?). Freed memory padhna A-bit error hai (yeh ab tumhari nahi). Uninitialised variable par if lagana V-bit error hai (touch kar sakte ho, par value kachra hai).

Practically: hamesha gcc -g -O0 se compile karo (taaki Valgrind exact line aur variable naam bata sake), phir valgrind --leak-check=full --track-origins=yes ./a.out chalao. Report mein "Invalid write/read", "uninitialised value", aur leak summary dikhega. Yaad rakhna: "program toh sahi chal raha tha" iska matlab bug nahi hai — woh undefined behaviour hai jo aaj heap luck se chal gaya. Aur leaks mein sabse important hai "definitely lost" — usko pehle fix karo, "still reachable" aksar harmless hota hai. Mnemonic: A-V-L — Address allowed? Value real? Left behind kuch?

Go deeper — visual, from zero

Test yourself — C Programming

Connections