Next Generation WASM Microkernel Operating System
at main 270 lines 9.7 kB view raw
1// Copyright 2025 Jonas Kruckenberg 2// 3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or 4// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or 5// http://opensource.org/licenses/MIT>, at your option. This file may not be 6// copied, modified, or distributed except according to those terms. 7 8#![no_std] 9#![no_main] 10#![feature(used_with_arg)] 11#![feature(thread_local, never_type)] 12#![feature(debug_closure_helpers)] 13#![expect(internal_features, reason = "panic internals")] 14#![feature(std_internals, panic_can_unwind, formatting_options)] 15#![feature(step_trait)] 16#![feature(box_into_inner)] 17#![feature(array_chunks)] 18#![feature(iter_array_chunks)] 19#![feature(iter_next_chunk)] 20#![feature(if_let_guard)] 21#![feature(allocator_api)] 22#![expect(dead_code, reason = "TODO")] // TODO remove 23#![feature(asm_unwind)] 24 25extern crate alloc; 26extern crate k23_panic_unwind; 27 28mod allocator; 29mod arch; 30mod backtrace; 31mod bootargs; 32mod device_tree; 33mod irq; 34mod mem; 35mod metrics; 36mod shell; 37mod state; 38#[cfg(test)] 39mod tests; 40mod tracing; 41mod util; 42mod wasm; 43 44use core::ops::Range; 45use core::slice; 46use core::time::Duration; 47 48use cfg_if::cfg_if; 49use k23_abort::abort; 50use k23_arrayvec::ArrayVec; 51use k23_fastrand::FastRand; 52use kasync::executor::{Executor, Worker}; 53use kasync::time::{Instant, Ticks, Timer}; 54use kmem::{AddressRangeExt, PhysicalAddress}; 55use loader_api::{BootInfo, LoaderConfig, MemoryRegionKind}; 56use mem::frame_alloc; 57use rand::{RngCore, SeedableRng}; 58use rand_chacha::ChaCha20Rng; 59 60use crate::backtrace::Backtrace; 61use crate::device_tree::DeviceTree; 62use crate::mem::bootstrap_alloc::BootstrapAllocator; 63use crate::state::{CpuLocal, Global}; 64 65/// The size of the stack in pages 66pub const STACK_SIZE_PAGES: u32 = 256; // TODO find a lower more appropriate value 67/// The size of the trap handler stack in pages 68pub const TRAP_STACK_SIZE_PAGES: usize = 64; // TODO find a lower more appropriate value 69/// The initial size of the kernel heap in pages. 70/// 71/// This initial size should be small enough so the loaders less sophisticated allocator can 72/// doesn't cause startup slowdown & inefficient mapping, but large enough so we can bootstrap 73/// our own virtual memory subsystem. At that point we are no longer reliant on this initial heap 74/// size and can dynamically grow the heap as needed. 75pub const INITIAL_HEAP_SIZE_PAGES: usize = 4096 * 2; // 32 MiB 76 77pub type Result<T> = anyhow::Result<T>; 78 79#[used(linker)] 80#[unsafe(link_section = ".loader_config")] 81static LOADER_CONFIG: LoaderConfig = { 82 let mut cfg = LoaderConfig::new_default(); 83 cfg.kernel_stack_size_pages = STACK_SIZE_PAGES; 84 cfg 85}; 86 87#[unsafe(no_mangle)] 88fn _start(cpuid: usize, boot_info: &'static BootInfo, boot_ticks: u64) -> ! { 89 k23_panic_unwind::set_hook(|info| { 90 tracing::error!("CPU {info}"); 91 92 // FIXME 32 seems adequate for unoptimized builds where the callstack can get quite deep 93 // but (at least at the moment) is absolute overkill for optimized builds. Sadly there 94 // is no good way to do conditional compilation based on the opt-level. 95 const MAX_BACKTRACE_FRAMES: usize = 32; 96 97 let backtrace = backtrace::__rust_end_short_backtrace(|| { 98 Backtrace::<MAX_BACKTRACE_FRAMES>::capture().unwrap() 99 }); 100 tracing::error!("{backtrace}"); 101 102 if backtrace.frames_omitted { 103 tracing::warn!("Stack trace was larger than backtrace buffer, omitted some frames."); 104 } 105 }); 106 107 // Unwinding expects at least one landing pad in the callstack, but capturing all unwinds that 108 // bubble up to this point is also a good idea since we can perform some last cleanup and 109 // print an error message. 110 let res = k23_panic_unwind::catch_unwind(|| { 111 backtrace::__rust_begin_short_backtrace(|| kmain(cpuid, boot_info, boot_ticks)); 112 }); 113 114 match res { 115 Ok(_) => arch::exit(0), 116 // If the panic propagates up to this catch here there is nothing we can do, this is a terminal 117 // failure. 118 Err(_) => { 119 tracing::error!("unrecoverable kernel panic"); 120 abort() 121 } 122 } 123} 124 125fn kmain(cpuid: usize, boot_info: &'static BootInfo, boot_ticks: u64) { 126 // perform EARLY per-cpu, architecture-specific initialization 127 // (e.g. resetting the FPU) 128 arch::per_cpu_init_early(); 129 tracing::per_cpu_init_early(cpuid); 130 131 let (fdt, fdt_region_phys) = locate_device_tree(boot_info); 132 let mut rng = ChaCha20Rng::from_seed(boot_info.rng_seed); 133 134 let global = state::try_init_global(|| { 135 // set up the basic functionality of the tracing subsystem as early as possible 136 tracing::init_early(); 137 138 // initialize a simple bump allocator for allocating memory before our virtual memory subsystem 139 // is available 140 let allocatable_memories = allocatable_memory_regions(boot_info); 141 tracing::info!("allocatable memories: {:?}", allocatable_memories); 142 let mut boot_alloc = BootstrapAllocator::new(&allocatable_memories); 143 144 // initializing the global allocator 145 allocator::init(&mut boot_alloc, boot_info); 146 147 let device_tree = DeviceTree::parse(fdt)?; 148 tracing::debug!("{device_tree:?}"); 149 150 let bootargs = bootargs::parse(&device_tree)?; 151 152 // initialize the backtracing subsystem after the allocator has been set up 153 // since setting up the symbolization context requires allocation 154 backtrace::init(boot_info, bootargs.backtrace); 155 156 // fully initialize the tracing subsystem now that we can allocate 157 tracing::init(bootargs.log); 158 159 // perform global, architecture-specific initialization 160 let arch = arch::init(); 161 162 // initialize the global frame allocator 163 // at this point we have parsed and processed the flattened device tree, so we pass it to the 164 // frame allocator for reuse 165 let frame_alloc = frame_alloc::init(boot_alloc, fdt_region_phys); 166 167 // initialize the virtual memory subsystem 168 mem::init(boot_info, &mut rng, frame_alloc).unwrap(); 169 170 // perform LATE per-cpu, architecture-specific initialization 171 // (e.g. setting the trap vector and enabling interrupts) 172 let cpu = arch::device::cpu::Cpu::new(&device_tree, cpuid)?; 173 174 let executor = Executor::with_capacity(boot_info.cpu_mask.count_ones() as usize).unwrap(); 175 let timer = Timer::new(Duration::from_millis(1), cpu.clock); 176 177 Ok(Global { 178 time_origin: Instant::from_ticks(&timer, Ticks(boot_ticks)), 179 timer, 180 executor, 181 device_tree, 182 boot_info, 183 arch, 184 }) 185 }) 186 .unwrap(); 187 188 // perform LATE per-cpu, architecture-specific initialization 189 // (e.g. setting the trap vector and enabling interrupts) 190 let arch_state = arch::per_cpu_init_late(&global.device_tree, cpuid).unwrap(); 191 192 state::init_cpu_local(CpuLocal { 193 id: cpuid, 194 arch: arch_state, 195 }); 196 197 tracing::info!( 198 "Booted in ~{:?} ({:?} in k23)", 199 Instant::now(&global.timer).duration_since(Instant::ZERO), 200 Instant::from_ticks(&global.timer, Ticks(boot_ticks)).elapsed(&global.timer) 201 ); 202 203 let mut worker2 = Worker::new(&global.executor, FastRand::from_seed(rng.next_u64())).unwrap(); 204 205 cfg_if! { 206 if #[cfg(test)] { 207 if cpuid == 0 { 208 arch::block_on(worker2.run(tests::run_tests(global))).unwrap().exit_if_failed(); 209 } else { 210 arch::block_on(worker2.run(futures::future::pending::<()>())).unwrap_err(); // the only way `run` can return is when the executor is closed 211 } 212 } else { 213 shell::init( 214 &global.device_tree, 215 &global.executor, 216 boot_info.cpu_mask.count_ones() as usize, 217 ); 218 arch::block_on(worker2.run(futures::future::pending::<()>())).unwrap_err(); // the only way `run` can return is when the executor is closed 219 } 220 } 221} 222 223/// Builds a list of memory regions from the boot info that are usable for allocation. 224/// 225/// The regions passed by the loader are guaranteed to be non-overlapping, but might not be 226/// sorted and might not be optimally "packed". This function will both sort regions and 227/// attempt to compact the list by merging adjacent regions. 228fn allocatable_memory_regions(boot_info: &BootInfo) -> ArrayVec<Range<PhysicalAddress>, 16> { 229 let temp: ArrayVec<Range<PhysicalAddress>, 16> = boot_info 230 .memory_regions 231 .iter() 232 .filter_map(|region| region.kind.is_usable().then_some(region.range.clone())) 233 .collect(); 234 235 // merge adjacent regions 236 let mut out: ArrayVec<Range<PhysicalAddress>, 16> = ArrayVec::new(); 237 'outer: for region in temp { 238 for other in &mut out { 239 if region.start == other.end { 240 other.end = region.end; 241 continue 'outer; 242 } 243 if region.end == other.start { 244 other.start = region.start; 245 continue 'outer; 246 } 247 } 248 249 out.push(region); 250 } 251 252 out 253} 254 255fn locate_device_tree(boot_info: &BootInfo) -> (&'static [u8], Range<PhysicalAddress>) { 256 let fdt = boot_info 257 .memory_regions 258 .iter() 259 .find(|region| region.kind == MemoryRegionKind::FDT) 260 .expect("no FDT region"); 261 262 let base = boot_info 263 .physical_address_offset 264 .add(fdt.range.start.get()) 265 .as_mut_ptr(); 266 267 // Safety: we need to trust the bootinfo data is correct 268 let slice = unsafe { slice::from_raw_parts(base, fdt.range.len()) }; 269 (slice, fdt.range.clone()) 270}