Back to the map

Module 06 · Memory management · Lesson 8 of 8

Address binding and the MMU

When a program's addresses become physical: compile-, load- and run-time binding, base and limit registers, segmentation versus paging, static and dynamic linking, PIE and ASLR, and what the MMU does on every access.

CoreShould-knowMid 12 min Updated Nov 2026

Why it matters

Every binary you ship was bound to addresses three times: the compiler chose offsets, the linker laid out sections, and the loader placed the result wherever the kernel's randomisation put it. That is why a segfault address like 0x7f3a1c04e2b0 means nothing until you subtract the load base, why a static Go binary needs no libc in its container, and why a 2005 stack-smashing exploit fails against a PIE binary under ASLR.

The idea

A logical address is what the CPU emits (the virtual address of the Virtual memory lesson); a physical address is what the memory bus sees. When the mapping between them is fixed is the address binding time.

Three binding times

  1. 1

    Compile time: absolute

    The compiler emits physical addresses for a known location; move the program and you recompile. Firmware works this way.
  2. 2

    Load time: relocatable

    Code is emitted as if at zero, plus relocation entries naming every place that holds an address; the loader adds the real base to each, and the program cannot move until it exits.
  3. 3

    Run time: the MMU

    Hardware translates every address, so the program never learns where it physically lives and the kernel can move it, swap pages back into different frames or share frames. Every general-purpose system binds this way.

The simplest MMU: base and limit

The base register holds where the process begins in physical memory, the limit register its length. On each access the hardware checks the logical address against the limit and adds the base; past the limit it traps to the kernel, which kills the process. Loading them is privileged, done on every context switch: protection and relocation in one compare and one add.

CPUlogical 0x1A3Fbase + limitwithin limit? add basepage tablepage 1 → frame 0x7C, keep 0xA3FRAM
Base and limit checks a bound and adds an offset; a page table swaps the page number for a frame number and keeps the offset.

Contiguous allocation and why it fails

Base and limit need each process in one contiguous block, placed into a free hole by first-fit, best-fit or worst-fit. As processes come and go the holes shrink and scatter: the external fragmentation of the Allocation and fragmentation lesson at the scale of whole processes, enough memory free in total but no hole big enough. The cure is compaction, copying processes together, possible only because binding is at run time, and slow.

Segmentation versus paging

Segmentation generalises base and limit: code, data and stack each become a segment with its own base, limit and protection bits in a segment table. A logical address is segment number plus offset; the hardware checks the offset against the limit, the access against the segment's rights (code read-only and executable, stack writable, not executable) and adds the base. Protection and sharing per segment come naturally, but segments are still contiguous, so external fragmentation stays. Paging fixes the unit instead: equal-size pages anywhere in physical memory, protection per page; the Paging and page tables lesson covers the rest.

PropertyBase and limitSegmentationPaging
UnitOne block per processVariable-size segmentsFixed 4 KB pages
Address formA single numberSegment number plus offsetPage number plus offset
ProtectionWhole processPer segmentPer page
FragmentationExternalExternalInternal, bounded

x86 carried both, segmentation first and paging on the result; in 64-bit mode the CS, DS, SS and ES bases are forced to zero and limits ignored, and only FS and GS keep a base, used for thread-local storage and per-CPU kernel data. ARM64 never had segments. Both are flat paged.

Linking and loading as binding

Static linking copies every library into the executable, so all symbols are bound before it ships; Go does this by default. Dynamic linking leaves imports as relocation entries that ld.so resolves at start or, lazily, on first call through the PLT and GOT. Dynamic loading binds a library with dlopen while the program runs, as plugins do.

A position-independent executable (PIE) has no absolute addresses in its code: references are relative to the program counter and anything absolute lives in the GOT, a data page. The kernel can then load it at a random base each run, address-space layout randomisation (ASLR), so an attacker does not know where the stack, heap or libc live. Shared libraries were always built this way, which is why two processes mapping libc at different addresses still share its code pages: relocation touches the GOT, not the text. Distributions have built everything as PIE since about 2017.

What the MMU does

On every access the MMU translates the logical address through the current process's tables, whose root sits in a register (CR3 on x86-64, TTBR0 on ARM64); checks the entry's protection bits, user or kernel, read, write, execute; and raises a fault when the entry is absent or the access forbidden, so lazy allocation, copy-on-write and SIGSEGV are one hardware event with different kernel answers. The TLB caches its translations, as The TLB and page faults lesson covers.

Trade-offs

Earlier binding is simpler and faster: absolute code needs no translation hardware and no relocation pass. Later binding buys moving, sharing, overcommit and randomisation, and pays in silicon, page-table memory, TLB misses and a loader that must resolve symbols before main runs. Dynamic linking saves disk and memory across processes but makes start-up slower and dependency-sensitive, since a missing .so fails at run time; static linking does the reverse. PIE's cost, a register and some indirection, is now negligible.

In an interview

Give the stack in one breath: "Code is linked as if at zero, loaded at a random base, and the MMU translates every access through the page table, so the process never sees a physical address." Then anchor it: "The crash address in the core file meant nothing alone; subtracting the base from /proc/PID/maps gave the offset into the binary, and addr2line turned that into a line number."

Check yourself

ASLR maps the same libc.so at different virtual addresses in two processes. Why is there still one copy of its code in RAM?

Address binding and the MMU · Backplane