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