summaryrefslogtreecommitdiffstats
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
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
-rw-r--r--rsdos/src/main.rs32
-rw-r--r--rsnix/src/lib.rs1
-rw-r--r--rsnix/src/memory.rs663
-rw-r--r--rsnix/src/paging.rs356
-rw-r--r--rsnix/src/uefi.rs40
5 files changed, 1050 insertions, 42 deletions
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs
index 543556b..1169785 100644
--- a/rsdos/src/main.rs
+++ b/rsdos/src/main.rs
@@ -106,6 +106,38 @@ pub extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut SystemT
106 106
107 dump_memory_summary(); 107 dump_memory_summary();
108 108
109 if rsnix::memory::self_test() {
110 rsnix::serial_println!("heap: self-test OK (alloc/free/coalesce)");
111 } else {
112 rsnix::serial_println!("heap: self-test FAILED");
113 }
114
115 if rsnix::memory::init_page_allocator() {
116 rsnix::serial_println!("pages: bitmap allocator ready");
117 } else {
118 rsnix::serial_println!("pages: init FAILED");
119 }
120 if rsnix::memory::page_self_test() {
121 rsnix::serial_println!("pages: self-test OK (alloc/free/runs)");
122 } else {
123 rsnix::serial_println!("pages: self-test FAILED");
124 }
125
126 if rsnix::paging::adopt_tables() {
127 rsnix::serial_println!(
128 "pg: adopted own page tables (cr3={:#x})",
129 rsnix::paging::cr3()
130 );
131 } else {
132 rsnix::serial_println!("pg: adopt_tables FAILED");
133 }
134
135 if rsnix::paging::self_test() {
136 rsnix::serial_println!("pg: self-test OK (map/unmap/split)");
137 } else {
138 rsnix::serial_println!("pg: self-test FAILED");
139 }
140
109 rsnix::serial_println!("boot: entering shell"); 141 rsnix::serial_println!("boot: entering shell");
110 shell::run(); 142 shell::run();
111} 143}
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;
6pub mod gop; 6pub mod gop;
7pub mod interrupts; 7pub mod interrupts;
8pub mod memory; 8pub mod memory;
9pub mod paging;
9pub mod ps2; 10pub mod ps2;
10pub mod serial; 11pub mod serial;
11pub mod uefi; 12pub 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;
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}
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
15use crate::memory::{alloc_pages, free_pages, PAGE_SIZE};
16
17pub const PAGE_PRESENT: u64 = 1 << 0;
18pub const PAGE_WRITABLE: u64 = 1 << 1;
19pub const PAGE_USER: u64 = 1 << 2;
20pub const PAGE_PS: u64 = 1 << 7; // large page (2 MB PDE, 1 GB PDPTE)
21pub const PAGE_NX: u64 = 1 << 63;
22
23/// Physical address bits in a page-table entry.
24const PHYS_ADDR_MASK: u64 = 0x000F_FFFF_FFFF_F000;
25
26const PAGE_2M: usize = 1 << 21;
27const PAGE_1G: usize = 1 << 30;
28
29/// Current PML4 root (physical address, identity-mapped).
30pub 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).
40fn 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.
55pub 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.
136unsafe 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.
162unsafe 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).
188pub 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`.
229pub 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.
240pub 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)?
282pub 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.
315pub 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
119pub const MEMORY_TYPE_CONVENTIONAL: u32 = 7; 119pub const MEMORY_TYPE_RESERVED: u32 = 0;
120#[allow(dead_code)] 120pub const MEMORY_TYPE_LOADER_CODE: u32 = 1;
121pub const MEMORY_TYPE_LOADER_DATA: u32 = 6; 121pub const MEMORY_TYPE_LOADER_DATA: u32 = 2;
122#[allow(dead_code)]
123pub const MEMORY_TYPE_BOOT_SERVICES_CODE: u32 = 3; 122pub const MEMORY_TYPE_BOOT_SERVICES_CODE: u32 = 3;
124#[allow(dead_code)]
125pub const MEMORY_TYPE_BOOT_SERVICES_DATA: u32 = 4; 123pub const MEMORY_TYPE_BOOT_SERVICES_DATA: u32 = 4;
124pub const MEMORY_TYPE_RUNTIME_CODE: u32 = 5;
125pub const MEMORY_TYPE_RUNTIME_DATA: u32 = 6;
126pub const MEMORY_TYPE_CONVENTIONAL: u32 = 7;
127pub const MEMORY_TYPE_UNUSABLE: u32 = 8;
128pub const MEMORY_TYPE_ACPI_RECLAIM: u32 = 9;
129pub const MEMORY_TYPE_ACPI_NVS: u32 = 10;
130pub const MEMORY_TYPE_MMIO: u32 = 11;
131pub const MEMORY_TYPE_MMIO_PORT: u32 = 12;
132pub const MEMORY_TYPE_PAL_CODE: u32 = 13;
133pub const MEMORY_TYPE_PERSISTENT: u32 = 14;
134
135/// Human-readable name for a UEFI memory type.
136pub 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