JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Virtual vs Physical: The Address Illusion

Every pointer you have ever printed was a polite lie. This guide reveals the illusion every program lives inside — that it owns a vast, private memory starting at the same address as everyone else — and shows who maintains the lie and why it is one of the best ideas in computing.

The number you have been trusting

By now you have printed plenty of addresses. You took the address of a variable with &x, stored it in a pointer, and ran something like `printf("%p\n", (void *)&x)` to watch a hexadecimal number fall out — maybe 0x7ffd3a1c4abc. You learned to picture memory as one long array of bytes, each with its own address, and that number told you exactly which byte your variable lived at. Everything in the earlier rungs treated that address as the honest truth: a real location, a real slot in the machine's RAM.

Here is the unsettling thing this rung is built on: that number is not a real location in your RAM. It is a virtual address — a fictional coordinate that means something only inside your running process. The actual byte it refers to could be physically sitting almost anywhere in RAM, at a completely different number, or — as a later guide will show — not in RAM at all just yet. The address you printed is real to your program and to nothing else. It is a virtual address, distinct from the physical address of the chip cell that actually holds the byte, and the gap between those two is the whole subject of this rung.

Why would a computer go to the trouble of lying to every program about where its own data lives? Because the lie buys three things at once that are very hard to get otherwise: privacy (no process can name, let alone touch, another process's bytes), simplicity (every program gets to believe it starts at the same tidy address and owns a huge, contiguous space), and flexibility (the system can move the real bytes around, or hold some on disk, without the program ever noticing). That whole arrangement has a name — virtual memory — and learning how it works is learning the deepest layer of the abstraction stack a systems programmer touches before the silicon itself.

Two address spaces, and the translator between them

Picture two separate worlds of addresses. The first is the virtual address space: the range of addresses your process is allowed to talk about, laid out exactly as the earlier guide on address-space layout described — code low down, then static data, a heap growing up, a stack growing down from high addresses. On a 64-bit machine this space is staggeringly large, far larger than any real RAM, and every process gets its own fresh copy of it. The second world is physical memory: the actual addresses of the actual RAM chips in your machine, a much smaller and very concrete set of slots, shared by everything running at once.

Between those two worlds sits a translator. Every single time your CPU reads or writes memory — every instruction fetch, every load, every store — the virtual address coming out of the program is converted into a physical address before it reaches the RAM. This conversion is called address translation, and it happens for every memory access, billions of times a second, which means it absolutely cannot be slow. So it is not done in software by the operating system on each access; it is done in dedicated hardware on the CPU called the memory management unit, or MMU, which the next guide takes apart in detail. For now hold just the shape of it:

  program says:   *p   where p = 0x00007ffd3a1c4abc   (virtual)
                           |
                           v   [ MMU translates, every access ]
  RAM actually sees:           0x000000011e8c4abc       (physical)

  same virtual address 0x00007ffd3a1c4abc
    in process A  ->  physical 0x000000011e8c4abc
    in process B  ->  physical 0x0000000007240abc   (totally different byte)
One virtual address, translated by the MMU on every access. The identical virtual number in two different processes maps to two different physical bytes — which is exactly why processes cannot see each other's memory.

Why the same address means different things

Look hard at the bottom of that sketch, because it explains something you may have already noticed and found strange. Run a small program twice, print the address of the same global variable, and you can get the same virtual address both times — yet the two runs never collide, never corrupt each other. Run two different programs at once and each can hold a pointer with value 0x00400000 pointing at its own code, with no conflict at all. That is impossible if addresses were physical: two things cannot occupy one RAM slot. It is perfectly fine when addresses are virtual, because each process has its own private translation, so its 0x00400000 lands on a different physical byte from everyone else's.

The translation is not just a relabeling — it is also a gate. For each region of a process's virtual space, the system records what is allowed there: may it be read, may it be written, may it hold instructions the CPU will execute. This is memory protection, and it rides along on the same translation machinery. When your program dereferences a virtual address that has no valid translation, or writes to one marked read-only (like the page holding a string literal), the hardware refuses, and the kernel turns that refusal into the segmentation fault you have met before. A segfault, seen from here, is the translator reporting that the virtual address you used does not map to anything you are permitted to touch.

It cannot be byte by byte: enter the page

There is an obvious problem hiding in all of this. If every single byte of virtual memory needed its own private entry in some lookup table saying which physical byte it maps to, the table would be as big as the memory it describes — useless. The fix is the central trick of the whole scheme: the address space is carved into fixed-size blocks called pages, almost always 4 KiB (4096 bytes) each, and translation is done one whole page at a time, not one byte at a time. A virtual page maps onto a physical block of the same size called a frame, and the system only has to remember the mapping for each page, not for each byte — roughly four thousand times fewer entries.

Once you see addresses as page-plus-offset, translation becomes tidy. Split a virtual address into two parts: the high bits pick which page, and the low bits are the offset — how far into the page the byte sits. Because a 4 KiB page is exactly 0x1000 bytes, the bottom 12 bits (since 2^12 = 4096) are the offset, and everything above them is the page number. Translation only ever touches the page-number part: it looks up which physical frame that virtual page maps to, then keeps the offset completely unchanged. The byte's position within its page is identical in both worlds; only the page itself gets relocated.

Where is that page-to-frame mapping kept? In a per-process structure called a page table — one table belonging to each process, holding one entry per page that says which physical frame it currently lives in, plus the protection bits that gate it. The MMU consults this table on every access, and to make that fast it caches the most recent translations in a tiny on-chip lookup buffer (you will meet it as the TLB next guide). The page table is exactly what makes the per-process illusion concrete: process A and process B each have their own table, so the same virtual page number resolves to different frames in each. We are only naming these pieces here; the next guide builds the actual page table and walks a translation through it from first principles.

What the illusion unlocks, and where this rung goes

Once a layer of translation sits between virtual and physical, a whole family of powerful tricks becomes possible, and they are the rest of this rung. Because the page table can say "this virtual page maps to no frame yet," the system can hand a program a huge address space and only find real RAM for a page the very first time the program actually touches it — that is demand paging, and the moment of touching an absent page is a page fault the kernel quietly resolves. Because a page can be marked read-only and shared, two processes can point at one physical copy and only get a private duplicate if one of them writes — that is copy-on-write, the reason fork() stays cheap. And because a file can be slid straight into the address space as pages, you can read a file by simply dereferencing pointers — that is mmap().

So hold the shape before we zoom in. Your program lives entirely in virtual addresses; the MMU translates each one to a physical address on every access, one page at a time, through a per-process page table that also enforces protection. That single mechanism gives every process privacy, a clean uniform layout, and the freedom for the kernel to place, move, share, or temporarily withhold the real bytes. "Pages, Page Tables, and the MMU" builds the translator itself. "Page Faults and Demand Paging" follows what happens when a page is not there. "Copy-on-Write and How fork Stays Cheap" and "mmap and Memory-Mapped Files" then cash in the illusion for two of the most useful tools you will ever wield. You now know what the lie is; the rest of the rung is how it is kept.