A Table

· vm, aslr, page table

In the last piece we saw our prog printed 0x104720000 while holding a 3 and a 300. The curveball was that the number isn't a location. It's a lookup key, and each process resolves it differently. (Same address but two values). So if addresses aren't locations, how has any of your code been working?

Addr in instructions ?

Your code is full of addresses. Pointers, arrays, function calls, the stack. If addresses aren't real, how has any of it been working? The answer is that your program never depended on a real one. AKA virtual addresses.

Here's the precise cliam, You have never written a physical address. A non-PIE linker can bake numbers into jump tables and immediates that look like addresses — those are still virtual, patched at load time, not physical locations in RAM. On a PIE binary (what you actually ran), the baked number is usually a distance anyway. Either way: never a physical address.

More than that, an enormous fraction of what your program does is expressed as offsets or simply put distances, rather than absolute positions. Cuz Distances works well with relocation. Positions don't.

Take an array for example

c
int arr[10];

arr and &arr[0] are the same address. The array is its first element's location, so it's not wrong to say that we address an array by its first element's address. And arr[5] isn't some table lookup. The compiler emits arithmetic, which put simply can look like , take the base, add 5 × 4 bytes, load from there. Every array access you have ever written is base-plus-offset. Strings are the same thing, a char array, terminated by a '\0', indexed by adding a distance to a base. (For anyone reading this from beyond the grave, I know it's called null-terminated char array.)

The stack works the same way. A local variable isn't at some absolute address the compiler knew about. It's at the frame pointer minus twenty, or the stack pointer plus eight. Push a frame, and every local in it shifts along with the frame; the offsets inside the frame don't change at all. That's why recursion works. And function calls, too, but there is where I want to stress on a wrong understanding i used to have all the time, because it's really important. A call instruction on x86-64 usually encodes its target as a relative displacement, not "jump to address X" rather "jump 2 or 142 bytes past the next instruction" — relative to the address of the instruction after the call, not the call itself. Relocatable. But ret is not relative. call pushes the absolute virtual return address onto the stack (the VA of that next instruction), and ret pops that VA and jumps to it. The MMU still translates it. Relative call, absolute-VA return, commit to memory.

(While we're being precise: x86-64 instructions are variable-length, anywhere from 1 to 15 bytes, with 15 being the hard architectural ceiling rather than anything typical; most are two to five. Base RISC-V instructions are 4 bytes; the compressed extension, which a lot of chips actually ship, is 2. Still a lot more regular than x86.)

And then there's the move that ties it all together: RIP-relative addressing. When your compiled code wants a global variable, it doesn't emit "load from 0x104720000." It emits something closer to:

asm
mov rax, [rip + 0x2f31]

Meaning: take RIP as it will be after this instruction (the address of the next one), add 12,081, load from there. Not the address of the mov itself , x86-64 RIP-relative is always relative to the following instruction. The mov has no idea where it sits. It only knows how far away its data is, and that distance is fixed forever at link time regardless of where the whole program ends up sitting.

this is where i plug my linking blog ;)

which is why the following is possible,
run the first program again. The address changed. Mine went from 0x104720000 on one run to 0x104c54000 on the next, while the binary has not recompiled. The loader dropped the entire program at a different base address, and the program was completely unbothered; every pointer inside it still worked perfectly. That's ASLR, address space layout randomization, and the reason for it is security. A whole family of exploits works by corrupting memory and then redirecting execution to a known address, a function you didn't mean to call, a buffer the attacker filled. All of it depends on the attacker knowing where things are. So the kernel stops telling them. Every launch, the code, the stack, the heap, and the shared libraries all land somewhere new. This only works because the binary is a PIE, position-independent executable, built entirely out of offsets, exactly as described above. On Linux, you can opt out with -no-pie and watch the address freeze. On Apple Silicon, you can't; it's mandatory.

My two addresses were 0x104720000 and 0x104c54000. Both end in three zeros. Both are exact multiples of 0x4000 — 16,384, or 16 KiB. The randomizer isn't choosing arbitrary byte offsets. It's choosing from a grid. Hold onto that; we're about to find out what the grid is.

So the TL;DR here is , your program is built out of distances. Distances don't care where you put them. The operating system is therefore free to drop your program absolutely anywhere in the virtual address space (on a page grid, we'll get there). That's ASLR. Physical RAM is a different knob , the MMU picks those frames later.

Every Process either you spwan it through your program or a program forks it , they get their own Virtual address space, private , isolated. On paper a 64-bit CPU can name $2^{64}$ addresses. Real silicon is smaller: 48 bits of virtual address on typical x86-64 (four-level page tables), 47 bits on Apple Silicon with 16 KiB pages (three 11-bit levels plus a 14-bit offset). Still enormous. Each process gets that whole logical space.

The addresses your program produces , 0x104720000 and friends, live in that virtual range before the MMU touches them. After the MMU, you have a physical address, and that one has to land in actual DRAM, 16 GB or whatever you have. That is really the crux of VM.

PA Feasible ?

Physical addressing: the CPU emits an address, the address goes straight to the memory bus, the memory responds. Works perfectly. It's what early machines did. It's what the microcontroller in your washing machine does. It's simple, it's fast, and there is nothing wrong with it , right up until you want to run two programs at once. Then it fails in three distinct ways.

It collides. Your compiler needs to decide where value lives. Say it picks 0x104720000. Fine. Now run two copies of the program. Both want that exact byte. (cuz it can , nothing is there to prevent it) . One of them has to lose, which means the compiler can't ever hardcode anything, which means every program has to be relocated at load time against every other program currently running. It's a scheduling nightmare that changes every time a user launches something.

It can't be protected. If any program can name any physical byte, then any program can write any physical byte. A bug in your text editor can scribble over the kernel. A malicious PDF reader can read your password manager's memory. There's no wall to put up, because there's no place to put the wall; the CPU is handing raw addresses to raw memory and nothing is in between to enforce protection.

And, yes, it doesn't fit. Your machine has, what, 16 GB? Your game folder is 90. Something has to give up.

Now here's the thing about virtual memory which is beautiful. One mechanism fixes all three.

Put a translation step between the CPU and the memory bus. The CPU generates a virtual address; a piece of dedicated hardware called the MMU ( memory management unit ) translates it into a physical address using a table the kernel maintains; only then does anything reach memory. And once that translation is happening on every single access anyway,

If you take away one idea from this piece, take that.

That's why the demo worked. The kernel did not hand parent and child a byte at 0x104720000. It handed each of them a table. The MMU looks up the same key in whichever table is currently installed. After the first write, those two entries point at two different pieces of RAM. Same printed number. Completely different memory.

The kernel never gives you memory. It gives you a table.

Right. Go home.

Except the table cannot have a row for every byte. A 64-bit keyspace, one entry per byte, and the table is larger than the memory it describes. So we don't translate bytes. We translate chunks. That's the grid ASLR was snapping to. That's next.