summaryrefslogtreecommitdiffstats
path: root/rsnix/src/memory.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-07-31 15:51:28 +0200
committersalmonik <salmonik@tuta.io>2026-07-31 15:51:28 +0200
commit5e03409586efc37931f07b40ee0c71a9a9e109f7 (patch)
treea1e6e014b94fb410ffce49a7611bf94295165729 /rsnix/src/memory.rs
parentrsboot: loader file buffers use LoaderData (2) instead of RuntimeData (6) (diff)
downloadrs-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.rs663
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;
16static mut MAP_WORK_BUF: [u8; 65536] = [0u8; 65536]; 16static 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// ---------------------------------------------------------------------------
22struct BumpAllocator;
23 42
24unsafe impl Sync for BumpAllocator {} 43const HEADER_SIZE: usize = 16; // size + free flag
44const MINI_SIZE: usize = 8; // back-pointer before the payload
45const MIN_BLOCK: usize = 32; // smallest block we will create (header + links)
46const MIN_ALIGN: usize = 16;
25 47
26unsafe impl GlobalAlloc for BumpAllocator { 48/// Block header — first 16 bytes of every block.
49#[repr(C)]
50struct Header {
51 size: usize,
52 free: usize,
53}
54
55impl 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)]
65struct FreeLinks {
66 prev: *mut Header,
67 next: *mut Header,
68}
69
70/// Head of the address-sorted free list (lowest address first).
71static mut FREE_LIST: *mut Header = ptr::null_mut();
72
73/// Heap region bounds (set by `init_heap`).
74static mut HEAP_START: usize = 0;
75static mut HEAP_END: usize = 0;
76/// Total heap capacity — the original region plus pages added by growth.
77static mut HEAP_CAPACITY: usize = 0;
78
79/// Minimum pages `grow_heap` takes from the page allocator per growth.
80const HEAP_GROW_PAGES: usize = 64;
81
82#[inline]
83fn 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.
89unsafe 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.
113unsafe 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.
131unsafe 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.
159unsafe 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.
197unsafe 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.
214struct Allocator;
215
216unsafe impl Sync for Allocator {}
217
218unsafe 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]
52static ALLOCATOR: BumpAllocator = BumpAllocator; 259static ALLOCATOR: Allocator = Allocator;
53
54/// Heap bump allocator bounds (`static mut` accessed only inside
55/// `GlobalAlloc` impl, which is already `unsafe`).
56static mut HEAP_START: usize = 0;
57static mut HEAP_END: usize = 0;
58static 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
149fn init_bump_allocator() { 350fn 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)]
176pub fn allocate(size: usize) -> *mut u8 { 385pub 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)]
406pub 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).
415pub 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.
437pub 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
522pub const PAGE_SIZE: usize = 4096;
523
524/// Bitmap storage (allocated once from the heap at init).
525static mut BITMAP: *mut u8 = ptr::null_mut();
526/// Number of bytes in the bitmap.
527static mut BITMAP_BYTES: usize = 0;
528/// Page number that bitmap bit 0 refers to.
529static mut BASE_PAGE: usize = 0;
530/// Number of pages covered by the bitmap.
531static mut SPAN_PAGES: usize = 0;
532/// Running count of free pages (kept in sync with the bitmap).
533static mut FREE_PAGES: usize = 0;
534
535/// Snapshot of physical page usage.
536#[derive(Debug, Clone, Copy)]
537pub 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.
546pub 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.
561pub 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.
645pub 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).
651pub 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`.
698pub 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.
727pub 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}