In this lecture we continue looking into memory allocation. We begin with Virtual Memory and how it is used to create the illusion of infinite memory! We then investigate how this illusion is created, and maintained, by a new non-contiguous allocation technique called paging!
Last time we established the critical distinction between logical addresses and physical addresses. Programmers use logical addresses — convenient labels starting at 0 — while hardware uses physical addresses — unique slots in actual memory chips. The MMU sits between these two worlds, translating one to the other so neither side has to compromise.
Let us trace the full pipeline from the code you write all the way to the physical hardware, and see exactly where the MMU draws the line.
Before stepping through the pipeline, watch one invariant: the program keeps the same logical view even when the hardware placement changes underneath it.
From Rust to Hardware — The Full Pipeline
Step through the transformation from source code to physical memory. The MMU sits between the logical and physical worlds.
Logical / Programmer Side
Rust Source Code
1#[no_mangle]
2fn one() -> i32 {
3 1
4}
5
6#[no_mangle]
7pub fn main() {
8 one();
9}
This is what you write. Line numbers, variable names, function names — all human-readable labels. The compiler turns this into machine code next.
This is the core promise of virtual memory: stable addressing for the program even when physical placement is hidden and flexible.
Step through all four phases above. Phases 1 and 2 are the logical side — what you as a programmer create and what the compiler produces. Everything uses logical addresses starting at 0x0000. Phases 3 and 4 are the physical side — where the hardware actually places your code. Notice how one() and main() end up at completely different physical locations, separated by unrelated data. Yet the call [rip + @one] instruction still works because the MMU transparently maps the logical label to its physical location. The programmer never notices.
Today's Agenda
The Universal Memory Dream — Revisiting LN13's ideal and why we need virtual memory
What Virtual Memory Is (and Is Not) — Clearing up the biggest misconception
Non-Contiguous Allocation — Breaking free from contiguous placement
Paging — Dividing memory into fixed-size pages and frames
Page Faults and Swapping — What happens when a page is not in RAM
The MMU in Action — Hardware-driven address translation
Page Tables — The data structure that maps pages to frames
An 8-Bit System — Walking through a complete paging example end-to-end
The Universal Memory Dream
Recall our discussion of the universal memory concept from LN13 — the theoretical ideal of a single, enormous, fast, cheap memory strip for all computation. We cannot build such hardware (yet), so we are stuck with the multi-strip, multi-speed, multi-cost reality of registers, caches, RAM, SSDs, and HDDs.
But what if, with a little more facade maintenance from our MMU, we could let programmers think and code as though they have a single infinite strip, while the physical implementation remains hidden behind the curtain?
If your MMU is sophisticated enough to maintain this illusion — supporting full logical-to-physical address translation — then you have a system that implements what is called Virtual Memory.
What Virtual Memory Is (and Is Not)
Virtual memory exists first to provide translation, protection, and isolation. One consequence is that the OS can also overcommit memory and move inactive data to backing storage. The core idea:
Divide each process into "chunks"
Load only the chunks the CPU currently needs into fast memory (RAM)
Swap chunks between memory and storage as needed
Before we go further, let us be very clear about what virtual memory is not:
⚠️ Common Misconception: Virtual memory is not inventing new space for storing things. It is not "downloading more RAM." You can only get more memory or storage by purchasing more or better hardware.
The distinction being made is that managing memory (RAM) is hard because it is limited, so we offload the things we do not pick up as often onto storage (SSD/HDD). When virtual memory says it is "placing chunks elsewhere," those chunks are placed on hardware so slow we no longer consider it readily available.
Recall the distinction between storage and memory: all ways to store computations on a system are functionally identical in their capacity to hold data. It is their speed that determines whether we call something "memory" or "storage." A cache holds things the same way an HDD does — it is just orders of magnitude faster.
As a fun aside, you can technically "download more RAM" by updating your system software to mark what would typically be storage hardware as memory. You can run programs off an HDD! The fundamental process is identical to RAM — it is just significantly slower. This is exactly what swap files and swap partitions do on modern operating systems.
Non-Contiguous Allocation — Revisiting the Foundation
Recall from LN14 that we desperately need non-contiguous allocation to fix fragmentation, but programmers refuse to restructure their code to accommodate it. Virtual memory solves this impasse: we perform non-contiguous allocation in the physical address space while maintaining the illusion of contiguous addressing in the logical address space.
To implement this, we need non-contiguous allocation techniques. We covered two contiguous techniques last time:
Fixed partitioning — simple, but classically suffered from internal fragmentation
Dynamic partitioning — eliminated internal fragmentation but created external fragmentation
What if we could leverage the fundamental principles of these two techniques to build something new? Our first non-contiguous technique is best described as "virtual fixed partitioning." Its name is paging.
💡 Key Insight: Paging takes the simplicity of fixed partitioning and removes its fatal flaw — the requirement for contiguity. Pages are fixed-size (like fixed partitions) but can be placed anywhere in physical memory (unlike fixed partitions).
Paging
Usually, a system has more virtual pages than physical frames. This is by design: programmers get as much logical space as they want (the "universal memory" illusion), while the physical hardware has a finite number of frames. The MMU handles the mapping so the programmer never notices the mismatch.
Before using the paging demo, watch what changes and what stays fixed: physical frame placement changes, but each page keeps its logical identity.
Paging — Pages and Frames
Logical pages (blue) are mapped to physical frames (green). Notice there are more pages than frames — that is the whole point of virtual memory.
Page / Frame size: 8 KB13 pages / 7 frames
13 pages but only 7 frames — 6 pages will have nowhere to go and must wait in storage until needed.
Step 0/7
Logical Address Space
1
pg 0
1
pg 1
1
pg 2
1
pg 3
1
pg 4+2 KB
2
pg 0
2
pg 1+4 KB
3
pg 0
3
pg 1
3
pg 2
3
pg 3+7 KB
4
pg 0
4
pg 1+7 KB
MMU
maps
→
Physical Address Space
RAM4 frames
—
f0empty
—
f1empty
—
f2empty
—
f3empty
Cache1 frame
—
f0empty
Swap / HDD2 frames
—
f0empty
—
f1empty
Total Pages
13
Total Frames
7
Loaded / Swapped
0 / 13
Internal Frag.
20 KB
Therefore, paging solves the placement problem by making contiguity a property of the virtual address space, not of the underlying physical memory.
Notice the familiar tradeoff from fixed partitioning: as you adjust the page/frame size, smaller pages match process sizes better (less internal fragmentation in the last page) but require more pages to track. Larger pages waste more space in the final page but require fewer tracking entries.
The critical difference from fixed partitioning is that pages do not need to be physically contiguous. A process's pages can be scattered across physical frames in RAM, and some pages may be absent and backed by secondary storage until needed. The program still behaves as though it has one contiguous virtual space.
Page Faults and Swapping
If we are dividing a process into pages and only loading some of them into physical frames, what happens when the CPU requests code or data that lives on an unloaded page?
Recall something critical: the assembly running on the CPU operates in logical address space. It does not know about pages, frames, or physical addresses. It just asks for "line 13" and expects to get data back.
When the MMU receives a logical address and discovers that the corresponding page is not loaded into any frame, the jig is up. The MMU declares a page fault.
💡 Connection to LN10-LN11: Notice that page faults create a scheduling problem that is structurally identical to CPU scheduling — multiple contenders competing for a limited resource, with the OS deciding who stays and who gets swapped out. The algorithms even share names: LRU, FIFO, and Working Set all appear in both domains.
As a side note, notice that this situation directly models the same management problem the CPU faces with scheduling. With CPU scheduling, multiple processes compete for a limited number of cores. With page management, multiple pages compete for a limited number of frames. There is an entirely adjacent scheduling problem dedicated to deciding which pages to evict — a direct parallel to CPU scheduling! Do not worry though; we have seen enough scheduling for one OS course.
This system only works because the hardware and OS cooperate to maintain the fiction of one coherent virtual address space. Let us take a deep dive into the exact steps involved.
The MMU in Action
Before stepping through the MMU translation, compare the fast path and the fault path: what is identical, and where does the expensive detour begin?
MMU in Action — Address Translation
Watch how the MMU translates logical addresses for the CPU, and what happens when a page fault occurs.
Step 1/5: CPU needs data at logical address 13
CPU
Running code...
Need data at
line 13
MMU
Idle
RAM
...
[1447]: "cat"
...
HDD
Storage
Step through both scenarios above. In the normal path, the MMU receives a logical address, translates it to a physical address using its page table, fetches the data from RAM, and returns it to the CPU. The CPU never knows the translation happened.
💡 Connection to LN8: The MMU we placed on our CPU diagram is doing all the heavy lifting here. In the fast path, the entire translation is invisible to software — pure hardware. Only in the fault path does the OS kernel get involved, making this a perfect division of labor between hardware (speed) and software (flexibility).
In the page fault path, the requested page is not resident. Extra steps are required: the kernel handles the fault, may need to evict some other page, reads the needed page into memory, updates the mapping, and then retries the access. The important operational detail is that the fault involves the OS and scheduler aswell as the hardware.
Page Tables — The Yellow Pages
The MMU has dedicated hardware support for converting logical addresses to physical addresses. When paging is implemented, it consults a page table whose contents are set up by the operating system.
The architecture of the page table rests on the bits of the virtual address. Recall that the MMU is hardware — it can only see and solve problems one instruction at a time. We as OS designers think in 64-bit instruction chunks, but the hardware reasons about individual bits within those chunks. This is exactly what we are about to see.
The idea: split each logical address into two parts:
Upper bits → Page number — used as a key to look up the corresponding frame in the page table
Lower bits → Offset — the position within the page; this is preserved as-is during translation
The MMU takes the page number bits, looks up the corresponding frame number in the page table, replaces the page bits with the frame bits, and keeps the offset unchanged. The result is a complete physical address.
Before you adjust the slider, predict which tradeoff will move: more page-number bits means what happens to page size and table size?
Page Table — Bit-Level Translation
See how a 16-bit logical address is split into page number and offset bits, looked up in the page table, and translated to a physical address.
Page bits: 4
4 page bits
24 = 16 possible pages
12 offset bits
212 = 4096 addresses/page
Logical Address: 0x002A
0
0
0
0
0
0
0
0
0
0
1
0
1
0
1
0
Page #0
Offset (42)
Step 1/5: Show the 16-bit logical address with page/offset split
Adjust the slider to change how many bits are dedicated to the page number. Watch how this affects the number of possible pages and the size of each page:
More page bits = more pages, smaller pages (finer granularity, less internal fragmentation, bigger page table)
This is the same tradeoff we saw with fixed partition sizes — but now it operates at the bit level of the instruction architecture itself.
Try the Page Fault Demo to see what happens when the page number has no corresponding valid entry. Therefore, the page table is not just a lookup map; it is also the hardware-visible record of whether a page is resident and accessible.
Putting It All Together — An 8-Bit System
Let us make all of this concrete by working through a complete example on an imaginary 8-bit computer. On a real 64-bit machine the numbers are enormous, but the mechanics are identical — an 8-bit system keeps everything small enough to trace by hand.
An 8-bit instruction can reference 28 = 256 different addresses. That is the entire logical address space of this system — 256 lines, numbered 0 to 255. Every logical address the CPU can express fits in those 8 bits.
Now we must decide how many of those 8 bits to sacrifice for the page identifier. Each bit we sacrifice doubles the number of pages while halving the number of lines in each page. Watch how the binary structure of the instruction literally carves the LAS into pages:
Page Bit Sacrifice — Dividing the Logical Address Space
An 8-bit system can reference 28 = 256 logical addresses — its entire logical address space. Watch how sacrificing upper bits to create a page identifier progressively divides this space.
8-bit instruction → addresses 0 to 255 → 256 total logical addresses (the full LAS)
Sacrifice 1 bit → 2 pages of 128 lines each
Instruction
P
O
O
O
O
O
O
O
1 page bit
7 offset bits
21 = 2 pages27 = 128 lines/page
Sacrificing 1 bit gives us 2 pages: 0 and 1. Each page holds 2⁷ = 128 lines. The split falls at line 127→128 because seven 1s in a row in binary (1111111) equals 127 — one less than 2⁷. When the offset overflows past 127, it carries into the page bit, flipping us from page 0 to page 1.
LAS (256 lines)
00–127
1128–255
Sacrifice 2 bits → 4 pages of 64 lines each
Instruction
P
P
O
O
O
O
O
O
2 page bits
6 offset bits
22 = 4 pages26 = 64 lines/page
Sacrificing 2 bits gives us 4 pages: 00, 01, 10, 11. Each page holds 2⁶ = 64 lines. The offset resets to 000000 at every page boundary because the carry rolls into the page bits — the offset is always 0-indexed within its page.
LAS (256 lines)
000–63
0164–127
10128–191
11192–255
Sacrifice 3 bits → 8 pages of 32 lines each
Instruction
P
P
P
O
O
O
O
O
3 page bits
5 offset bits
23 = 8 pages25 = 32 lines/page
Sacrificing 3 bits gives us 8 pages: 000 through 111. Each page holds 2⁵ = 32 lines. The upper 3 bits tell you which page; the lower 5 bits tell you exactly where within that page. This is where we will build our full example.
LAS (256 lines)
0000–31
00132–63
01064–95
01196–127
100128–159
101160–191
110192–223
111224–255
Key takeaway: The upper page-identifier bits tell you how many pages you get. The lower payload/offset bits tell you the exact size of each page. Together they always account for all 8 bits — and therefore all 256 addresses.
The pattern is mechanical: the page bits count pages, the offset bits size them. No matter how we split, all 256 addresses are accounted for — we are just choosing the granularity of our pages.
From Pages to Frames
Now we cross into the physical side. Let us commit to the 3-bit page scenario (8 pages of 32 lines each) and ask: how much physical memory do we need?
Each frame must be exactly 32 bytes — the same size as a page. If our physical memory is smaller than 32 bytes, we cannot even fit a single frame. If it is exactly 32 bytes, we get one frame. If it is 64 bytes, we get two. We will go with 64 bytes and see what the page table looks like.
With only 2 frames available for 8 pages, the page table has 8 rows but only 2 of them map to a frame. The other 6 are page faults — those pages live in storage until they are needed. When a translation succeeds, the 3 page bits are stripped off and replaced with however many frame bits we need (just 1 bit for 2 frames). The 5 offset bits pass through completely unchanged.
Full Translation — 8-Bit System with 3 Page Bits
Starting from the 3-bit page scenario (8 pages, 32 lines each), we choose a physical memory size, build the page table, and translate a logical address end-to-end.
Logical Address Space
0000–31
00132–63
01064–95
01196–127
100128–159
101160–191
110192–223
111224–255
8 pages × 32 lines
Physical Memory — Which Size Works?
Each frame must be 32 bytes (same as a page). How many frames can each memory size hold?
16 bytes
24 = 16
16 < 32
✗ Too small for even 1 frame
32 bytes
25 = 32
frame 0
Exactly 1 frame
64 bytes ✓
26 = 64
frame 0
frame 1
2 frames — we go with this
Page Table (8 rows)
PageFrame
000→✗ fault
001→0 (f0)
010→✗ fault
011→✗ fault
100→✗ fault
101→1 (f1)
110→✗ fault
111→✗ fault
Only 2 of 8 pages are loaded — the rest trigger page faults
Translation Example
① Logical address arrives (8 bits)
Logical: 0xAA (decimal 170)
1
0
1
0
1
0
1
0
page 5
offset 10
Page bits 101 → page table lookup → frame 1
swap bits
② Physical address produced (6 bits)
Physical: 0x2A (decimal 42)
1
0
1
0
1
0
frame 1
offset 10
Offset preserved: the lower 5 bits (01010 = 10) are identical in both addresses. Only the upper page bits were replaced with frame bits.
Why is the physical address only 6 bits?Because 64 bytes of memory only needs 6 bits to address (26 = 64). We replaced 3 page bits with just 1 frame bit (since 2 frames need only 1 bit). The 5 offset bits pass through unchanged. Physical address = 1 frame bit + 5 offset bits = 6 bits. This is the whole point — the logical address space (256 addresses) is larger than the physical address space (64 addresses), and the page table bridges the gap.
📚 Historical Note: The concept of paging was first implemented in the Atlas computer at the University of Manchester in 1961. Atlas had a one-level store that automatically moved data between a small magnetic core memory and a large drum — the first hardware-managed virtual memory system. Every modern laptop is a descendant of that idea.
This is the entire mechanism. The logical address space (256 addresses, 8 bits) is four times larger than the physical address space (64 addresses, 6 bits). The page table bridges that gap, and the offset bits that index within a page are the same bits that index within a frame — because pages and frames are the same size. That is why the offset is preserved: it does not need to be translated at all.
Summary
The logical / physical address distinction from LN14 is the foundation of virtual memory
Virtual memory lets the OS manage processes larger than physical RAM by swapping "chunks" (pages) between memory and storage
Virtual memory is not creating new RAM — it is a translation and protection mechanism that can also use backing storage for nonresident pages
Paging divides logical space into pages and physical space into frames of equal size
Pages can be non-contiguously placed in frames — solving the fragmentation problem without the programmer noticing
A page fault occurs when the CPU requests a page that is not currently resident or not currently mapped for that access
The page table maps page numbers to frame numbers plus status bits; the OS maintains it and the MMU consults it
More page bits = more pages, smaller pages, less waste, bigger table; fewer page bits = the opposite
📝 Lecture Notes
Key Takeaways:
Virtual memory maintains a stable per-process address space while physical placement remains flexible underneath
Paging is "virtual fixed partitioning" — it keeps the fixed-size-block tradeoff (internal fragmentation in the last page, tracking overhead) while gaining non-contiguous placement
The MMU performs translation in hardware, while the OS sets up and repairs the mappings
Page faults are the expensive edge case: when a needed page is not resident, the kernel must intervene before execution continues
The page table splits each virtual address into page-number bits (the lookup key) and offset bits (preserved as-is), replacing the page number with a frame number to produce the physical address
The page/offset bit split is a fundamental architectural decision that affects page count, page size, internal fragmentation, and page table size