The complete road — how a line of code becomes something the CPU runs, and how memory (pages, frames, the page table) makes it all fit. Now with diagrams.
Your program looks tidy and ordered (left). Real RAM is a scattered shared mess (right). The page table connects them — that's the whole trick.
It begins as plain text in a .cpp file on your disk. The CPU cannot run text — it has no idea what int main() means yet.
// main.cpp — just text on disk
int main() {
int x = 42;
return 0;
}
Nothing in RAM. Nothing running. Raw material.
The compiler turns each source file into machine-code object files. The linker stitches them together (plus libraries) into one executable binary.
main.cpp ──▶ compiler ──▶ main.o ┐ util.cpp ──▶ compiler ──▶ util.o ┤──▶ linker ──▶ ./program (binary on disk) libc, libstdc++ ┘
Still a file on disk — but now it's CPU-runnable instructions. (The compiler/linker part of your Cherno track is the on-ramp to this road.)
The OS creates a process and hands it a private virtual address space — a made-up range of addresses only this program can see.
Yes — it's imaginary, and no — it does not hold RAM. It stores nothing itself. It's just a set of address numbers (labels) the program is allowed to use — like house numbers on a street with no houses built yet. The real bytes live in RAM frames; the page table translates a virtual number into the real RAM spot.
Inside that virtual space, your program is arranged into the regions you know. This tidy picture is the virtual layout — how it looks to the program:
high address ┌──────────────┐
│ Stack │ ↓ grows down — locals, function calls
│ ⋮ │
│ Heap │ ↑ grows up — new / malloc
│ BSS / Data │ — globals & statics
low address │ Text (code) │ — your instructions
└──────────────┘
Every region is made of pages. Remember: this whole neat layout is virtual — the real bytes are scattered across RAM.
So it doesn't need one big contiguous block of RAM. Chop the program into equal-size pages, and each page goes into any free slot, anywhere.
Bonuses unlocked: virtual memory (run programs bigger than RAM), isolation & protection, and sharing (one library copy used by many programs).
It's wasted RAM you get without paging. Programs load and quit, leaving scattered holes. Total free can be big, yet no single hole is large enough for a contiguous request.
Is external fragmentation a method like paging? No — it's a problem, not a technique. You never choose “external fragmentation vs paging.” The old contiguous-allocation method suffers from it; paging is the method that cures it.
One big ball that fills the box exactly. Page size = frame size, so one page completely fills one frame — never many pages in a frame, never one page across two frames. Strictly one-to-one.
Can 2 KB of stack + 2 KB of heap share one page in one frame?
Even though 2 + 2 = 4 KB (one page's worth), stack and heap are not next to each other. Stack sits at the top of the address space, heap near the bottom, with a huge gap between. A page only ever holds bytes from one continuous stretch of virtual addresses — and stack & heap aren't continuous. So each gets its own page → its own frame. Minimum: 2 pages, 2 frames.
By the page table — a per-process list mapping program's page → real frame. Pages are connected through this table, not through location (see the big-picture diagram at the top).
When your program touches an address, the CPU's MMU splits it in two:
Virtual address = [ page number | offset ] │ │ which page how far into it
Step 1: page number → page table → frame number. Step 2: frame number + offset → real physical address.
Example — 4 KB pages, program reads virtual address 12,290: 12,290 ÷ 4096 = page 3, remainder 2 (offset) page table: page 3 → frame 47 physical address = start of frame 47 + 2 ✅
Speed note (TLB): the CPU caches recent page→frame translations in the TLB, so most accesses skip the table lookup entirely.
The OS loads pages on demand — only when used. Touch a page that isn't in RAM → page fault → OS grabs a free frame, loads the bytes from disk, updates the table, resumes you — invisibly.
new/malloc 30 KB, physical frames are often handed out only as you write to each page. No RAM burned on untouched memory.RAM full and a new page needed? The OS evicts a cold page to disk (swap), freeing its frame. If that page is needed again later, a page fault brings it back.
RAM full ──▶ evict cold page ──▶ disk (swap) ──▶ frame free ──▶ load needed page
Your pages are one tenant. Frames hold everything the machine is doing — see the mix on the right side of the big-picture diagram: other processes' pages, the OS kernel, the page tables themselves, shared libraries (one libc for many programs), the page cache (files cached from disk), I/O / DMA buffers, and free frames waiting to be handed out.
The CPU repeats fetch → decode → execute: ask for the next instruction by virtual address → MMU (via TLB / page table) translates to a frame → bytes fetched from RAM → instruction runs. Every access goes through the same translation.
With 10 KB pages (easy math):
30 KB heap → 3 pages → each backed by a frame, scattered, connected by the page table. Program still sees one clean 30 KB heap. (Real pages are usually 4 KB, so 30 KB ≈ 8 pages/frames — same idea. With demand paging, a frame is handed out only on first write to that page.)
You write code → compiler + linker make an executable on disk → the OS starts a process with a private, imaginary virtual layout → it's cut into pages so it needn't be contiguous (killing external fragmentation) → each page is stored in one frame in RAM → the page table connects page → frame → the MMU translates every address (page number + offset) so scattered RAM looks contiguous → pages load on demand and overflow to disk when RAM is full → the CPU fetches from frames and executes.