Step 1 of 5 · Reading · ~4 min
Read
Boot & Memory
Virtual Memory & Paging
Every process believes it owns a clean address space starting at zero. They are all wrong, and wrong safely: the lie is told per process, enforced by the MMU and maintained by the kernel. Paging is the machinery. In this lesson your program plays the MMU of a 32-bit x86 CPU: it is handed physical memory containing real page tables and a CR3 register, and for each memory access it either produces the physical address or raises a page fault with the exact error code the hardware would push.
Splitting the address
Memory is managed in 4 KiB pages, so the low 12 bits of an address are the offset and pass through untouched. The classic 32-bit scheme has two levels, so the rest splits in two:
31 22 21 12 11 0
+--------------+--------------+--------------+
| directory ix | table ix | offset |
+--------------+--------------+--------------+
10 bits 10 bits 12 bits
CR3 holds the physical address of the 4 KiB page directory: 1024 entries of 4 bytes. Each present directory entry (PDE) points at a page table of 1024 page-table entries (PTE). The PTE names the physical frame.
The entry format
Both levels use the same layout. The frame address is in bits 12-31; the flags are below it:
| Bit | Name | Meaning |
|---|---|---|
| 0 | P | present; clear it and any access faults |
| 1 | R/W | writes allowed |
| 2 | U/S | user mode (ring 3) allowed |
| 5 | A | accessed: the CPU sets it when the entry is used |
| 6 | D | dirty: the CPU sets it on a write (in the PTE) |
| 7 | PS | in a PDE: this entry maps a 4 MiB page directly |
An access is allowed only if both levels allow it: a write needs R/W set in the PDE and the PTE, a user access needs U/S in both. The kernel uses this to hide kernel memory from user mode: U/S clear.
A worked walk
CR3 = 0x1000. The directory entry at 0x1004 is 0x00002007 (frame 0x2000, flags P, R/W, U/S). The table entry at 0x200C is 0x00009005 (frame 0x9000, flags P and U/S, read-only). User read of 0x00403025:
- directory index =
0x00403025 >> 22= 1, so the PDE is at0x1000 + 1*4=0x1004. - table index =
(0x00403025 >> 12) & 0x3FF=0x403 & 0x3FF= 3, so the PTE is at0x2000 + 3*4=0x200C. - offset =
0x025. Physical address =0x9000 | 0x025=0x9025.
A user write to the same address finds R/W clear in the PTE. That is a protection fault.
Page faults are a feature
When translation fails the CPU puts the faulting address in CR2 and pushes an error code before entering vector 14:
| Bit | Clear | Set |
|---|---|---|
| 0 (P) | page not present | protection violation on a present page |
| 1 (W) | the access was a read | the access was a write |
| 2 (U) | the CPU was in supervisor mode | the CPU was in user mode |
The example fault is error code 0x7 (present, write, user). A user read of an unmapped page is 0x4. The kernel reads the code and CR2 and decides: allocate a frame (demand paging), copy the page (copy-on-write), or kill the process (the segfault you know).
The accessed and dirty bits are how the kernel learns what is cold and what must be written back before eviction. Your MMU sets them in memory, and only when the walk succeeds.
Common mistakes
- Checking permissions on the PTE only. The PDE's R/W and U/S matter too.
- Forgetting the offset, or using the PTE's flag bits as part of the frame address (mask with
0xFFFFF000). - Using the wrong error-code bit for user mode (bit 2) or write (bit 1), or setting bit 0 for a not-present fault.
- Treating a
CR3value with low bits set as an unaligned directory. The low 12 bits are control flags, not address.
The real TLB caches finished translations, which is why changing a mapping needs invlpg and writing CR3 flushes. x86-64 repeats the same walk four times, 9 bits per level.
Your exercise
The starter reads memory and CR3 for you. You write the translation, including the 4 MiB PS case, the hardware-set accessed/dirty bits and the fault code.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…