Address translation is the quiet mechanism that makes almost everything else in modern computing possible — process isolation, memory protection, running more programs than physical RAM could hold at once, even something as basic as every process believing it has its own private, contiguous address space starting near zero. I want to walk through what address translation actually is, how it works mechanically, and why the whole concept exists in the first place.
Virtual Addresses vs Physical Addresses
Every running process operates entirely in terms of virtual addresses — numbers a program uses to reference memory, generated by compiled code, pointers, the stack, all of it. These numbers have no direct relationship to where data actually sits in physical RAM. The actual RAM is addressed using physical addresses, and it’s the job of address translation to convert one into the other, transparently, on every single memory access.
Process's view: Reality:
+-------------------+ +----------------------+
| 0x0000 - code | | Physical RAM |
| 0x1000 - data | ----> | scattered frames, |
| 0x7fff.... - stack | | shared with every |
+-------------------+ | other running process |
+----------------------+
Two processes can both believe they own address 0x400000, and both can be right, simultaneously, without conflict — because that virtual address gets translated to two completely different physical addresses depending on which process is running. This is the foundational trick that makes process isolation possible without every program needing to know or care about what else is running on the machine.
Why Address Translation Exists At All
It would be simpler, in a sense, for programs to just address physical RAM directly — and very early computing systems, and today’s simplest microcontrollers, do exactly that. But direct physical addressing creates serious problems the moment you want to run more than one program safely:
- No isolation. Any process could read or corrupt any other process’s memory, or the kernel’s own memory, by just referencing the right address.
- No relocation flexibility. Every program would need to know exactly which physical addresses were free at load time, and two programs couldn’t both assume they start at address zero.
- No way to run programs larger than physical RAM, or to share physical memory efficiently between many programs whose combined memory needs exceed what’s actually installed.
Virtual memory, powered by address translation, solves all three at once: each process gets an isolated, private-feeling address space; the OS is free to place physical pages wherever convenient (and move them later); and programs can reference far more virtual address space than physical RAM actually provides, with the OS filling in physical backing only for the parts actually in use.
How Translation Actually Happens: Paging
The dominant mechanism (used by essentially every general-purpose OS today — Linux, Windows, macOS, and their mobile counterparts) is paging: dividing both virtual and physical address space into fixed-size chunks (commonly 4KB) called pages and frames, respectively, and maintaining a mapping table between them.
A virtual address is split into two components:
+---------------------------+------------------+
| Virtual Page Number | Page Offset |
+---------------------------+------------------+
The page number identifies which page the address falls in; the offset identifies exactly where within that page. Translation only ever needs to convert the page number — the offset is identical between virtual and physical addresses, since pages and frames are the same size and page-aligned.
Virtual Address
|
v
[ Virtual Page Number ] [ Offset ]
|
v
Page table lookup ---> Physical Frame Number
|
v
[ Physical Frame Number ] [ Offset ] <-- same offset, unchanged
|
v
Physical Address
On x86-64 Linux, that “page table lookup” is itself a multi-level walk (PGD -> PUD -> PMD -> PTE, as covered in more depth in a companion article on Page Table Entries), because a single flat table mapping every possible virtual page directly would be enormous and mostly empty for any real process. The multi-level, hierarchical structure lets the OS only allocate table entries for regions of address space actually in use.
The Role of the MMU
All of this translation happens in hardware, performed by the Memory Management Unit (MMU), a component built directly into the CPU. Software (the OS) is responsible for setting up and maintaining the page tables, but the actual per-access translation — happening potentially billions of times per second — is done entirely by dedicated silicon, because doing it in software on every access would be far too slow.
The TLB (Translation Lookaside Buffer) sits alongside the MMU’s page-table-walking logic as a cache of recent translations, avoiding the multi-level walk entirely for the overwhelming majority of accesses (a topic covered thoroughly in its own dedicated article, since it’s central enough to deserve full treatment on its own).
Segmentation: The Other (Mostly Historical) Approach
Paging isn’t the only address translation scheme that’s existed. Segmentation divides memory into variable-sized logical segments (code, data, stack, etc.) rather than fixed-size pages, translating a segment-relative address into a physical one via a segment base and limit. Early x86 processors relied heavily on segmentation, and vestiges of it (segment registers, though mostly vestigial in 64-bit mode) still exist in the x86-64 architecture for legacy compatibility. Modern general-purpose OSes, however, rely almost entirely on paging, because fixed-size pages are dramatically simpler to manage without suffering the external fragmentation problems variable-sized segments introduce.
Multi-Level Translation Under Virtualization
Things get an extra layer more complex under virtualization, worth mentioning because it’s an increasingly common real-world scenario. A virtual machine’s guest OS performs its own translation from guest-virtual to what it believes is “physical” memory — but that guest-physical address is itself really just another virtual address from the host’s perspective, requiring a second translation stage (guest-physical to host-physical) managed by the hypervisor. Modern CPUs support this directly in hardware via Extended Page Tables (Intel EPT) or Nested Page Tables (AMD NPT), avoiding the need for the hypervisor to trap and emulate every guest page table update in software, which would otherwise be prohibitively slow.
Address Translation Across Platforms
- Linux on x86-64 uses 4-level (or 5-level, on newer kernels/hardware supporting 5-level paging for larger address spaces) page tables, managed through the generic
mmsubsystem with architecture-specific low-level code. - Windows uses fundamentally the same hardware paging mechanism (since it’s dictated by the CPU architecture, not the OS), with its own higher-level abstractions (Virtual Address Descriptors, working sets) layered on top.
- Android, on ARM hardware, uses ARM’s own translation table format, conceptually equivalent but with different bit layouts and typically supporting multiple translation granule sizes (4KB, 16KB, 64KB).
- iOS/macOS, also on ARM (Apple Silicon) today, uses the same fundamental ARM paging mechanism, with XNU’s own VM subsystem managing the page tables.
- Classic UNIX systems on varied architectures (SPARC, MIPS, PA-RISC) each had their own translation table formats, but pioneered the conceptual model — demand-paged, multi-level, hardware-assisted translation — that essentially every modern OS still follows.
Practical, Real-World Consequences
- ASLR (Address Space Layout Randomization), a widely used security mitigation, is only possible because address translation decouples virtual addresses from physical layout — the OS can randomize where a program’s code, stack, and heap sit in virtual address space on every run without touching how physical memory is actually organized.
- Memory-mapped files (
mmap()) work by having the OS set up page table entries that translate a range of virtual addresses to physical pages backed by file content rather than anonymous memory, letting file I/O piggyback on the exact same translation machinery as ordinary memory access. - Swapping relies on the OS being able to mark a PTE as “not present” and repurpose the entry to record swap location, transparently pausing and resuming translation for that page as needed without the process ever being aware.
Troubleshooting and Observability
- On Linux,
/proc/<pid>/mapsshows a process’s virtual memory layout — the mapped ranges, permissions, and backing files — directly reflecting what the translation system has set up for that process. /proc/<pid>/pagemapallows inspection of the actual physical frame each virtual page currently maps to, useful for low-level memory debugging.- Unexpectedly slow memory-intensive code is often a translation-overhead problem (TLB misses from poor locality or insufficient hugepage use) rather than a raw compute problem — profile with hardware performance counters before assuming otherwise.
dmesg/kernel logs reporting segmentation faults or general protection faults are, at the hardware level, address-translation failures — either no valid mapping existed, or a permission check embedded in the page table walk failed.
Best Practices
- Design memory-intensive software with locality of reference in mind — address translation (and its caching via the TLB) rewards predictable, sequential, or repeated access patterns far more than scattered, random ones.
- Use
mmap()for large file I/O where appropriate, letting the same translation machinery handle file-backed pages efficiently rather than manually copying data through read/write syscalls. - Understand that ASLR, memory protection, and process isolation are not separate features bolted onto memory management — they are direct, natural consequences of address translation existing at all.
Summary
Address translation is the mechanism that separates the tidy, private-feeling address space every program sees from the messy, shared reality of physical RAM underneath. By converting virtual addresses to physical ones on every memory access — using paging, hardware-accelerated by the MMU and TLB, and orchestrated by OS-maintained page tables — modern systems achieve process isolation, memory protection, flexible physical memory allocation, and the ability to run programs larger than physical RAM, all without programs themselves needing any awareness that the translation is happening at all.
FAQs
What’s the difference between a virtual address and a physical address? A virtual address is what a program uses to reference memory, private to that process’s address space; a physical address identifies an actual location in RAM. Address translation converts one into the other on every access.
What component performs address translation? The Memory Management Unit (MMU), a hardware component built into the CPU, performs the actual translation on every access, working from page tables the OS maintains in memory, with a TLB caching recent results.
Is paging the only way to do address translation? No — segmentation is an older, largely historical alternative, but virtually all modern general-purpose operating systems rely on paging due to its simpler, fragmentation-resistant fixed-size-unit design.
How does virtualization affect address translation? It adds an additional translation stage — guest-virtual to guest-physical, then guest-physical to host-physical — accelerated in hardware via technologies like Intel EPT or AMD NPT to avoid prohibitively slow software emulation.
References
- Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A
- Silberschatz, A., Galvin, P., Gagne, G., Operating System Concepts, virtual memory and paging chapters
- Arm Architecture Reference Manual, Virtual Memory System Architecture (VMSA)
- Linux kernel documentation,
Documentation/mm/directory