From 71b6445b048456bf198d8b622570db95f4665cb9 Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 21 Aug 2026 21:17:44 +0200 Subject: exec: PE32+ loader, versioned ABI, first external binaries (v0.6.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rsapi: the kernel<->binary ABI crate (no_std, no deps). Versioned Exports table (magic + version header), EntryPoint = extern "C" on x86_64-unknown-uefi (Win64: RCX/RDX/R8), Line/Arg/StreamResult types. Binaries link only this crate. rsnix::load: PE32+ parser — DOS/COFF/optional headers, section copy, BSS zero-fill, .reloc base relocations (DIR64) via rva_to_file mapping (the reloc directory is an RVA, not a file offset). Images are allocated at LOAD_BASE=192MiB inside the page allocator's conventional span and identity-mapped user+writable+nx. rsnix::exec: resolution name -> /disk//bin//.efi per mounted volume; spawn reserves a sched slot, stores a per-task ExecCtx (exports + argv block + image) before launch_at installs the frame — no adopt race. Trampoline calls entry(&Exports, argc, argv), posts TaskMsg::User(exit_code) to the shell. abort_current notifies the shell with u64::MAX when an exec'd binary dies, so a faulting command cannot leave the shell blocked forever. sched: reserve_slot/launch_at split out of spawn; task_exit made pub. shell: exec_external runs before non-frozen built-ins (ADR 0006 resolution rule); frozen core (help/clear/echo/halt/poweroff/exit/ rescan) is exempt. Exports v1 wires put_line to the terminal and get_line to EOF; stream injection lands in Phase 3. ^C while waiting kills the command task. bins/{hello,boom}: gate binaries. hello prints through the exports table; boom faults on 0x400000000000 to prove containment. The image entry point IS the ABI entry (efi_main carries the Exports signature). Version 0.6.0: exec exists end-to-end. Gates verified in QEMU: 'hello' loads from \\bin\\cli\\hello.efi, calls put_line across the ABI, exits 0, shell unblocks; 'boom' faults, task dies gracefully with full report, shell survives, jobs shows the corpse; built-in fallback works when the file is absent; test-disk PASS. --- shell/Cargo.toml | 1 + shell/src/lib.rs | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) (limited to 'shell') diff --git a/shell/Cargo.toml b/shell/Cargo.toml index f95b574..b59dfec 100644 --- a/shell/Cargo.toml +++ b/shell/Cargo.toml @@ -8,5 +8,6 @@ workspace = true [dependencies] rsnix = { path = "../rsnix" } +rsapi = { path = "../rsapi" } rsdosedit = { path = "../rsdosedit" } rspaint = { path = "../rspaint" } diff --git a/shell/src/lib.rs b/shell/src/lib.rs index d1710cc..9ca999e 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs @@ -745,6 +745,15 @@ write `{} TIL `", } } + // Resolution rule (ADR 0006): \bin\\ first, else the + // built-in. The frozen core (help/clear/echo/poweroff/exit/halt/ + // rescan) is exempt — it exists to fix the system and never migrates. + if !matches!(cmd, "help" | "clear" | "echo" | "halt" | "poweroff" | "exit" | "rescan") + && exec_external(&parsed) + { + return; + } + match cmd { "help" => cmd_help(&parsed), "echo" => cmd_echo(&parsed), @@ -791,6 +800,108 @@ write `{} TIL `", } } +// --------------------------------------------------------------------------- +// External binaries (ADR 0006 Phase 2) +// +// Resolution: \bin\cli\ on each mounted volume; else "unknown +// command". The Exports table is constructed per invocation: v1 wires +// put_line to the active terminal and get_line to EOF (stream injection +// through the filter pipeline lands in Phase 3). +// --------------------------------------------------------------------------- + +static mut LAST_EXIT_CODE: i32 = 0; + +/// Last exit code of an external command ($? equivalent for scripts). +pub fn last_exit_code() -> i32 { + unsafe { core::ptr::addr_of!(LAST_EXIT_CODE).read() } +} + +/// ABI callback: print one line to the invoking terminal. +fn abi_put_line(line: rsapi::Line) -> bool { + term_println(line.as_str()); + true +} + +/// ABI callback: read one input line — no stream source wired yet (Phase 3). +fn abi_get_line(_out: &mut rsapi::Line) -> rsapi::StreamResult { + rsapi::StreamResult::Eof +} + +/// ABI callback: kernel log. +fn abi_log_line(line: rsapi::Line) { + rsnix::klog::append(line.as_str(), 0); +} + +/// ABI callback: exit with a code. +fn abi_exit(code: i32) -> ! { + unsafe { + LAST_EXIT_CODE = code; + } + // Unwind the binary by longjmp-style task abort: the trampoline never + // returns here. We report the code via the static and kill the task. + rsnix::sched::abort_current("exit") +} + +static EXPORTS_V1: rsapi::Exports = rsapi::Exports { + magic: rsapi::EXPORTS_MAGIC, + version: rsapi::ABI_VERSION, + put_line: abi_put_line, + get_line: abi_get_line, + log_line: abi_log_line, + exit: abi_exit, +}; + +/// Run `cmd` as an external PE32+ binary from /bin/cli/. Returns false +/// when no external binary resolved (the caller falls back to built-ins). +fn exec_external(p: &Parsed) -> bool { + let Some(h) = rsnix::exec::resolve("cli", &p.cmd) else { + return false; + }; + let bytes = match rsnix::exec::read_file(h) { + Ok(b) => b, + Err(_) => { + term_err(&alloc::format!("{}: read failed", p.cmd)); + return true; + } + }; + let image = match rsnix::load::load_image(&bytes) { + Ok(img) => img, + Err(e) => { + term_err(&alloc::format!("{}: cannot load ({:?})", p.cmd, e)); + return true; + } + }; + let mut argv: Vec = Vec::with_capacity(p.args.len() + 1); + argv.push(p.cmd.clone()); + argv.extend(p.args.iter().cloned()); + let name = alloc::format!("{}", p.cmd); + match rsnix::exec::spawn(image, &EXPORTS_V1, &argv, &name) { + Ok(id) => { + // Wait for the completion message (TaskMsg::User(exit_code)). + loop { + match rsnix::sched::recv_blocking() { + rsnix::sched::TaskMsg::User(code) => { + unsafe { + LAST_EXIT_CODE = code as i32; + } + break; + } + rsnix::sched::TaskMsg::Key(rsnix::ps2::KeyEvent::Ctrl('c')) => { + // ^C while waiting: kill the command task. + let _ = rsnix::sched::kill(id); + term_err("^C"); + break; + } + _ => {} + } + } + } + Err(rsnix::exec::ExecError::Busy) => term_err(&alloc::format!("{}: task table full", p.cmd)), + Err(_) => term_err(&alloc::format!("{}: cannot start", p.cmd)), + } + true +} + // --------------------------------------------------------------------------- // Built-in commands // --------------------------------------------------------------------------- -- cgit v1.2.3