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 | |
| 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/lib.rs | 1 | ||||
| -rw-r--r-- | rsnix/src/memory.rs | 663 | ||||
| -rw-r--r-- | rsnix/src/paging.rs | 356 | ||||
| -rw-r--r-- | rsnix/src/uefi.rs | 40 |
4 files changed, 1018 insertions, 42 deletions
diff --git a/rsnix/src/lib.rs b/rsnix/src/lib.rs index 0bc0cb9..d2dacf0 100644 --- a/rsnix/src/lib.rs +++ b/rsnix/src/lib.rs | |||
| @@ -6,6 +6,7 @@ pub mod font; | |||
| 6 | pub mod gop; | 6 | pub mod gop; |
| 7 | pub mod interrupts; | 7 | pub mod interrupts; |
| 8 | pub mod memory; | 8 | pub mod memory; |
| 9 | pub mod paging; | ||
| 9 | pub mod ps2; | 10 | pub mod ps2; |
| 10 | pub mod serial; | 11 | pub mod serial; |
| 11 | pub mod uefi; | 12 | pub mod uefi; |
diff --git a/rsnix/src/memory.rs b/rsnix/src/memory.rs index 39fd88d..d33c781 100644 --- a/rsnix/src/memory.rs +++ b/rsnix/src/memory.rs | |||
| @@ -16,46 +16,247 @@ static mut MAP_KEY: usize = 0; | |||
| 16 | static mut MAP_WORK_BUF: [u8; 65536] = [0u8; 65536]; | 16 | static mut MAP_WORK_BUF: [u8; 65536] = [0u8; 65536]; |
| 17 | 17 | ||
| 18 | // --------------------------------------------------------------------------- | 18 | // --------------------------------------------------------------------------- |
| 19 | // Bump allocator — no deallocation, just pointer advancement. | 19 | // Free-list heap allocator — first fit, splitting, coalescing. |
| 20 | // Used as Rust's #[global_allocator] once the heap region is set up. | 20 | // Used as Rust's #[global_allocator] once the heap region is set up. |
| 21 | // | ||
| 22 | // Every block (free or allocated) starts with a 16-byte header: | ||
| 23 | // | ||
| 24 | // [ size: usize ][ free: usize ] | ||
| 25 | // | ||
| 26 | // - `size` covers the whole block including the header. | ||
| 27 | // - `free` is 1 for free blocks, 0 for allocated blocks. | ||
| 28 | // | ||
| 29 | // Free blocks are linked into one address-sorted doubly-linked list; the | ||
| 30 | // prev/next links live in the block's payload (free space is unused, so | ||
| 31 | // it's free real estate for the links). Allocating walks the list for the | ||
| 32 | // first fit, splits the remainder into a fresh free block, and records the | ||
| 33 | // block address in an 8-byte back-pointer immediately before the returned | ||
| 34 | // payload — needed because alignment can leave a gap between the block | ||
| 35 | // header and the payload. Freeing reads the back-pointer to find its | ||
| 36 | // block, marks it free, re-inserts it in address order, and coalesces with | ||
| 37 | // any adjacent free neighbours. | ||
| 38 | // | ||
| 39 | // All sizes are rounded to 16 bytes so block boundaries stay 16-aligned | ||
| 40 | // and every payload is at least 16-byte aligned. | ||
| 21 | // --------------------------------------------------------------------------- | 41 | // --------------------------------------------------------------------------- |
| 22 | struct BumpAllocator; | ||
| 23 | 42 | ||
| 24 | unsafe impl Sync for BumpAllocator {} | 43 | const HEADER_SIZE: usize = 16; // size + free flag |
| 44 | const MINI_SIZE: usize = 8; // back-pointer before the payload | ||
| 45 | const MIN_BLOCK: usize = 32; // smallest block we will create (header + links) | ||
| 46 | const MIN_ALIGN: usize = 16; | ||
| 25 | 47 | ||
| 26 | unsafe impl GlobalAlloc for BumpAllocator { | 48 | /// Block header — first 16 bytes of every block. |
| 49 | #[repr(C)] | ||
| 50 | struct Header { | ||
| 51 | size: usize, | ||
| 52 | free: usize, | ||
| 53 | } | ||
| 54 | |||
| 55 | impl Header { | ||
| 56 | /// Where the free-list links live (only meaningful while free). | ||
| 57 | #[inline] | ||
| 58 | fn links(&self) -> *mut FreeLinks { | ||
| 59 | unsafe { (self as *const Header as *mut u8).add(HEADER_SIZE) as *mut FreeLinks } | ||
| 60 | } | ||
| 61 | } | ||
| 62 | |||
| 63 | /// Free-list links, stored in the payload of free blocks only. | ||
| 64 | #[repr(C)] | ||
| 65 | struct FreeLinks { | ||
| 66 | prev: *mut Header, | ||
| 67 | next: *mut Header, | ||
| 68 | } | ||
| 69 | |||
| 70 | /// Head of the address-sorted free list (lowest address first). | ||
| 71 | static mut FREE_LIST: *mut Header = ptr::null_mut(); | ||
| 72 | |||
| 73 | /// Heap region bounds (set by `init_heap`). | ||
| 74 | static mut HEAP_START: usize = 0; | ||
| 75 | static mut HEAP_END: usize = 0; | ||
| 76 | /// Total heap capacity — the original region plus pages added by growth. | ||
| 77 | static mut HEAP_CAPACITY: usize = 0; | ||
| 78 | |||
| 79 | /// Minimum pages `grow_heap` takes from the page allocator per growth. | ||
| 80 | const HEAP_GROW_PAGES: usize = 64; | ||
| 81 | |||
| 82 | #[inline] | ||
| 83 | fn align_up(x: usize, a: usize) -> usize { | ||
| 84 | (x + a - 1) & !(a - 1) | ||
| 85 | } | ||
| 86 | |||
| 87 | /// Insert `block` into the address-sorted free list. | ||
| 88 | /// Caller guarantees `block` is not already in the list. | ||
| 89 | unsafe fn free_list_insert(block: *mut Header) { | ||
| 90 | let addr = block as usize; | ||
| 91 | let mut prev: *mut Header = ptr::null_mut(); | ||
| 92 | let mut cur = FREE_LIST; | ||
| 93 | while !cur.is_null() && (cur as usize) < addr { | ||
| 94 | prev = cur; | ||
| 95 | cur = unsafe { (*(*cur).links()).next }; | ||
| 96 | } | ||
| 97 | unsafe { | ||
| 98 | let links = (*block).links(); | ||
| 99 | (*links).prev = prev; | ||
| 100 | (*links).next = cur; | ||
| 101 | if prev.is_null() { | ||
| 102 | FREE_LIST = block; | ||
| 103 | } else { | ||
| 104 | (*(*prev).links()).next = block; | ||
| 105 | } | ||
| 106 | if !cur.is_null() { | ||
| 107 | (*(*cur).links()).prev = block; | ||
| 108 | } | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 112 | /// Unlink `block` from the free list. | ||
| 113 | unsafe fn free_list_remove(block: *mut Header) { | ||
| 114 | unsafe { | ||
| 115 | let links = (*block).links(); | ||
| 116 | let prev = (*links).prev; | ||
| 117 | let next = (*links).next; | ||
| 118 | if prev.is_null() { | ||
| 119 | FREE_LIST = next; | ||
| 120 | } else { | ||
| 121 | (*(*prev).links()).next = next; | ||
| 122 | } | ||
| 123 | if !next.is_null() { | ||
| 124 | (*(*next).links()).prev = prev; | ||
| 125 | } | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 129 | /// Merge `block` with any adjacent free neighbours (both directions). | ||
| 130 | /// `block` must already be in the free list. | ||
| 131 | unsafe fn coalesce(block: *mut Header) { | ||
| 132 | unsafe { | ||
| 133 | // Absorb the next free block if it sits right after us. | ||
| 134 | let next = (*(*block).links()).next; | ||
| 135 | if !next.is_null() && (block as usize) + (*block).size == next as usize { | ||
| 136 | (*block).size += (*next).size; | ||
| 137 | let next_links = (*next).links(); | ||
| 138 | (*(*block).links()).next = (*next_links).next; | ||
| 139 | if !(*next_links).next.is_null() { | ||
| 140 | (*(*(*next_links).next).links()).prev = block; | ||
| 141 | } | ||
| 142 | } | ||
| 143 | // Be absorbed by the previous free block if it ends right before us. | ||
| 144 | let prev = (*(*block).links()).prev; | ||
| 145 | if !prev.is_null() && (prev as usize) + (*prev).size == block as usize { | ||
| 146 | (*prev).size += (*block).size; | ||
| 147 | let block_links = (*block).links(); | ||
| 148 | (*(*prev).links()).next = (*block_links).next; | ||
| 149 | if !(*block_links).next.is_null() { | ||
| 150 | (*(*(*block_links).next).links()).prev = prev; | ||
| 151 | } | ||
| 152 | } | ||
| 153 | } | ||
| 154 | } | ||
| 155 | |||
| 156 | /// First-fit search: find the first block big enough for `size` aligned to | ||
| 157 | /// `align`, split it, and return the aligned payload (back-pointer written). | ||
| 158 | /// Returns null when no block fits. | ||
| 159 | unsafe fn alloc_from_free_list(size: usize, align: usize) -> *mut u8 { | ||
| 160 | unsafe { | ||
| 161 | let mut cur = FREE_LIST; | ||
| 162 | while !cur.is_null() { | ||
| 163 | let block = cur; | ||
| 164 | let block_size = (*block).size; | ||
| 165 | // Payload sits after the header + back-pointer, aligned up. | ||
| 166 | let payload = align_up(block as usize + HEADER_SIZE + MINI_SIZE, align); | ||
| 167 | if payload + size <= (block as usize) + block_size { | ||
| 168 | let consumed = payload + size - (block as usize); | ||
| 169 | let remainder = block_size - consumed; | ||
| 170 | |||
| 171 | free_list_remove(block); | ||
| 172 | |||
| 173 | if remainder >= MIN_BLOCK { | ||
| 174 | // Split: carve the tail into a fresh free block. | ||
| 175 | let rem = (payload + size) as *mut Header; | ||
| 176 | (*rem).size = remainder; | ||
| 177 | (*rem).free = 1; | ||
| 178 | free_list_insert(rem); | ||
| 179 | (*block).size = consumed; | ||
| 180 | } | ||
| 181 | (*block).free = 0; | ||
| 182 | |||
| 183 | // Back-pointer so `free` can find the block even when | ||
| 184 | // alignment left a gap before the payload. | ||
| 185 | *((payload as *mut usize).sub(1)) = block as usize; | ||
| 186 | return payload as *mut u8; | ||
| 187 | } | ||
| 188 | cur = (*(*block).links()).next; | ||
| 189 | } | ||
| 190 | ptr::null_mut() | ||
| 191 | } | ||
| 192 | } | ||
| 193 | |||
| 194 | /// Expand the heap by taking `pages` contiguous physical pages from the | ||
| 195 | /// page allocator and adding them to the free list as one free block. | ||
| 196 | /// Returns false when the page allocator can't provide them. | ||
| 197 | unsafe fn grow_heap(pages: usize) -> bool { | ||
| 198 | unsafe { | ||
| 199 | if let Some(addr) = alloc_pages(pages) { | ||
| 200 | let block = addr as *mut Header; | ||
| 201 | (*block).size = pages * PAGE_SIZE; | ||
| 202 | (*block).free = 1; | ||
| 203 | free_list_insert(block); | ||
| 204 | coalesce(block); | ||
| 205 | HEAP_CAPACITY += pages * PAGE_SIZE; | ||
| 206 | true | ||
| 207 | } else { | ||
| 208 | false | ||
| 209 | } | ||
| 210 | } | ||
| 211 | } | ||
| 212 | |||
| 213 | /// The kernel heap allocator. | ||
| 214 | struct Allocator; | ||
| 215 | |||
| 216 | unsafe impl Sync for Allocator {} | ||
| 217 | |||
| 218 | unsafe impl GlobalAlloc for Allocator { | ||
| 27 | unsafe fn alloc(&self, layout: Layout) -> *mut u8 { | 219 | unsafe fn alloc(&self, layout: Layout) -> *mut u8 { |
| 28 | let align = layout.align().max(16); | 220 | let size = align_up(layout.size().max(1), MIN_ALIGN); |
| 29 | let size = layout.size(); | 221 | let align = layout.align().max(MIN_ALIGN); |
| 30 | unsafe { | 222 | unsafe { |
| 31 | let a = (HEAP_CURSOR + align - 1) & !(align - 1); | 223 | // First fit; when the free list is exhausted, grow the heap |
| 32 | let e = a + size; | 224 | // from the page allocator and retry. Growth is proportional |
| 33 | if e > HEAP_END || e < a { | 225 | // to the request (min HEAP_GROW_PAGES), so large allocations |
| 34 | return ptr::null_mut(); | 226 | // converge quickly. |
| 227 | loop { | ||
| 228 | let p = alloc_from_free_list(size, align); | ||
| 229 | if !p.is_null() { | ||
| 230 | return p; | ||
| 231 | } | ||
| 232 | let need = (size / PAGE_SIZE + 1).max(HEAP_GROW_PAGES); | ||
| 233 | if !grow_heap(need) { | ||
| 234 | return ptr::null_mut(); | ||
| 235 | } | ||
| 35 | } | 236 | } |
| 36 | HEAP_CURSOR = e; | ||
| 37 | a as *mut u8 | ||
| 38 | } | 237 | } |
| 39 | } | 238 | } |
| 40 | 239 | ||
| 41 | unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) { | 240 | unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) { |
| 42 | // Bump allocator — cannot free individual allocations. | 241 | if ptr.is_null() { |
| 43 | // Memory is reclaimed on reboot. Good enough for now. | 242 | return; |
| 243 | } | ||
| 44 | unsafe { | 244 | unsafe { |
| 45 | core::ptr::read_volatile(&_ptr); | 245 | let block = *((ptr as *const usize).sub(1)) as *mut Header; |
| 246 | debug_assert!( | ||
| 247 | (*block).free == 0, | ||
| 248 | "heap: double free at {:#x}", | ||
| 249 | ptr as usize | ||
| 250 | ); | ||
| 251 | (*block).free = 1; | ||
| 252 | free_list_insert(block); | ||
| 253 | coalesce(block); | ||
| 46 | } | 254 | } |
| 47 | let _ = _layout; | ||
| 48 | } | 255 | } |
| 49 | } | 256 | } |
| 50 | 257 | ||
| 51 | #[global_allocator] | 258 | #[global_allocator] |
| 52 | static ALLOCATOR: BumpAllocator = BumpAllocator; | 259 | static ALLOCATOR: Allocator = Allocator; |
| 53 | |||
| 54 | /// Heap bump allocator bounds (`static mut` accessed only inside | ||
| 55 | /// `GlobalAlloc` impl, which is already `unsafe`). | ||
| 56 | static mut HEAP_START: usize = 0; | ||
| 57 | static mut HEAP_END: usize = 0; | ||
| 58 | static mut HEAP_CURSOR: usize = 0; | ||
| 59 | 260 | ||
| 60 | /// Capture the UEFI memory map into the static buffer. Uses a pre-allocated | 261 | /// Capture the UEFI memory map into the static buffer. Uses a pre-allocated |
| 61 | /// .bss working buffer so no pool allocation (and thus no memory-map change) | 262 | /// .bss working buffer so no pool allocation (and thus no memory-map change) |
| @@ -131,7 +332,7 @@ pub fn capture_memory_map(bs: &BootServices) -> Result<usize, Status> { | |||
| 131 | // Do NOT free anything — the static buffer stays. Map key is still | 332 | // Do NOT free anything — the static buffer stays. Map key is still |
| 132 | // valid because no pool allocation/free happened after GetMemoryMap. | 333 | // valid because no pool allocation/free happened after GetMemoryMap. |
| 133 | 334 | ||
| 134 | init_bump_allocator(); | 335 | init_heap(); |
| 135 | Ok(map_key) | 336 | Ok(map_key) |
| 136 | } | 337 | } |
| 137 | 338 | ||
| @@ -146,7 +347,7 @@ pub fn refresh_and_exit(bs: &BootServices, image_handle: Handle) -> Result<usize | |||
| 146 | } | 347 | } |
| 147 | } | 348 | } |
| 148 | 349 | ||
| 149 | fn init_bump_allocator() { | 350 | fn init_heap() { |
| 150 | unsafe { | 351 | unsafe { |
| 151 | let mut best_start = 0usize; | 352 | let mut best_start = 0usize; |
| 152 | let mut best_size = 0usize; | 353 | let mut best_size = 0usize; |
| @@ -163,10 +364,18 @@ fn init_bump_allocator() { | |||
| 163 | if best_size > 2 * 1024 * 1024 { | 364 | if best_size > 2 * 1024 * 1024 { |
| 164 | let s = best_start + 2 * 1024 * 1024; | 365 | let s = best_start + 2 * 1024 * 1024; |
| 165 | let e = best_start + best_size; | 366 | let e = best_start + best_size; |
| 166 | if s < e { | 367 | if s < e && e - s >= MIN_BLOCK { |
| 167 | HEAP_START = s; | 368 | HEAP_START = s; |
| 168 | HEAP_END = e; | 369 | HEAP_END = e; |
| 169 | HEAP_CURSOR = s; | 370 | HEAP_CAPACITY = e - s; |
| 371 | // Seed the free list with one block covering the whole heap. | ||
| 372 | let block = s as *mut Header; | ||
| 373 | (*block).size = e - s; | ||
| 374 | (*block).free = 1; | ||
| 375 | let links = (*block).links(); | ||
| 376 | (*links).prev = ptr::null_mut(); | ||
| 377 | (*links).next = ptr::null_mut(); | ||
| 378 | FREE_LIST = block; | ||
| 170 | } | 379 | } |
| 171 | } | 380 | } |
| 172 | } | 381 | } |
| @@ -174,15 +383,8 @@ fn init_bump_allocator() { | |||
| 174 | 383 | ||
| 175 | #[allow(dead_code)] | 384 | #[allow(dead_code)] |
| 176 | pub fn allocate(size: usize) -> *mut u8 { | 385 | pub fn allocate(size: usize) -> *mut u8 { |
| 177 | unsafe { | 386 | let layout = unsafe { Layout::from_size_align_unchecked(size.max(1), MIN_ALIGN) }; |
| 178 | let a = (HEAP_CURSOR + 15) & !15; | 387 | unsafe { ALLOCATOR.alloc(layout) } |
| 179 | let e = a + size; | ||
| 180 | if e > HEAP_END || e < a { | ||
| 181 | return ptr::null_mut(); | ||
| 182 | } | ||
| 183 | HEAP_CURSOR = e; | ||
| 184 | a as *mut u8 | ||
| 185 | } | ||
| 186 | } | 388 | } |
| 187 | 389 | ||
| 188 | #[allow(dead_code)] | 390 | #[allow(dead_code)] |
| @@ -194,3 +396,390 @@ pub fn get_map() -> (&'static [MemoryDescriptor], usize) { | |||
| 194 | ) | 396 | ) |
| 195 | } | 397 | } |
| 196 | } | 398 | } |
| 399 | |||
| 400 | // --------------------------------------------------------------------------- | ||
| 401 | // Heap statistics + self-test | ||
| 402 | // --------------------------------------------------------------------------- | ||
| 403 | |||
| 404 | /// Snapshot of current heap usage. | ||
| 405 | #[derive(Debug, Clone, Copy)] | ||
| 406 | pub struct HeapStats { | ||
| 407 | pub heap_start: usize, | ||
| 408 | pub heap_end: usize, | ||
| 409 | pub heap_size: usize, | ||
| 410 | pub free_bytes: usize, | ||
| 411 | pub free_blocks: usize, | ||
| 412 | } | ||
| 413 | |||
| 414 | /// Current heap usage — walks the free list (cheap, it stays short). | ||
| 415 | pub fn heap_stats() -> HeapStats { | ||
| 416 | unsafe { | ||
| 417 | let mut free_bytes = 0usize; | ||
| 418 | let mut free_blocks = 0usize; | ||
| 419 | let mut cur = FREE_LIST; | ||
| 420 | while !cur.is_null() { | ||
| 421 | free_bytes += (*cur).size; | ||
| 422 | free_blocks += 1; | ||
| 423 | cur = (*(*cur).links()).next; | ||
| 424 | } | ||
| 425 | HeapStats { | ||
| 426 | heap_start: HEAP_START, | ||
| 427 | heap_end: HEAP_END, | ||
| 428 | heap_size: HEAP_CAPACITY, | ||
| 429 | free_bytes, | ||
| 430 | free_blocks, | ||
| 431 | } | ||
| 432 | } | ||
| 433 | } | ||
| 434 | |||
| 435 | /// Allocator smoke test — exercises alloc/free/coalesce without touching | ||
| 436 | /// the heap itself (fixed-size stack arrays only). Returns true on success. | ||
| 437 | pub fn self_test() -> bool { | ||
| 438 | unsafe { | ||
| 439 | const N: usize = 128; | ||
| 440 | // (ptr, size, align) triplets — fixed array on the stack. | ||
| 441 | let mut blocks: [(usize, usize, usize); N] = [(0, 0, 0); N]; | ||
| 442 | |||
| 443 | // Allocate a mixed batch: sizes 1..2048, aligns 16..2048. | ||
| 444 | let mut seed: u64 = 0x9E37_79B9_7F4A_7C15; | ||
| 445 | let mut n = 0usize; | ||
| 446 | for i in 0..N { | ||
| 447 | let size = ((seed as usize) % 2048) + 1; | ||
| 448 | seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1); | ||
| 449 | let align = 16usize << (seed as usize % 8); | ||
| 450 | seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1); | ||
| 451 | let layout = Layout::from_size_align(size, align).unwrap(); | ||
| 452 | let p = ALLOCATOR.alloc(layout); | ||
| 453 | if p.is_null() { | ||
| 454 | break; // heap exhausted — stop | ||
| 455 | } | ||
| 456 | if !(p as usize).is_multiple_of(align) { | ||
| 457 | return false; // alignment violated | ||
| 458 | } | ||
| 459 | core::ptr::write_bytes(p, i as u8 + 1, size); // scribble | ||
| 460 | blocks[n] = (p as usize, size, align); | ||
| 461 | n += 1; | ||
| 462 | } | ||
| 463 | if n < 16 { | ||
| 464 | return false; // should easily fit in the heap | ||
| 465 | } | ||
| 466 | |||
| 467 | // Free every third block. | ||
| 468 | let mut i = 0; | ||
| 469 | while i < n { | ||
| 470 | let (ptr, size, align) = blocks[i]; | ||
| 471 | let layout = Layout::from_size_align(size, align).unwrap(); | ||
| 472 | ALLOCATOR.dealloc(ptr as *mut u8, layout); | ||
| 473 | blocks[i] = (0, 0, 0); | ||
| 474 | i += 3; | ||
| 475 | } | ||
| 476 | |||
| 477 | // Reallocate the freed slots — space must be reused. | ||
| 478 | for i in 0..n { | ||
| 479 | if blocks[i].0 != 0 { | ||
| 480 | continue; // still live | ||
| 481 | } | ||
| 482 | let size = ((seed as usize) % 2048) + 1; | ||
| 483 | seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1); | ||
| 484 | let align = 16usize << (seed as usize % 8); | ||
| 485 | seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1); | ||
| 486 | let layout = Layout::from_size_align(size, align).unwrap(); | ||
| 487 | let p = ALLOCATOR.alloc(layout); | ||
| 488 | if p.is_null() { | ||
| 489 | return false; // failed to reuse freed space | ||
| 490 | } | ||
| 491 | if !(p as usize).is_multiple_of(align) { | ||
| 492 | return false; | ||
| 493 | } | ||
| 494 | core::ptr::write_bytes(p, 0x5A, size); | ||
| 495 | blocks[i] = (p as usize, size, align); | ||
| 496 | } | ||
| 497 | |||
| 498 | // Free everything and check the heap coalesces back to one block. | ||
| 499 | for (ptr, size, align) in blocks.iter() { | ||
| 500 | if *ptr == 0 { | ||
| 501 | continue; | ||
| 502 | } | ||
| 503 | let layout = Layout::from_size_align(*size, *align).unwrap(); | ||
| 504 | ALLOCATOR.dealloc(*ptr as *mut u8, layout); | ||
| 505 | } | ||
| 506 | let stats = heap_stats(); | ||
| 507 | stats.free_blocks == 1 && stats.free_bytes == stats.heap_size | ||
| 508 | } | ||
| 509 | } | ||
| 510 | |||
| 511 | // --------------------------------------------------------------------------- | ||
| 512 | // Physical page allocator — bitmap over the usable (conventional) regions. | ||
| 513 | // --------------------------------------------------------------------------- | ||
| 514 | // One bit per 4 KB page: 0 = free, 1 = allocated. The bitmap spans the whole | ||
| 515 | // [lowest conventional page, highest conventional page] range; pages that are | ||
| 516 | // NOT conventional inside that span stay marked allocated, so MMIO holes and | ||
| 517 | // firmware-reserved gaps are never handed out. The kernel heap region is also | ||
| 518 | // reserved, keeping the heap and page allocators from double-allocating the | ||
| 519 | // same RAM. | ||
| 520 | // --------------------------------------------------------------------------- | ||
| 521 | |||
| 522 | pub const PAGE_SIZE: usize = 4096; | ||
| 523 | |||
| 524 | /// Bitmap storage (allocated once from the heap at init). | ||
| 525 | static mut BITMAP: *mut u8 = ptr::null_mut(); | ||
| 526 | /// Number of bytes in the bitmap. | ||
| 527 | static mut BITMAP_BYTES: usize = 0; | ||
| 528 | /// Page number that bitmap bit 0 refers to. | ||
| 529 | static mut BASE_PAGE: usize = 0; | ||
| 530 | /// Number of pages covered by the bitmap. | ||
| 531 | static mut SPAN_PAGES: usize = 0; | ||
| 532 | /// Running count of free pages (kept in sync with the bitmap). | ||
| 533 | static mut FREE_PAGES: usize = 0; | ||
| 534 | |||
| 535 | /// Snapshot of physical page usage. | ||
| 536 | #[derive(Debug, Clone, Copy)] | ||
| 537 | pub struct PageStats { | ||
| 538 | pub base_page: usize, | ||
| 539 | pub span_pages: usize, | ||
| 540 | pub span_bytes: usize, | ||
| 541 | pub free_pages: usize, | ||
| 542 | pub used_pages: usize, | ||
| 543 | } | ||
| 544 | |||
| 545 | /// Current physical page usage. | ||
| 546 | pub fn page_stats() -> PageStats { | ||
| 547 | unsafe { | ||
| 548 | PageStats { | ||
| 549 | base_page: BASE_PAGE, | ||
| 550 | span_pages: SPAN_PAGES, | ||
| 551 | span_bytes: SPAN_PAGES * PAGE_SIZE, | ||
| 552 | free_pages: FREE_PAGES, | ||
| 553 | used_pages: SPAN_PAGES.saturating_sub(FREE_PAGES), | ||
| 554 | } | ||
| 555 | } | ||
| 556 | } | ||
| 557 | |||
| 558 | /// Build the bitmap from the stashed memory map and reserve the heap. | ||
| 559 | /// Returns false if there is no conventional memory or the bitmap cannot | ||
| 560 | /// be allocated. | ||
| 561 | pub fn init_page_allocator() -> bool { | ||
| 562 | unsafe { | ||
| 563 | // Lowest and highest conventional page numbers. | ||
| 564 | let mut low: usize = usize::MAX; | ||
| 565 | let mut high: usize = 0; | ||
| 566 | for i in 0..MAP_ENTRIES { | ||
| 567 | let d = &MEMORY_MAP[i]; | ||
| 568 | if d.kind == MEMORY_TYPE_CONVENTIONAL { | ||
| 569 | let s = d.physical_start as usize; | ||
| 570 | let e = s + d.number_of_pages as usize * PAGE_SIZE; | ||
| 571 | if s < low { | ||
| 572 | low = s; | ||
| 573 | } | ||
| 574 | if e > high { | ||
| 575 | high = e; | ||
| 576 | } | ||
| 577 | } | ||
| 578 | } | ||
| 579 | if low == usize::MAX || high <= low { | ||
| 580 | return false; | ||
| 581 | } | ||
| 582 | |||
| 583 | let base_page = low / PAGE_SIZE; | ||
| 584 | let span_pages = high.div_ceil(PAGE_SIZE) - base_page; | ||
| 585 | let bytes = span_pages.div_ceil(8); | ||
| 586 | |||
| 587 | // The bitmap itself lives in the heap (one allocation, never freed). | ||
| 588 | // `bytes` >= 1 and align 16 is a power of two, so this cannot fail. | ||
| 589 | let layout = unsafe { Layout::from_size_align_unchecked(bytes, 16) }; | ||
| 590 | let bm = ALLOCATOR.alloc(layout); | ||
| 591 | if bm.is_null() { | ||
| 592 | return false; | ||
| 593 | } | ||
| 594 | |||
| 595 | // Start everything allocated, then free only usable pages. | ||
| 596 | core::ptr::write_bytes(bm, 0xFF, bytes); | ||
| 597 | |||
| 598 | for i in 0..MAP_ENTRIES { | ||
| 599 | let d = &MEMORY_MAP[i]; | ||
| 600 | if d.kind == MEMORY_TYPE_CONVENTIONAL { | ||
| 601 | let s = d.physical_start as usize / PAGE_SIZE; | ||
| 602 | let e = (d.physical_start as usize + d.number_of_pages as usize * PAGE_SIZE) | ||
| 603 | .div_ceil(PAGE_SIZE); | ||
| 604 | for p in s..e { | ||
| 605 | let idx = p - base_page; | ||
| 606 | *bm.add(idx / 8) &= !(1 << (idx % 8)); | ||
| 607 | } | ||
| 608 | } | ||
| 609 | } | ||
| 610 | |||
| 611 | // Reserve the heap so the page allocator never hands out its pages. | ||
| 612 | if HEAP_END > HEAP_START { | ||
| 613 | let s = HEAP_START / PAGE_SIZE; | ||
| 614 | let e = HEAP_END.div_ceil(PAGE_SIZE); | ||
| 615 | for p in s..e { | ||
| 616 | let idx = p - base_page; | ||
| 617 | *bm.add(idx / 8) |= 1 << (idx % 8); | ||
| 618 | } | ||
| 619 | } | ||
| 620 | |||
| 621 | // Never hand out page 0 (the null-pointer trap page). | ||
| 622 | if base_page == 0 { | ||
| 623 | *bm |= 0x01; | ||
| 624 | } | ||
| 625 | |||
| 626 | // Count free pages once; keep the count in sync from here on. | ||
| 627 | let mut free = 0usize; | ||
| 628 | for i in 0..span_pages { | ||
| 629 | if (*bm.add(i / 8) >> (i % 8)) & 1 == 0 { | ||
| 630 | free += 1; | ||
| 631 | } | ||
| 632 | } | ||
| 633 | |||
| 634 | BITMAP = bm; | ||
| 635 | BITMAP_BYTES = bytes; | ||
| 636 | BASE_PAGE = base_page; | ||
| 637 | SPAN_PAGES = span_pages; | ||
| 638 | FREE_PAGES = free; | ||
| 639 | true | ||
| 640 | } | ||
| 641 | } | ||
| 642 | |||
| 643 | /// Allocate `count` contiguous physical pages. Returns the physical | ||
| 644 | /// address of the first page, or None when no run fits. | ||
| 645 | pub fn alloc_pages(count: usize) -> Option<usize> { | ||
| 646 | alloc_pages_aligned(count, 1) | ||
| 647 | } | ||
| 648 | |||
| 649 | /// Allocate `count` contiguous physical pages whose start address is | ||
| 650 | /// aligned to `align_pages` (a power of two, in page units). | ||
| 651 | pub fn alloc_pages_aligned(count: usize, align_pages: usize) -> Option<usize> { | ||
| 652 | if count == 0 || !align_pages.is_power_of_two() { | ||
| 653 | return None; | ||
| 654 | } | ||
| 655 | unsafe { | ||
| 656 | if BITMAP.is_null() || FREE_PAGES < count { | ||
| 657 | return None; | ||
| 658 | } | ||
| 659 | let span = SPAN_PAGES; | ||
| 660 | let mask = align_pages - 1; | ||
| 661 | let mut run_start = 0usize; | ||
| 662 | let mut run_len = 0usize; | ||
| 663 | for idx in 0..span { | ||
| 664 | let free = (*BITMAP.add(idx / 8) >> (idx % 8)) & 1 == 0; | ||
| 665 | if free { | ||
| 666 | if run_len == 0 { | ||
| 667 | run_start = idx; | ||
| 668 | } | ||
| 669 | run_len += 1; | ||
| 670 | // Aligned page number within (or just past) this run. | ||
| 671 | let p = BASE_PAGE + run_start; | ||
| 672 | let p_align = (p + mask) & !mask; | ||
| 673 | if p_align == 0 { | ||
| 674 | // Candidate starts at page 0 — restart the run here. | ||
| 675 | run_start = idx; | ||
| 676 | run_len = 1; | ||
| 677 | continue; | ||
| 678 | } | ||
| 679 | let idx_align = p_align - BASE_PAGE; | ||
| 680 | if idx_align + count <= run_start + run_len { | ||
| 681 | // Mark the run allocated. | ||
| 682 | for k in idx_align..idx_align + count { | ||
| 683 | *BITMAP.add(k / 8) |= 1 << (k % 8); | ||
| 684 | } | ||
| 685 | FREE_PAGES -= count; | ||
| 686 | return Some(p_align * PAGE_SIZE); | ||
| 687 | } | ||
| 688 | } else { | ||
| 689 | run_len = 0; | ||
| 690 | } | ||
| 691 | } | ||
| 692 | None | ||
| 693 | } | ||
| 694 | } | ||
| 695 | |||
| 696 | /// Free `count` physical pages starting at `addr`. The range must be | ||
| 697 | /// page-aligned and previously allocated by `alloc_pages`. | ||
| 698 | pub fn free_pages(addr: usize, count: usize) { | ||
| 699 | unsafe { | ||
| 700 | if BITMAP.is_null() || count == 0 { | ||
| 701 | return; | ||
| 702 | } | ||
| 703 | let base = BASE_PAGE; | ||
| 704 | let span = SPAN_PAGES; | ||
| 705 | let first = addr / PAGE_SIZE; | ||
| 706 | let in_range = | ||
| 707 | addr.is_multiple_of(PAGE_SIZE) && first >= base && first + count <= base + span; | ||
| 708 | if !in_range { | ||
| 709 | debug_assert!(false, "free_pages: bad range {:#x} len {}", addr, count); | ||
| 710 | return; | ||
| 711 | } | ||
| 712 | for p in first..first + count { | ||
| 713 | let idx = p - base; | ||
| 714 | debug_assert!( | ||
| 715 | (*BITMAP.add(idx / 8) >> (idx % 8)) & 1 != 0, | ||
| 716 | "free_pages: double free at {:#x}", | ||
| 717 | p * PAGE_SIZE | ||
| 718 | ); | ||
| 719 | *BITMAP.add(idx / 8) &= !(1 << (idx % 8)); | ||
| 720 | } | ||
| 721 | FREE_PAGES += count; | ||
| 722 | } | ||
| 723 | } | ||
| 724 | |||
| 725 | /// Smoke test for the page allocator: distinct single pages, a contiguous | ||
| 726 | /// run, scribble/readback, and a clean return to the baseline free count. | ||
| 727 | pub fn page_self_test() -> bool { | ||
| 728 | unsafe { | ||
| 729 | let before = page_stats(); | ||
| 730 | if before.free_pages < 1024 { | ||
| 731 | return false; // not enough free pages to play with | ||
| 732 | } | ||
| 733 | |||
| 734 | // 64 single pages — all distinct, page-aligned, non-zero. | ||
| 735 | let mut pages: [usize; 64] = [0; 64]; | ||
| 736 | for i in 0..64 { | ||
| 737 | let Some(addr) = alloc_pages(1) else { | ||
| 738 | return false; | ||
| 739 | }; | ||
| 740 | if addr % PAGE_SIZE != 0 || addr == 0 { | ||
| 741 | return false; | ||
| 742 | } | ||
| 743 | pages[i] = addr; | ||
| 744 | } | ||
| 745 | for i in 0..64 { | ||
| 746 | for j in 0..i { | ||
| 747 | if pages[i] == pages[j] { | ||
| 748 | return false; // overlap | ||
| 749 | } | ||
| 750 | } | ||
| 751 | } | ||
| 752 | |||
| 753 | // A contiguous run of 16. | ||
| 754 | let Some(run) = alloc_pages(16) else { | ||
| 755 | return false; | ||
| 756 | }; | ||
| 757 | if run % PAGE_SIZE != 0 || run == 0 { | ||
| 758 | return false; | ||
| 759 | } | ||
| 760 | |||
| 761 | // Free count must drop by exactly 64 + 16. | ||
| 762 | if page_stats().free_pages != before.free_pages - 64 - 16 { | ||
| 763 | return false; | ||
| 764 | } | ||
| 765 | |||
| 766 | // Scribble through the run and read it back (it's real RAM). | ||
| 767 | for k in 0..16 { | ||
| 768 | core::ptr::write_volatile((run + k * PAGE_SIZE) as *mut u8, 0xAB); | ||
| 769 | } | ||
| 770 | for k in 0..16 { | ||
| 771 | if core::ptr::read_volatile((run + k * PAGE_SIZE) as *const u8) != 0xAB { | ||
| 772 | return false; | ||
| 773 | } | ||
| 774 | } | ||
| 775 | |||
| 776 | // Free everything and check we're back to baseline. | ||
| 777 | for &p in pages.iter() { | ||
| 778 | free_pages(p, 1); | ||
| 779 | } | ||
| 780 | free_pages(run, 16); | ||
| 781 | |||
| 782 | let after = page_stats(); | ||
| 783 | after.free_pages == before.free_pages | ||
| 784 | } | ||
| 785 | } | ||
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 | } | ||
diff --git a/rsnix/src/uefi.rs b/rsnix/src/uefi.rs index 2e3ed6b..619739e 100644 --- a/rsnix/src/uefi.rs +++ b/rsnix/src/uefi.rs | |||
| @@ -116,13 +116,43 @@ pub struct MemoryDescriptor { | |||
| 116 | pub attribute: u64, | 116 | pub attribute: u64, |
| 117 | } | 117 | } |
| 118 | 118 | ||
| 119 | pub const MEMORY_TYPE_CONVENTIONAL: u32 = 7; | 119 | pub const MEMORY_TYPE_RESERVED: u32 = 0; |
| 120 | #[allow(dead_code)] | 120 | pub const MEMORY_TYPE_LOADER_CODE: u32 = 1; |
| 121 | pub const MEMORY_TYPE_LOADER_DATA: u32 = 6; | 121 | pub const MEMORY_TYPE_LOADER_DATA: u32 = 2; |
| 122 | #[allow(dead_code)] | ||
| 123 | pub const MEMORY_TYPE_BOOT_SERVICES_CODE: u32 = 3; | 122 | pub const MEMORY_TYPE_BOOT_SERVICES_CODE: u32 = 3; |
| 124 | #[allow(dead_code)] | ||
| 125 | pub const MEMORY_TYPE_BOOT_SERVICES_DATA: u32 = 4; | 123 | pub const MEMORY_TYPE_BOOT_SERVICES_DATA: u32 = 4; |
| 124 | pub const MEMORY_TYPE_RUNTIME_CODE: u32 = 5; | ||
| 125 | pub const MEMORY_TYPE_RUNTIME_DATA: u32 = 6; | ||
| 126 | pub const MEMORY_TYPE_CONVENTIONAL: u32 = 7; | ||
| 127 | pub const MEMORY_TYPE_UNUSABLE: u32 = 8; | ||
| 128 | pub const MEMORY_TYPE_ACPI_RECLAIM: u32 = 9; | ||
| 129 | pub const MEMORY_TYPE_ACPI_NVS: u32 = 10; | ||
| 130 | pub const MEMORY_TYPE_MMIO: u32 = 11; | ||
| 131 | pub const MEMORY_TYPE_MMIO_PORT: u32 = 12; | ||
| 132 | pub const MEMORY_TYPE_PAL_CODE: u32 = 13; | ||
| 133 | pub const MEMORY_TYPE_PERSISTENT: u32 = 14; | ||
| 134 | |||
| 135 | /// Human-readable name for a UEFI memory type. | ||
| 136 | pub fn memory_type_name(kind: u32) -> &'static str { | ||
| 137 | match kind { | ||
| 138 | 0 => "Reserved", | ||
| 139 | 1 => "LoaderCode", | ||
| 140 | 2 => "LoaderData", | ||
| 141 | 3 => "BootServicesCode", | ||
| 142 | 4 => "BootServicesData", | ||
| 143 | 5 => "RuntimeCode", | ||
| 144 | 6 => "RuntimeData", | ||
| 145 | 7 => "Conventional", | ||
| 146 | 8 => "Unusable", | ||
| 147 | 9 => "ACPIReclaim", | ||
| 148 | 10 => "ACPINVS", | ||
| 149 | 11 => "MMIO", | ||
| 150 | 12 => "MMIOPort", | ||
| 151 | 13 => "PalCode", | ||
| 152 | 14 => "Persistent", | ||
| 153 | _ => "Unknown", | ||
| 154 | } | ||
| 155 | } | ||
| 126 | 156 | ||
| 127 | // --------------------------------------------------------------------------- | 157 | // --------------------------------------------------------------------------- |
| 128 | // Boot services table | 158 | // Boot services table |
