Recursive Mapping
Recursive mapping is a clever trick that lets us access page tables through virtual addresses after paging is enabled — without needing to separately track the physical addresses of every page table we create.
The Problem It Solves
After paging is enabled, the CPU translates all virtual addresses. But map_page() needs to modify page table entries. To do that, it needs to write to the page table — which means it needs a virtual address that maps to the page table’s physical location.
Without recursive mapping, we’d need to maintain a separate data structure that records “page table for virtual range X is at physical address Y.” That’s bookkeeping overhead we can avoid.
The Trick: PD[1023] Points to Itself
We set the last entry of the page directory to point to the page directory itself:
#![allow(unused)]
fn main() {
(*page_dir).entries[1023] = pd_frame | PAGE_PRESENT | PAGE_WRITABLE;
}
Now, when the CPU translates a virtual address that has 1023 as its page directory index, it ends up reading the page directory as if it were a page table. And if the page table index is also 1023, it reads the page directory as if it were a page!
The Magic Addresses
This creates two special virtual address regions:
Virtual address 0xFFFFF000:
┌─────────────────────────────────────────────────┐
│ PD index = 1023 → PD itself (used as page table)│
│ PT index = 1023 → PD itself (used as page) │
│ Offset = 0x000 │
└─────────────────────────────────────────────────┘
Result: You're reading the page DIRECTORY as if it were a page.
0xFFFFF000 gives you access to the page directory's contents.
Virtual addresses 0xFFC00000 – 0xFFFFEFFF:
┌─────────────────────────────────────────────────┐
│ PD index = 1023 → PD itself (used as page table)│
│ PT index = N → PD[N] entry (the Nth PT frame)│
│ Offset = X → byte within that PT frame │
└─────────────────────────────────────────────────┘
Result: You can read and write any page table!
0xFFC00000 = page table 0 (covers 0x00000000-0x003FFFFF)
0xFFC01000 = page table 1 (covers 0x00400000-0x007FFFFF)
...etc
Constants
#![allow(unused)]
fn main() {
const PD_RECURSIVE_ADDR: u32 = 0xFFFFF000; // Access the page directory
const PT_BASE_ADDR: u32 = 0xFFC00000; // Access page tables
}
How map_page Uses It
#![allow(unused)]
fn main() {
pub unsafe fn map_page(physaddr: u32, virtualaddr: u32, flags: u32) {
let pdindex = (virtualaddr >> 22) & 0x3FF; // Top 10 bits
let ptindex = (virtualaddr >> 12) & 0x3FF; // Middle 10 bits
// Access the page directory through the recursive mapping
let pd = PD_RECURSIVE_ADDR as *mut u32;
let pd_entry_ptr = pd.add(pdindex as usize);
let pd_entry = ptr::read_volatile(pd_entry_ptr);
// If the page table for this range doesn't exist yet, create it
if !is_set(pd_entry, PDEBit::P) {
let new_pt_frame = allocate_frame()
.expect("Out of memory: cannot allocate page table");
// Install the new page table in the page directory
ptr::write_volatile(pd_entry_ptr, new_pt_frame | PAGE_PRESENT | PAGE_WRITABLE | PAGE_USER);
// Zero the new page table (via the PT_BASE_ADDR window)
let pt_virt = (PT_BASE_ADDR + (pdindex * 0x1000)) as *mut u32;
for i in 0..1024 {
ptr::write_volatile(pt_virt.add(i), 0);
}
}
// Write the mapping into the page table
let pt = (PT_BASE_ADDR + (pdindex * 0x1000)) as *mut u32;
let pt_entry_ptr = pt.add(ptindex as usize);
ptr::write_volatile(pt_entry_ptr, (physaddr & !0xFFF) | (flags & 0xFFF) | PAGE_PRESENT);
// Invalidate TLB entry for this virtual address
flush_tlb(virtualaddr);
}
}
ptr::read_volatile / ptr::write_volatile — Volatile reads and writes prevent the compiler from caching or reordering these memory accesses. The page tables are hardware-visible state, not just ordinary RAM.
Why This Is Clever
Without recursive mapping, map_page would need to:
- Look up the physical address of the relevant page table
- Temporarily map that physical address somewhere accessible
- Write the entry
- Unmap the temporary mapping
With recursive mapping, steps 1-2-4 vanish. The special virtual addresses (PT_BASE_ADDR + pdindex * 0x1000) give direct access to any page table, always.
The Trade-Off
Recursive mapping permanently uses PD[1023], which means the virtual address range 0xFFC00000 – 0xFFFFFFFF (4MB) is reserved for page table access. This region can’t be used for anything else.
For a 4GB address space, sacrificing 4MB for page table access is a reasonable trade.