Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Look up the physical address of the relevant page table
  2. Temporarily map that physical address somewhere accessible
  3. Write the entry
  4. 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.