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/memory.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 'rsnix/src/memory.rs')
| -rw-r--r-- | rsnix/src/memory.rs | 663 |
1 files changed, 626 insertions, 37 deletions
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 | } | ||
