Systems programming · lesson 02

Virtual memory

Two processes can both have a pointer to address <code>0x7fff1234</code> and be pointing at completely different bytes. Virtual memory is the illusion the OS maintains so every process believes it owns the entire address space.

in progress
12 min

The problem virtual memory solves

On an early computer with no virtual memory, all programs shared physical RAM directly. If process A used address 1000, and process B also used address 1000, they'd stomp on each other's data. A buggy program could overwrite the OS itself. Implementing isolation required every programmer to know exactly where every other program was loaded — fragile, slow, and impossible to scale.

Virtual memory solves this by giving each process its own private address space. The addresses your program uses — the ones you print with %p — are virtual addresses. The CPU hardware translates each one to a physical address before accessing RAM. Two processes can use the same virtual address and get different physical memory.

Pages: the unit of virtual memory

Virtual memory is managed in fixed-size chunks called pages, typically 4 KB on x86-64. The OS maps virtual pages to physical pages (called frames). The mapping is stored in a per-process data structure called the page table, maintained in kernel memory.

The hardware component that performs the translation — walking the page table on every memory access — is the Memory Management Unit (MMU), built into the CPU. To avoid doing a full page table walk for every instruction, the CPU caches recent translations in the Translation Lookaside Buffer (TLB).

💡
A virtual address has two parts: page number and offset. On a 4 KB page, the bottom 12 bits of the address are the byte offset within the page (2¹² = 4096). The upper bits are the virtual page number, which the MMU looks up in the page table to find the physical frame number. The physical address is frame × 4096 + offset.

Page faults

When you access a virtual address that has no mapping in the page table, the MMU raises a page fault — a hardware exception that transfers control to the OS kernel's fault handler.

Not all page faults are errors. The OS uses them deliberately:

  • Demand paging — when a program starts, the OS maps its pages without loading them all into RAM. The first access to each page triggers a fault, and the OS loads that page on demand.
  • Stack growth — the stack starts small. Accessing a valid but unmapped stack address triggers a fault; the OS grows the stack by mapping a new page.
  • Copy-on-write — after fork(), parent and child share physical pages marked read-only. The first write to a shared page triggers a fault; the OS copies the page and gives each process its own copy.
  • Swapping — if RAM is full, the OS can evict physical pages to disk. Accessing a swapped-out page faults; the OS reloads it from swap.
🚫
A segfault is a page fault the OS decides not to handle. If your process accesses an address that has no valid mapping and isn't covered by any OS recovery case — a null pointer dereference, a write to read-only text, an address completely outside the mapped regions — the kernel sends SIGSEGV to your process. The default action is to terminate and dump core.

Why virtual memory enables process isolation

Each process has its own page table. Physical RAM is shared between all processes, but because each process's page table maps its virtual addresses to different physical frames, no process can read or modify another process's memory — unless the OS explicitly sets up a shared mapping (as with mmap and shared memory).

This is the foundation of security on modern operating systems. A buffer overflow in one process cannot corrupt another process's memory, because they operate in completely separate virtual address spaces.

Address space layout randomization (ASLR)

ASLR randomizes the base addresses of the stack, heap, and mapped libraries each time a program runs. Even though a process always sees the same structure (stack high, heap low, text at some base), the actual virtual addresses change between runs.

This defeats a class of exploits that rely on knowing where specific code or data lives in memory. An attacker who finds a buffer overflow cannot reliably guess where to redirect execution if the addresses are random. You may notice this when debugging: the addresses in %p output change between runs.

c
#include <stdio.h> #include <stdlib.h> int global = 1; int main(void) { int local = 2; int *heap = malloc(sizeof(int)); // run twice: these addresses differ each time (ASLR) printf("text: %p\n", (void *)main); printf("global: %p\n", (void *)&global); printf("stack: %p\n", (void *)&local); printf("heap: %p\n", (void *)heap); free(heap); }
one-line takeaway

Virtual addresses are translated to physical addresses by the MMU on every access — each process has its own page table, so the same virtual address in two processes refers to different physical memory.