#![no_std] #![no_main] #![allow(unsafe_code)] use core::panic::PanicInfo; use rsnix::uefi::*; extern crate alloc; // --------------------------------------------------------------------------- // Panic handler // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- // Panic handler // --------------------------------------------------------------------------- #[panic_handler] fn panic(info: &PanicInfo) -> ! { use core::fmt::Write; // A background task (id >= 2) panicked: die gracefully — log it, // mark the task done, switch away. The shell keeps running and // `jobs` shows the corpse. Panics in the shell, idle, ISR context, // or before the scheduler exists still halt the whole system. if rsnix::sched::is_ready() && !rsnix::sched::in_isr() && rsnix::sched::current() >= 2 { let mut buf = [0u8; 192]; let n = { let mut w = rsnix::strbuf::FmtBuf::new(&mut buf); let _ = write!(&mut w, "{}", info.message()); if let Some(loc) = info.location() { let _ = write!(&mut w, " at {}:{}", loc.file(), loc.line()); } w.len() }; let msg = core::str::from_utf8(&buf[..n]).unwrap_or("task panic"); rsnix::sched::abort_current(msg); } rsnix::serial::write_str("\r\n*** PANIC ***\r\n"); let _ = write!(&mut rsnix::serial::SerialWriter, "{}\r\n", info.message()); if let Some(loc) = info.location() { let _ = write!( &mut rsnix::serial::SerialWriter, " at {}:{}\r\n", loc.file(), loc.line() ); } // Real hardware has no serial console — put the panic on the screen // too (white-on-red band), so a pre-shell panic is not a silent // blank screen. The last boot stage is appended so the user can // see how far boot got before the crash. let mut loc_buf = [0u8; 96]; let mut msg_buf = [0u8; 128]; let mut stage_buf = [0u8; 96]; let loc_n = { let mut w = rsnix::strbuf::FmtBuf::new(&mut loc_buf); if let Some(loc) = info.location() { let _ = write!(&mut w, "at {}:{}", loc.file(), loc.line()); } else { let _ = write!(&mut w, "(no location)"); } w.len() }; let msg_n = { let mut w = rsnix::strbuf::FmtBuf::new(&mut msg_buf); let _ = write!(&mut w, "{}", info.message()); w.len() }; let stage_n = { let s = rsnix::gop::last_boot_stage(); let n = s.len().min(stage_buf.len()); stage_buf[..n].copy_from_slice(&s.as_bytes()[..n]); n }; let lines = [ "*** PANIC ***", core::str::from_utf8(&msg_buf[..msg_n]).unwrap_or("?"), core::str::from_utf8(&loc_buf[..loc_n]).unwrap_or("?"), core::str::from_utf8(&stage_buf[..stage_n]).unwrap_or(""), ]; rsnix::gop::dump_lines(&lines); // Persist to the disk so the report survives the reboot on real // hardware (no serial console). Best-effort; heap-free lines. rsnix::fs::write_crash_log(&lines); loop { unsafe { core::arch::asm!("cli; hlt"); } } } // --------------------------------------------------------------------------- // EFI entry point // --------------------------------------------------------------------------- /// EFI entry point. /// /// # Safety /// /// The firmware hands us raw pointers (image handle, system table) that /// must remain valid for the lifetime of the call — a contract guaranteed /// by the UEFI loader. #[unsafe(no_mangle)] pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut SystemTable) -> ! { let st = unsafe { &mut *system_table }; let bs = unsafe { &mut *st.boot_services }; let con_out = unsafe { &mut *st.con_out }; // Keep the runtime-services table for later (ResetSystem → poweroff). rsnix::power::init(st.runtime_services); // Capture the kernel's own image range BEFORE ExitBootServices: the // heap and page allocators must never hand out the running kernel's // code/data/bss pages (a heap allocation landing on the .bss corrupts // statics like the FAT ACTIVE_VOL — observed as a dead filesystem in // the fault path). unsafe { let mut li: *mut core::ffi::c_void = core::ptr::null_mut(); let li_guid = rsnix::uefi::EFI_LOADED_IMAGE_PROTOCOL_GUID; if (bs.handle_protocol)(image_handle, &li_guid as *const rsnix::uefi::Guid, &mut li) == rsnix::uefi::EFI_SUCCESS && !li.is_null() { let p = &*(li as *const rsnix::uefi::LoadedImageProtocol); rsnix::memory::set_kernel_image_range(p.image_base as usize, p.image_size as usize); rsnix::serial_println!( "mem: kernel image 0x{:x}..0x{:x} ({} KiB) reserved", p.image_base as usize, p.image_base as usize + p.image_size as usize, p.image_size as usize / 1024 ); } else { rsnix::serial_println!("mem: WARNING — kernel image range unknown"); } } // ---- Phase 1: UEFI boot services alive ---- // Print banner via UEFI console unsafe { let s = concat!("rsnix v", env!("CARGO_PKG_VERSION"), " booting..\n"); let bytes = s.as_bytes(); let mut buf: [u16; 48] = [0; 48]; for (i, &b) in bytes.iter().enumerate().take(47) { buf[i] = b as u16; } (con_out.output_string)(con_out, buf.as_ptr()); } rsnix::serial::init(); rsnix::serial_println!("=== rsnix boot ==="); // Locate GOP (Graphics Output Protocol) *before* the memory map is // captured: the heap/page allocators need to know the framebuffer // region so they never hand out its pages (some real firmware lists // it as conventional memory). let mut gop_interface: *mut core::ffi::c_void = core::ptr::null_mut(); let gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID; let gop_result = unsafe { (bs.locate_protocol)( &gop_guid as *const Guid, core::ptr::null_mut(), &mut gop_interface, ) }; if gop_result == EFI_SUCCESS { let gop = unsafe { &mut *(gop_interface as *mut GraphicsOutputProtocol) }; rsnix::gop::peek_fb_region(gop); } // Stash the memory map let map_key = match rsnix::memory::capture_memory_map(bs) { Ok(k) => k, Err(e) => { rsnix::serial_println!("mem: capture_memory_map failed 0x{:x}", e); 0 } }; if gop_result == EFI_SUCCESS { let gop = unsafe { &mut *(gop_interface as *mut GraphicsOutputProtocol) }; rsnix::serial_println!("gop: framebuffer found"); rsnix::gop::init(gop); } else { rsnix::serial_println!("gop: not found (0x{:x})", gop_result); } rsnix::gop::boot_marker("rsnix v0.5.10 booting.."); rsnix::vt::init(); let (rows, cols) = rsnix::vt::size(); rsnix::serial_println!("vt: terminal ready ({}x{})", rows, cols); let (fw, fh) = rsnix::gop::pixel_dims(); rsnix::gop::boot_marker(&alloc::format!( "rsnix: gop {}x{} vt {}x{} ok", fw, fh, cols, rows )); if let Some((ib, ie)) = rsnix::memory::kernel_image_range() { rsnix::gop::boot_marker(&alloc::format!( "rsnix: img 0x{:x}-0x{:x} ({} KiB)", ib, ie, (ie - ib) / 1024 )); } // ExitBootServices — we now own the machine // DEBUG (OptiPlex sim): corrupt the first key so EBS fails once and // the retry path (capture_memory_map -> init_heap rebuild) runs — the // exact condition that wiped the pre-EBS shadow on real hardware. let ebs_first_key = if rsnix::memory::simulate_ebs_failure() { map_key ^ 1 } else { map_key }; if unsafe { (bs.exit_boot_services)(image_handle, ebs_first_key) } != EFI_SUCCESS { rsnix::serial_println!("WARN: ExitBootServices failed, retrying..."); match unsafe { rsnix::memory::refresh_and_exit(bs, image_handle) } { Ok(_) => {} Err(e) => { rsnix::serial_println!("FATAL: cannot ExitBootServices (0x{:x})", e); loop { unsafe { core::arch::asm!("cli; hlt"); } } } } } // ---- Phase 2: Native mode ---- rsnix::serial::init(); rsnix::serial_println!("serial: COM1 115200 8N1"); rsnix::ps2::init(); rsnix::serial_println!("ps2: keyboard initialized"); if rsnix::ps2::mouse_init() { rsnix::serial_println!("ps2: mouse ready (IRQ12, stream mode)"); } else { rsnix::serial_println!("ps2: mouse not present"); } if rsnix::gdt::init() { rsnix::serial_println!("gdt: own GDT + TSS (IST: page-fault/double-fault)"); } else { rsnix::serial_println!("gdt: kept UEFI GDT (IST fault stacks off)"); } rsnix::interrupts::init(); rsnix::serial_println!("intr: IDT loaded, PIC remapped, timer ~100 Hz"); // Prove the timer actually ticks before handing the machine to the // scheduler: if the PIC/APIC route is broken on real hardware, the // first `hlt` (shell input, idle) never wakes and the system silently // freezes. Spin briefly with interrupts on and require uptime to // advance; a dead timer becomes a visible panic instead. let t0 = rsnix::interrupts::uptime_ms(); let mut timer_ok = false; for _ in 0..2_000_000 { core::hint::spin_loop(); if rsnix::interrupts::uptime_ms() != t0 { timer_ok = true; break; } } if timer_ok { rsnix::serial_println!("intr: timer verified (uptime advances)"); } else { panic!("timer: no PIT tick within 2M spins — interrupt routing broken"); } rsnix::gop::boot_marker("rsnix: serial+ps2+gdt+intr ok"); dump_memory_summary(); if rsnix::memory::self_test() { let hs = rsnix::memory::heap_stats(); rsnix::gop::boot_marker(&alloc::format!( "rsnix: heap 0x{:x}-0x{:x} ({} MiB) ok", hs.heap_start, hs.heap_end, hs.heap_size / 1024 / 1024 )); rsnix::serial_println!( "heap: self-test OK (0x{:x}..0x{:x} {} KiB free={} KiB)", hs.heap_start, hs.heap_end, hs.heap_size / 1024, hs.free_bytes / 1024 ); } else { rsnix::serial_println!("heap: self-test FAILED"); } if rsnix::memory::init_page_allocator() { rsnix::serial_println!("pages: bitmap allocator ready"); } else { rsnix::serial_println!("pages: init FAILED"); } rsnix::gop::boot_marker("rsnix: shadow ok"); if rsnix::memory::page_self_test() { rsnix::serial_println!("pages: self-test OK (alloc/free/runs)"); } else { rsnix::serial_println!("pages: self-test FAILED"); } if rsnix::paging::adopt_tables() { rsnix::serial_println!( "pg: adopted own page tables (cr3={:#x})", rsnix::paging::cr3() ); } else { rsnix::serial_println!("pg: adopt_tables FAILED"); } if rsnix::paging::self_test() { rsnix::serial_println!("pg: self-test OK (map/unmap/split)"); } else { rsnix::serial_println!("pg: self-test FAILED"); } // Scheduler init needs the page allocator (idle task's stack + guard // page) and the adopted page tables, so it runs after the memory // subsystem is fully up. The tick (interrupts::init) is already // live, which is what `sleep`/wake scheduling depends on. rsnix::sched::init(); let t = rsnix::rtc::now(); rsnix::serial_println!( "rtc: {}-{:02}-{:02} {:02}:{:02}:{:02}", t.year, t.month, t.day, t.hour, t.minute, t.second ); let devices = rsnix::pci::probe(); rsnix::serial_println!("pci: {} device(s) on all buses", devices.len()); // Disk: AHCI SATA driver, then mount every FAT32 volume under /disk//. if !rsnix::ahci::init() { rsnix::serial_println!("ahci: no disk attached"); } rsnix::gop::boot_marker(&alloc::format!( "rsnix: ahci {} disk(s)", rsnix::ahci::disk_count() )); rsnix::fat::init(); // prints one line per mounted volume rsnix::gop::boot_marker(&alloc::format!( "rsnix: fat {} volume(s) mounted", rsnix::fat::volume_count() )); let (dirs, files) = rsnix::fs::init(); rsnix::serial_println!("fs: vfs ready — {} dirs, {} files", dirs, files); let fstest = rsnix::fs::self_test(); if fstest { rsnix::serial_println!("fs: self-test OK (create/read/write/remove)"); } else { rsnix::serial_println!("fs: self-test FAILED"); } rsnix::gop::boot_marker(&alloc::format!( "rsnix: fs ok {} dirs/{} files self-test {}", dirs, files, if fstest { "ok" } else { "FAILED" } )); rsnix::serial_println!("boot: entering shell"); rsnix::gop::boot_marker("rsnix: boot done — shell"); // USB (xHCI) / EHCI mouse is polled; fall back to PS/2 if no USB mouse. // Each stage draws its own marker: a real-hardware freeze leaves the // last completed stage on the screen, so the hang point is identifiable // without a serial console. // STOPGAP: skip ALL USB host-controller init so the BIOS USB-Legacy SMI // keeps emulating the keyboard/mouse on the 8042 (PS/2). Resetting the // EHCI without a completed USBLEGSUP ownership handshake killed the // firmware's legacy emulation, leaving the OptiPlex 7010 with zero input. // While NO_USB is set, USB input is served entirely by the BIOS (IRQ1/ // IRQ12 + our PS/2 driver) and no EHCI/xHCI/UHCI register is touched. const NO_USB: bool = false; rsnix::gop::boot_marker("rsnix: usb init"); if !NO_USB { rsnix::usb::init(); } else { rsnix::serial_println!("usb: init skipped (stopgap) — BIOS USB-Legacy owns input"); } rsnix::gop::boot_marker("rsnix: ehci init"); if !NO_USB { rsnix::ehci::init(); } else { rsnix::serial_println!("ehci: init skipped (stopgap)"); } rsnix::gop::boot_marker("rsnix: uhci init"); if !NO_USB { rsnix::uhci::init(); } else { rsnix::serial_println!("uhci: init skipped (stopgap)"); } if rsnix::ps2::mouse_present() || rsnix::usb::controller_up() || rsnix::ehci::controller_up() || rsnix::uhci::controller_up() { rsnix::gop::boot_marker("rsnix: cursor spawn"); rsnix::cursor::spawn(); } else { rsnix::serial_println!("cursor: no mouse — overlay disabled"); } rsnix::gop::boot_marker("rsnix: shell run"); // The shell REPL is about to start: from here on task 0 may legitimately // park in poll_key with a fixed frame, so the watchdog's boot-phase spin // rule for task 0 is disarmed (boot-phase hangs already panicked). rsnix::sched::mark_shell_running(); // Windowed GUI: initialise the window manager and its input pump. // The console (vt) stays layer 0; GUI apps are windows composited on top. rsnix::wm::init(); { let t = rsnix::vt::home_terminal(); let (buf, w, h) = t.window_params(); let id = rsnix::wm::create_home_window(buf as *mut u32, w, h); t.set_window(id); } rsnix::wm::spawn_input_pump(); // Register the terminal-spawn callback so F1 in the WM creates a // windowed shell. The task lives here in rsdos (which depends on // both rsnix and shell). unsafe { rsnix::wm::SPAWN_TERMINAL_CB = Some(terminal_task); } shell::run(); } /// Callback registered with the WM for F1. Stores the spawn arguments /// and spawns the terminal task (can't block the input pump). static mut TERM_TITLE_BUF: [u8; 64] = [0u8; 64]; static mut TERM_TITLE_LEN: usize = 0; static mut TERM_W: usize = 0; static mut TERM_H: usize = 0; fn terminal_task(title: &str, w: usize, h: usize) { rsnix::serial_println!("term: callback invoked title='{}' {}x{}", title, w, h); // Copy title into static buffer so the spawned task can see it. let bytes = title.as_bytes(); let n = bytes.len().min(63); unsafe { TERM_TITLE_BUF[..n].copy_from_slice(&bytes[..n]); TERM_TITLE_BUF[n] = 0; TERM_TITLE_LEN = n; TERM_W = w; TERM_H = h; } match rsnix::sched::spawn(terminal_entry, "term") { Some(id) => rsnix::serial_println!("term: spawned task {}", id), None => rsnix::serial_println!("term: spawn FAILED"), } } /// The actual terminal task — reads args from statics, creates a window, /// and runs the shell. fn terminal_entry() { use rsnix::vt::Terminal; let (w, h) = unsafe { (TERM_W, TERM_H) }; let title = unsafe { core::str::from_utf8_unchecked(&TERM_TITLE_BUF[..TERM_TITLE_LEN]) }; rsnix::serial_println!("term: entry title='{}' {}x{}", title, w, h); let Some(win_id) = rsnix::wm::create_window(title, w, h) else { rsnix::serial_println!("term: create_window FAILED"); return; }; rsnix::serial_println!("term: win_id={}", win_id); let Some((buf, ww, hh)) = rsnix::wm::buffer(win_id) else { rsnix::serial_println!("term: buffer FAILED"); rsnix::wm::destroy(win_id); return; }; rsnix::serial_println!("term: buffer {:?} {}x{}", buf as *mut u32, ww, hh); // Build a Terminal that renders into the WM-allocated buffer. let mut term = Terminal::from_buffer(buf as *mut u32, ww, hh); term.set_window(win_id); rsnix::serial_println!("term: terminal created, entering shell"); // Redirect all vt output (gop_print!, put_char, …) to this terminal. rsnix::vt::set_active(&mut term as *mut Terminal); // Run the shell — it owns input, line editing, and command execution. shell::run_windowed(); // Shell exited (^D or window close). Clean up. rsnix::serial_println!("term: shell exited, cleaning up"); rsnix::vt::set_active(core::ptr::null_mut()); drop(term); rsnix::wm::destroy(win_id); rsnix::serial_println!("term: done"); } // --------------------------------------------------------------------------- // Print a memory summary from the stashed memory map // --------------------------------------------------------------------------- fn dump_memory_summary() { let (map, key) = rsnix::memory::get_map(); let mut conventional_4k = 0u64; let mut addr_space_4k = 0u64; for desc in map.iter() { let pages = desc.number_of_pages; addr_space_4k += pages; if desc.kind == 7 { conventional_4k += pages; } } rsnix::serial_println!( "mem: {} entries, key=0x{:x}, usable: {} KB ({} MB)", map.len(), key, conventional_4k * 4, (conventional_4k * 4) / 1024, ); rsnix::serial_println!( "mem: address space: {} MB (incl. PCIe MMIO)", (addr_space_4k * 4) / 1024, ); }