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. --- rsnix/src/exec.rs | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 rsnix/src/exec.rs (limited to 'rsnix/src/exec.rs') diff --git a/rsnix/src/exec.rs b/rsnix/src/exec.rs new file mode 100644 index 0000000..7ba39aa --- /dev/null +++ b/rsnix/src/exec.rs @@ -0,0 +1,137 @@ +// --------------------------------------------------------------------------- +// exec — run an external binary (ADR 0006 Phase 2). +// +// Resolution: name → \bin\\ on each mounted volume → None. +// The shell calls exec::spawn with a fully constructed Exports table +// (streams point at its pipeline/tty) and clean argv. The binary runs as +// its own task; on exit it posts TaskMsg::User(exit_code) to the spawner. +// +// The Exports table and argv block are heap boxes moved into static slots +// indexed by task id — the trampoline adopts them before jumping to the +// image entry. One slot per MAX_TASKS; spawn fails if the slot is busy. +// --------------------------------------------------------------------------- +use crate::load; +use crate::sched::{self, TaskMsg}; +use alloc::string::String; +use alloc::vec::Vec; +use rsapi::{Arg, EntryPoint, Exports}; + +/// Per-task invocation context adopted by the trampoline. +struct ExecCtx { + exports: &'static Exports, + /// Heap block holding the argv strings (kept alive until task exit — + /// the Arg pointers reference into it). + #[allow(dead_code)] + argv_buf: Vec, + args: Vec, + /// Image pages to free when the task dies. + image: load::Image, +} + +static mut CTX: [Option; sched::MAX_TASKS] = [const { None }; sched::MAX_TASKS]; + +/// Error of exec::resolve / exec::spawn. +pub enum ExecError { + NotFound, + LoadFailed(&'static str), + Busy, +} + +/// Scan every mounted volume for `\bin\\` (LFN exact match). +/// Returns the VFS handle of the first hit. +pub fn resolve(kind: &str, name: &str) -> Option { + for vol in 0..crate::fat::volume_count() { + if !crate::fat::is_mounted(vol) { + continue; + } + let uid = String::from(crate::fat::uid(vol)); + let path = alloc::format!("/disk/{}/bin/{}/{}.efi", uid, kind, name); + if let Some(h) = crate::fs::resolve(&path) { + return Some(h); + } + } + None +} + +/// Read the whole file into a heap buffer (public: the shell uses this +/// to fetch binary bytes before calling load::load_image). +#[allow(dead_code)] +pub fn read_file(h: usize) -> Result, ()> { + let size = crate::fs::size(h) as usize; + let mut buf = Vec::with_capacity(size); + let mut chunk = [0u8; 512]; + let mut pos = 0u64; + loop { + let n = crate::fs::read(h, &mut chunk, pos)?; + if n == 0 { + break; + } + buf.extend_from_slice(&chunk[..n]); + pos += n as u64; + } + Ok(buf) +} + +/// Spawn `image` as a new task running entry(exports, argc, argv). +/// +/// The caller keeps `exports` alive (it is constructed per invocation in +/// shell-owned memory that outlives the command — the pipeline/tty live +/// in the blocked spawner). +pub fn spawn( + image: load::Image, + exports: &'static Exports, + argv: &[String], + name: &str, +) -> Result { + // Copy the argv strings into one contiguous block owned by the task. + let mut argv_buf: Vec = Vec::new(); + let mut args: Vec = Vec::with_capacity(argv.len()); + for a in argv { + let off = argv_buf.len(); + argv_buf.extend_from_slice(a.as_bytes()); + args.push(Arg { + ptr: unsafe { argv_buf.as_ptr().add(off) }, + len: a.len(), + }); + } + + // Reserve a task slot first so the context is in place before the + // task can possibly run (spawn may schedule immediately). + let id = sched::reserve_slot().ok_or(ExecError::Busy)?; + unsafe { + CTX[id] = Some(ExecCtx { + exports, + argv_buf, + args, + image, + }); + } + if !sched::launch_at(id, trampoline, name) { + unsafe { + CTX[id] = None; + } + return Err(ExecError::Busy); + } + Ok(id) +} + +/// Trampoline: adopt the exec context, call the entry, report completion. +fn trampoline() { + let id = sched::current(); + let taken = unsafe { + let ctx_ptr = core::ptr::addr_of_mut!(CTX); + (*ctx_ptr)[id].take() + }; + let Some(ctx) = taken else { + sched::abort_current("exec: no context") + }; + // Call the image entry with the table; the return value is the exit code. + let code = unsafe { + let f: EntryPoint = core::mem::transmute(ctx.image.entry); + f(ctx.exports, ctx.args.len(), ctx.args.as_ptr()) + }; + // Report completion to the spawner (the shell), then die. The spawner + // frees the image pages after reaping the message. + let _ = sched::send(sched::SHELL_TID, TaskMsg::User(code as u64)); + drop(ctx); +} -- cgit v1.2.3