diff options
| author | salmonik <salmonik@tuta.io> | 2026-07-31 15:51:28 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-07-31 15:51:28 +0200 |
| commit | 5e03409586efc37931f07b40ee0c71a9a9e109f7 (patch) | |
| tree | a1e6e014b94fb410ffce49a7611bf94295165729 /rsnix/src/paging.rs | |
| parent | rsboot: loader file buffers use LoaderData (2) instead of RuntimeData (6) (diff) | |
| download | rs-dos-5e03409586efc37931f07b40ee0c71a9a9e109f7.tar.gz rs-dos-5e03409586efc37931f07b40ee0c71a9a9e109f7.zip | |
rsnix: memory management — free-list heap, bitmap pages, paging adoption
- uefi.rs: full EfiMemoryType constants (0-14) + memory_type_name(); fix
MEMORY_TYPE_LOADER_DATA from 6 to 2 per the UEFI spec
- memory.rs: replace the bump allocator with a first-fit free-list heap
(splitting, coalescing, 8-byte back-pointer for alignment-safe free);
add a bitmap physical page allocator (heap region reserved, page-0
guard, aligned runs); grow the heap from the page allocator on demand
- paging.rs: identity page-table helpers — adopt_tables() rebuilds the
firmware map into our own tables (OVMF maps its tables read-only, so
the firmware tree can't be modified in place) and switches CR3;
map_page/unmap_page/is_mapped with 2 MB/1 GB large-page splitting
- main.rs: boot self-tests for heap, pages, and paging
Diffstat (limited to '')
| -rw-r--r-- | rsnix/src/paging.rs | 356 |
1 files changed, 356 insertions, 0 deletions
diff --git a/rsnix/src/paging.rs b/rsnix/src/paging.rs new file mode 100644 index 0000000..766bde3 --- /dev/null +++ b/rsnix/src/paging.rs | |||
| @@ -0,0 +1,356 @@ | |||
| 1 | // --------------------------------------------------------------------------- | ||
| 2 | // Identity page-table helpers — 4-level paging (x86_64). | ||
| 3 | // | ||
| 4 | // The kernel keeps an identity-mapped 4-level tree (no higher half). At | ||
| 5 | // boot it runs on UEFI's tables; `adopt_tables()` rebuilds that map into | ||
| 6 | // our own tables (UEFI maps its own table pages read-only, so the firmware | ||
| 7 | // tree cannot be modified in place) and switches CR3. After that the | ||
| 8 | // helpers walk our tree to map / unmap / query individual 4 KB pages 1:1. | ||
| 9 | // Page-table pages come from the physical page allocator. Firmware may | ||
| 10 | // have used 2 MB / 1 GB large pages for RAM, so the walk splits large pages | ||
| 11 | // into 4 KB tables on demand. Split tables stay allocated afterwards (no | ||
| 12 | // refcounting) — fine for a kernel that owns all memory. | ||
| 13 | // --------------------------------------------------------------------------- | ||
| 14 | |||
| 15 | use crate::memory::{alloc_pages, free_pages, PAGE_SIZE}; | ||
| 16 | |||
| 17 | pub const PAGE_PRESENT: u64 = 1 << 0; | ||
| 18 | pub const PAGE_WRITABLE: u64 = 1 << 1; | ||
| 19 | pub const PAGE_USER: u64 = 1 << 2; | ||
| 20 | pub const PAGE_PS: u64 = 1 << 7; // large page (2 MB PDE, 1 GB PDPTE) | ||
| 21 | pub const PAGE_NX: u64 = 1 << 63; | ||
| 22 | |||
| 23 | /// Physical address bits in a page-table entry. | ||
| 24 | const PHYS_ADDR_MASK: u64 = 0x000F_FFFF_FFFF_F000; | ||
| 25 | |||
| 26 | const PAGE_2M: usize = 1 << 21; | ||
| 27 | const PAGE_1G: usize = 1 << 30; | ||
| 28 | |||
| 29 | /// Current PML4 root (physical address, identity-mapped). | ||
| 30 | pub fn cr3() -> usize { | ||
| 31 | let val: usize; | ||
| 32 | unsafe { | ||
| 33 | core::arch::asm!("mov {0}, cr3", out(reg) val, options(nostack, preserves_flags)); | ||
| 34 | } | ||
| 35 | val | ||
| 36 | } | ||
| 37 | |||
| 38 | /// Full TLB flush by reloading CR3 (slow but bulletproof; map/unmap are | ||
| 39 | /// rare operations in this kernel). | ||
| 40 | fn flush_tlb_all() { | ||
| 41 | unsafe { | ||
| 42 | let cr3: usize; | ||
| 43 | core::arch::asm!("mov {0}, cr3", out(reg) cr3, options(nostack, preserves_flags)); | ||
| 44 | core::arch::asm!("mov cr3, {0}", in(reg) cr3, options(nostack, preserves_flags)); | ||
| 45 | } | ||
| 46 | } | ||
| 47 | |||
| 48 | /// Rebuild the identity map into our own page tables and switch CR3. | ||
| 49 | /// | ||
| 50 | /// UEFI maps its own page-table pages read-only (R/W clear), so we cannot | ||
| 51 | /// modify the firmware tree in place: map/unmap would #PF. Instead, walk | ||
| 52 | /// the current tables, replicate every present leaf (4 KB / 2 MB / 1 GB) | ||
| 53 | /// into fresh tables from the page allocator, force R/W so the tree stays | ||
| 54 | /// editable, then load the new PML4 into CR3. | ||
| 55 | pub fn adopt_tables() -> bool { | ||
| 56 | unsafe { | ||
| 57 | let old_pml4 = cr3() as *const u64; | ||
| 58 | let Some(new_pml4) = alloc_pages(1) else { | ||
| 59 | return false; | ||
| 60 | }; | ||
| 61 | core::ptr::write_bytes(new_pml4 as *mut u8, 0, PAGE_SIZE); | ||
| 62 | let new_pml4 = new_pml4 as *mut u64; | ||
| 63 | |||
| 64 | for l4 in 0..512 { | ||
| 65 | let l4e = old_pml4.add(l4).read_volatile(); | ||
| 66 | if l4e & PAGE_PRESENT == 0 { | ||
| 67 | continue; | ||
| 68 | } | ||
| 69 | // Replicate this PDPT into a fresh one. | ||
| 70 | let old_pdpt = (l4e & PHYS_ADDR_MASK) as *const u64; | ||
| 71 | let Some(new_pdpt) = alloc_pages(1) else { | ||
| 72 | return false; | ||
| 73 | }; | ||
| 74 | core::ptr::write_bytes(new_pdpt as *mut u8, 0, PAGE_SIZE); | ||
| 75 | let new_pdpt = new_pdpt as *mut u64; | ||
| 76 | *new_pml4.add(l4) = | ||
| 77 | (new_pdpt as u64) | PAGE_PRESENT | PAGE_WRITABLE | (l4e & PAGE_USER); | ||
| 78 | |||
| 79 | for l3 in 0..512 { | ||
| 80 | let pdpte = old_pdpt.add(l3).read_volatile(); | ||
| 81 | if pdpte & PAGE_PRESENT == 0 { | ||
| 82 | continue; | ||
| 83 | } | ||
| 84 | if pdpte & PAGE_PS != 0 { | ||
| 85 | // 1 GB large page — copy the leaf, force writable. | ||
| 86 | *new_pdpt.add(l3) = pdpte | PAGE_WRITABLE; | ||
| 87 | continue; | ||
| 88 | } | ||
| 89 | let old_pd = (pdpte & PHYS_ADDR_MASK) as *const u64; | ||
| 90 | let Some(new_pd) = alloc_pages(1) else { | ||
| 91 | return false; | ||
| 92 | }; | ||
| 93 | core::ptr::write_bytes(new_pd as *mut u8, 0, PAGE_SIZE); | ||
| 94 | let new_pd = new_pd as *mut u64; | ||
| 95 | *new_pdpt.add(l3) = | ||
| 96 | (new_pd as u64) | PAGE_PRESENT | PAGE_WRITABLE | (pdpte & PAGE_USER); | ||
| 97 | |||
| 98 | for l2 in 0..512 { | ||
| 99 | let pde = old_pd.add(l2).read_volatile(); | ||
| 100 | if pde & PAGE_PRESENT == 0 { | ||
| 101 | continue; | ||
| 102 | } | ||
| 103 | if pde & PAGE_PS != 0 { | ||
| 104 | // 2 MB large page — copy the leaf, force writable. | ||
| 105 | *new_pd.add(l2) = pde | PAGE_WRITABLE; | ||
| 106 | continue; | ||
| 107 | } | ||
| 108 | let old_pt = (pde & PHYS_ADDR_MASK) as *const u64; | ||
| 109 | let Some(new_pt) = alloc_pages(1) else { | ||
| 110 | return false; | ||
| 111 | }; | ||
| 112 | core::ptr::write_bytes(new_pt as *mut u8, 0, PAGE_SIZE); | ||
| 113 | let new_pt = new_pt as *mut u64; | ||
| 114 | *new_pd.add(l2) = | ||
| 115 | (new_pt as u64) | PAGE_PRESENT | PAGE_WRITABLE | (pde & PAGE_USER); | ||
| 116 | |||
| 117 | for l1 in 0..512 { | ||
| 118 | let pte = old_pt.add(l1).read_volatile(); | ||
| 119 | if pte & PAGE_PRESENT != 0 { | ||
| 120 | *new_pt.add(l1) = pte | PAGE_WRITABLE; | ||
| 121 | } | ||
| 122 | } | ||
| 123 | } | ||
| 124 | } | ||
| 125 | } | ||
| 126 | |||
| 127 | // Switch to the new tables (CR3 reload flushes the TLB). | ||
| 128 | let new_cr3 = new_pml4 as usize; | ||
| 129 | core::arch::asm!("mov cr3, {0}", in(reg) new_cr3, options(nostack, preserves_flags)); | ||
| 130 | true | ||
| 131 | } | ||
| 132 | } | ||
| 133 | |||
| 134 | /// Ensure the PDPT entry points at a page-directory table, splitting a | ||
| 135 | /// 1 GB large page at `base_1g` if present. Returns the PD pointer. | ||
| 136 | unsafe fn ensure_pd(pdpte: &mut u64, base_1g: usize) -> Option<*mut u64> { | ||
| 137 | unsafe { | ||
| 138 | if *pdpte & PAGE_PRESENT == 0 { | ||
| 139 | let page = alloc_pages(1)? as *mut u64; | ||
| 140 | core::ptr::write_bytes(page as *mut u8, 0, PAGE_SIZE); | ||
| 141 | *pdpte = (page as u64) | PAGE_PRESENT | PAGE_WRITABLE; | ||
| 142 | return Some(page); | ||
| 143 | } | ||
| 144 | if *pdpte & PAGE_PS != 0 { | ||
| 145 | // Split the 1 GB page into 512 × 2 MB pages. | ||
| 146 | let pd = alloc_pages(1)? as *mut u64; | ||
| 147 | core::ptr::write_bytes(pd as *mut u8, 0, PAGE_SIZE); | ||
| 148 | let inherit = *pdpte & (PAGE_USER | PAGE_NX); | ||
| 149 | for i in 0..512 { | ||
| 150 | let p = base_1g + i * PAGE_2M; | ||
| 151 | *pd.add(i) = (p as u64) | PAGE_PRESENT | PAGE_WRITABLE | PAGE_PS | inherit; | ||
| 152 | } | ||
| 153 | *pdpte = (pd as u64) | PAGE_PRESENT | PAGE_WRITABLE | (*pdpte & PAGE_USER); | ||
| 154 | return Some(pd); | ||
| 155 | } | ||
| 156 | Some(((*pdpte & PHYS_ADDR_MASK) as usize) as *mut u64) | ||
| 157 | } | ||
| 158 | } | ||
| 159 | |||
| 160 | /// Ensure the PD entry points at a page table, splitting a 2 MB large page | ||
| 161 | /// at `base_2m` if present. Returns the PT pointer. | ||
| 162 | unsafe fn ensure_pt(pde: &mut u64, base_2m: usize) -> Option<*mut u64> { | ||
| 163 | unsafe { | ||
| 164 | if *pde & PAGE_PRESENT == 0 { | ||
| 165 | let page = alloc_pages(1)? as *mut u64; | ||
| 166 | core::ptr::write_bytes(page as *mut u8, 0, PAGE_SIZE); | ||
| 167 | *pde = (page as u64) | PAGE_PRESENT | PAGE_WRITABLE; | ||
| 168 | return Some(page); | ||
| 169 | } | ||
| 170 | if *pde & PAGE_PS != 0 { | ||
| 171 | // Split the 2 MB page into 512 × 4 KB pages. | ||
| 172 | let pt = alloc_pages(1)? as *mut u64; | ||
| 173 | core::ptr::write_bytes(pt as *mut u8, 0, PAGE_SIZE); | ||
| 174 | let inherit = *pde & (PAGE_USER | PAGE_NX); | ||
| 175 | for i in 0..512 { | ||
| 176 | let p = base_2m + i * PAGE_SIZE; | ||
| 177 | *pt.add(i) = (p as u64) | PAGE_PRESENT | PAGE_WRITABLE | inherit; | ||
| 178 | } | ||
| 179 | *pde = (pt as u64) | PAGE_PRESENT | PAGE_WRITABLE | (*pde & PAGE_USER); | ||
| 180 | return Some(pt); | ||
| 181 | } | ||
| 182 | Some(((*pde & PHYS_ADDR_MASK) as usize) as *mut u64) | ||
| 183 | } | ||
| 184 | } | ||
| 185 | |||
| 186 | /// Identity-map the physical page at `phys` (must be 4 KB aligned). | ||
| 187 | /// `flags` adds to PRESENT (e.g. PAGE_WRITABLE, PAGE_USER). | ||
| 188 | pub fn map_page(phys: usize, flags: u64) -> bool { | ||
| 189 | if !phys.is_multiple_of(PAGE_SIZE) { | ||
| 190 | return false; | ||
| 191 | } | ||
| 192 | unsafe { | ||
| 193 | let pml4 = cr3() as *mut u64; | ||
| 194 | let l4 = (phys >> 39) & 0x1FF; | ||
| 195 | let l3 = (phys >> 30) & 0x1FF; | ||
| 196 | let l2 = (phys >> 21) & 0x1FF; | ||
| 197 | let l1 = (phys >> 12) & 0x1FF; | ||
| 198 | |||
| 199 | let pml4e = &mut *pml4.add(l4); | ||
| 200 | if *pml4e & PAGE_PRESENT == 0 { | ||
| 201 | let Some(page) = alloc_pages(1) else { | ||
| 202 | return false; | ||
| 203 | }; | ||
| 204 | core::ptr::write_bytes(page as *mut u8, 0, PAGE_SIZE); | ||
| 205 | *pml4e = (page as u64) | PAGE_PRESENT | PAGE_WRITABLE; | ||
| 206 | } | ||
| 207 | // PML4 entries are never large pages. | ||
| 208 | |||
| 209 | let base_1g = phys & !(PAGE_1G - 1); | ||
| 210 | let pdpte = &mut *(((*pml4e & PHYS_ADDR_MASK) as usize) as *mut u64).add(l3); | ||
| 211 | let Some(pd) = ensure_pd(pdpte, base_1g) else { | ||
| 212 | return false; | ||
| 213 | }; | ||
| 214 | |||
| 215 | let base_2m = phys & !(PAGE_2M - 1); | ||
| 216 | let pde = &mut *pd.add(l2); | ||
| 217 | let Some(pt) = ensure_pt(pde, base_2m) else { | ||
| 218 | return false; | ||
| 219 | }; | ||
| 220 | |||
| 221 | let pte = &mut *pt.add(l1); | ||
| 222 | *pte = (phys as u64) | flags | PAGE_PRESENT; | ||
| 223 | flush_tlb_all(); | ||
| 224 | true | ||
| 225 | } | ||
| 226 | } | ||
| 227 | |||
| 228 | /// Identity-map `count` consecutive pages starting at `phys`. | ||
| 229 | pub fn map_range(phys: usize, count: usize, flags: u64) -> bool { | ||
| 230 | for i in 0..count { | ||
| 231 | if !map_page(phys + i * PAGE_SIZE, flags) { | ||
| 232 | return false; | ||
| 233 | } | ||
| 234 | } | ||
| 235 | true | ||
| 236 | } | ||
| 237 | |||
| 238 | /// Unmap the 4 KB page at `phys`. Splits large pages as needed so only | ||
| 239 | /// this one page is removed. Returns false if it wasn't mapped. | ||
| 240 | pub fn unmap_page(phys: usize) -> bool { | ||
| 241 | if !phys.is_multiple_of(PAGE_SIZE) { | ||
| 242 | return false; | ||
| 243 | } | ||
| 244 | unsafe { | ||
| 245 | let pml4 = cr3() as *mut u64; | ||
| 246 | let l4 = (phys >> 39) & 0x1FF; | ||
| 247 | let l3 = (phys >> 30) & 0x1FF; | ||
| 248 | let l2 = (phys >> 21) & 0x1FF; | ||
| 249 | let l1 = (phys >> 12) & 0x1FF; | ||
| 250 | |||
| 251 | let pml4e = &mut *pml4.add(l4); | ||
| 252 | if *pml4e & PAGE_PRESENT == 0 { | ||
| 253 | return false; | ||
| 254 | } | ||
| 255 | let base_1g = phys & !(PAGE_1G - 1); | ||
| 256 | let pdpte = &mut *(((*pml4e & PHYS_ADDR_MASK) as usize) as *mut u64).add(l3); | ||
| 257 | if *pdpte & PAGE_PRESENT == 0 { | ||
| 258 | return false; | ||
| 259 | } | ||
| 260 | let Some(pd) = ensure_pd(pdpte, base_1g) else { | ||
| 261 | return false; | ||
| 262 | }; | ||
| 263 | let base_2m = phys & !(PAGE_2M - 1); | ||
| 264 | let pde = &mut *pd.add(l2); | ||
| 265 | if *pde & PAGE_PRESENT == 0 { | ||
| 266 | return false; | ||
| 267 | } | ||
| 268 | let Some(pt) = ensure_pt(pde, base_2m) else { | ||
| 269 | return false; | ||
| 270 | }; | ||
| 271 | let pte = &mut *pt.add(l1); | ||
| 272 | if *pte & PAGE_PRESENT == 0 { | ||
| 273 | return false; | ||
| 274 | } | ||
| 275 | *pte = 0; | ||
| 276 | flush_tlb_all(); | ||
| 277 | true | ||
| 278 | } | ||
| 279 | } | ||
| 280 | |||
| 281 | /// Is the page at `phys` mapped (present at any level, incl. large pages)? | ||
| 282 | pub fn is_mapped(phys: usize) -> bool { | ||
| 283 | unsafe { | ||
| 284 | let pml4 = cr3() as *const u64; | ||
| 285 | let l4 = (phys >> 39) & 0x1FF; | ||
| 286 | let l3 = (phys >> 30) & 0x1FF; | ||
| 287 | let l2 = (phys >> 21) & 0x1FF; | ||
| 288 | let l1 = (phys >> 12) & 0x1FF; | ||
| 289 | |||
| 290 | let pml4e = *pml4.add(l4); | ||
| 291 | if pml4e & PAGE_PRESENT == 0 { | ||
| 292 | return false; | ||
| 293 | } | ||
| 294 | let pdpte = *((pml4e & PHYS_ADDR_MASK) as *const u64).add(l3); | ||
| 295 | if pdpte & PAGE_PRESENT == 0 { | ||
| 296 | return false; | ||
| 297 | } | ||
| 298 | if pdpte & PAGE_PS != 0 { | ||
| 299 | return true; // 1 GB page covers phys | ||
| 300 | } | ||
| 301 | let pde = *((pdpte & PHYS_ADDR_MASK) as *const u64).add(l2); | ||
| 302 | if pde & PAGE_PRESENT == 0 { | ||
| 303 | return false; | ||
| 304 | } | ||
| 305 | if pde & PAGE_PS != 0 { | ||
| 306 | return true; // 2 MB page covers phys | ||
| 307 | } | ||
| 308 | let pte = *((pde & PHYS_ADDR_MASK) as *const u64).add(l1); | ||
| 309 | pte & PAGE_PRESENT != 0 | ||
| 310 | } | ||
| 311 | } | ||
| 312 | |||
| 313 | /// Smoke test: allocate two physical pages, unmap one, verify the mapping | ||
| 314 | /// state, remap it, verify data still works, and free both. | ||
| 315 | pub fn self_test() -> bool { | ||
| 316 | let Some(p1) = alloc_pages(1) else { | ||
| 317 | return false; | ||
| 318 | }; | ||
| 319 | let Some(p2) = alloc_pages(1) else { | ||
| 320 | return false; | ||
| 321 | }; | ||
| 322 | |||
| 323 | // Both pages must already be identity-mapped (they're RAM). | ||
| 324 | if !is_mapped(p1) || !is_mapped(p2) { | ||
| 325 | return false; | ||
| 326 | } | ||
| 327 | |||
| 328 | // Unmap p1 — only p1 should disappear. | ||
| 329 | if !unmap_page(p1) { | ||
| 330 | return false; | ||
| 331 | } | ||
| 332 | if is_mapped(p1) { | ||
| 333 | return false; | ||
| 334 | } | ||
| 335 | if !is_mapped(p2) { | ||
| 336 | return false; | ||
| 337 | } | ||
| 338 | |||
| 339 | // Remap and verify we can write through the mapping. | ||
| 340 | if !map_page(p1, PAGE_WRITABLE) { | ||
| 341 | return false; | ||
| 342 | } | ||
| 343 | if !is_mapped(p1) { | ||
| 344 | return false; | ||
| 345 | } | ||
| 346 | unsafe { | ||
| 347 | core::ptr::write_volatile(p1 as *mut u8, 0xCD); | ||
| 348 | if core::ptr::read_volatile(p1 as *const u8) != 0xCD { | ||
| 349 | return false; | ||
| 350 | } | ||
| 351 | } | ||
| 352 | |||
| 353 | free_pages(p1, 1); | ||
| 354 | free_pages(p2, 1); | ||
| 355 | true | ||
| 356 | } | ||
