Address Spaces

Code / heap / stack layout, why each process gets its own view, and the three goals (transparency, efficiency, protection).

Concept Foundational
6 min read
address-space virtual-memory process layout isolation

Summary#

An address space is the OS’s per-process abstraction of memory: a flat, byte-addressable range that the process believes it owns end to end, even though physically it shares DRAM with hundreds of other processes and the kernel. The classic layout has code (the program’s instructions) at the bottom, heap growing upward, stack growing downward from the top, and a gap of unmapped virtual addresses between them so each side can grow without colliding with the other.

The point of the abstraction is to give every process the same simple mental model — “here are my bytes, addressed 0 to 2^N” — while the kernel underneath maps fragments of that virtual range onto wherever there’s room in physical DRAM. Three goals justify the cost: transparency (the process should not know it’s being virtualized), efficiency (translation must not destroy performance), and protection (one process must not be able to read or write another’s memory).

Why it matters#

Without address-space virtualization, programs would have to be linked against the actual physical addresses they will occupy at runtime, two programs could never run at the same time without one rewriting the other’s instructions, and a single buffer-overrun bug would corrupt the kernel. Early single-tasking systems lived in that world — CP/M, MS-DOS — and the brittleness is exactly why every multi-process OS since the late 1960s has provided a virtual address space per process.

The abstraction also enables features that look unrelated at first glance: shared libraries (the same physical page mapped read-only into many processes), copy-on-write fork (a child shares all the parent’s pages until first write), memory-mapped files (a file’s bytes mapped into the address space so reads become loads), and swap (a page can live on disk and still have a valid virtual address). Each of these is a clever exploitation of the same indirection.

How it works#

The canonical layout#

high address ┌──────────────────────┐ 0x7fff_ffff_ffff (user-space top)
│ stack │ ← grows down
│ ─────────────────── │
│ │
│ (unmapped) │ ← big gap for growth
│ │
│ ─────────────────── │
│ mmap / libs │ ← shared objects, anon mmaps
│ ─────────────────── │
│ heap │ ← grows up via brk / mmap
│ ─────────────────── │
│ bss (zero) │
│ data (init) │
│ code (text) │
low address └──────────────────────┘ 0x00000000_00000000

The exact numbers vary — code typically starts at 0x400000 on x86_64 Linux (or randomised by ASLR), and the stack base sits just below the canonical-user limit. Above the user-space top is the kernel half, which is the same in every process and mapped only when the CPU is in supervisor mode.

Why a gap between heap and stack#

Both heap and stack are dynamic — neither knows in advance how big it needs to be. Putting them at opposite ends of the address space with the gap in between means each grows toward the other without either having to commit to a fixed size. The kernel only allocates physical pages when a virtual page is touched, so a “16 TB virtual gap” costs nothing in DRAM.

Code, data, bss — three sections, one binary#

The compiler produces three flavours of static content:

  • .text — the instructions. Read-only, executable.
  • .data — initialised globals (int counter = 7;). Read-write, written to disk in the executable.
  • .bss — zero-initialised globals (int counter;). Read-write, but the executable only records the size — the loader zeros it at startup, so the binary stays small even for multi-megabyte zero arrays.

Per-process means per-page-table#

Every process has its own page table; switching processes means updating the page-table base register (CR3 on x86) so the MMU starts translating using the new process’s mappings. Hardware support is what makes this cheap enough to do thousands of times per second. The deeper mechanism is covered in Paging Fundamentals.

Variants and trade-offs#

One address space per process (Linux, BSD, Windows, macOS) — strong isolation, well-understood crash semantics, hardware protection for free. The cost is the TLB flush on every context switch and the per-process page-table memory.
One shared address space (single-address-space OSes, unikernels, microcontrollers) — no TLB flush, no per-process page tables, cheap IPC. The cost is no inter-process isolation; one bad pointer crashes everything. Acceptable for embedded and single-purpose appliances.

Other recurring decisions:

  • Address-space size — 32-bit gives 4 GiB, which 2010-era servers already outgrew. 64-bit hardware advertises 64 bits but currently implements only 48 (or 57 with 5-level paging on recent x86), keeping page tables small. Canonical addresses (high half kernel, low half user) come from this.
  • ASLR — randomise the base of stack, heap, libs, and (with PIE) code so a leaked address can’t be reused across runs. Cost: tiny startup overhead, occasional debugging friction.
  • Guard pages — unmapped pages between stack and the next region, between heap and the next region, sometimes between thread stacks. A pointer that strays into one of these faults instead of silently corrupting an adjacent region.
Why doesn't every region just start at address 0?

Two reasons. First, NULL is conventionally a sentinel for “no pointer” — leaving the lowest page unmapped turns null-pointer dereferences into immediate page faults instead of corrupting whatever happens to live at address 0. Second, the canonical layout (.text low, stack high) is partly historical and partly tuned for stack/heap growth in opposite directions; reorganising it would break decades of debuggers, profilers, and tools that grew up assuming the layout.

When this is asked in interviews#

Foundational and mid-level interviews open here all the time: “Draw the layout of a process’s memory” or “What’s a virtual address?” The strong answer covers the four sections (text / data / bss / heap / stack), the gap, the per-process page table, and the three goals (transparency / efficiency / protection). The weak answer lists “stack and heap” without explaining the gap or who owns the mapping.

Follow-ups by seniority:

  • “What’s the difference between a virtual and a physical address?” — foundational.
  • “Why is the stack at the top and not the bottom?” — mid-level; tests understanding of growth direction and history.
  • “What happens when the heap and the stack meet?” — mid-level; touches brk failure or stack-overflow signals.
  • “How does ASLR interact with the loader and the linker?” — senior; PIE binaries, GOT/PLT, relocation.

In systems infrastructure interviews (kernel, hypervisor, language runtime), expect the question to pivot quickly into paging, segmentation, or copy-on-write — address spaces are the foundation everything else assumes.

Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.