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. --- rsapi/src/lib.rs | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 rsapi/src/lib.rs (limited to 'rsapi/src/lib.rs') diff --git a/rsapi/src/lib.rs b/rsapi/src/lib.rs new file mode 100644 index 0000000..f71e69f --- /dev/null +++ b/rsapi/src/lib.rs @@ -0,0 +1,86 @@ +//! rsapi — the kernel↔binary ABI (ADR 0006). +//! +//! One versioned `Exports` table is constructed per invocation and handed +//! to the binary's entry point. The header starts with a magic and a +//! `version: u32` so a future kernel can keep old binaries working +//! (dispatch on the version, never on struct layout). +//! +//! Everything here is `no_std`-clean: plain structs and function pointers, +//! no allocation, no rsnix dependency — binaries link only this crate. + +#![no_std] + +/// Table magic: "RSEX". +pub const EXPORTS_MAGIC: u32 = 0x5245_5853; + +/// ABI revision. Bump on any signature change; the kernel refuses to run +/// binaries whose required major exceeds its own. +pub const ABI_VERSION: u32 = 1; + +/// Entry-point signature. `extern "C"` on the x86_64-unknown-uefi +/// target is the Win64 convention: the three arguments arrive in +/// RCX/RDX/R8 on both sides of the table. +pub type EntryPoint = extern "C" fn(exports: &'static Exports, argc: usize, argv: *const Arg) -> i32; + +/// One C-style argv element: pointer + length (rs-dos strings are not +/// NUL-terminated in memory). +#[derive(Clone, Copy)] +pub struct Arg { + pub ptr: *const u8, + pub len: usize, +} + +/// A line of text handed across the ABI (stream I/O). +#[derive(Clone, Copy)] +pub struct Line { + pub ptr: *const u8, + pub len: usize, +} + +impl Line { + /// Borrow as a &str (lossy is impossible: lines are always UTF-8). + pub fn as_str(&self) -> &'static str { + unsafe { core::str::from_utf8_unchecked(core::slice::from_raw_parts(self.ptr, self.len)) } + } +} + +/// Outcome of `get_line`. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum StreamResult { + /// A line is available (see `out`). + Line, + /// Input exhausted. + Eof, + /// Aborted (^C or upstream killed); stop immediately. + Abort, +} + +// --------------------------------------------------------------------------- +// Exports table — v1 +// --------------------------------------------------------------------------- + +/// The function table a binary receives. Layout is fixed by `version`; +/// new functions are appended, existing ones never move. +#[repr(C)] +pub struct Exports { + // ---- header ---- + pub magic: u32, + pub version: u32, + + // ---- console / streams (v1) ---- + /// Print one line to the command's output stream. + /// Returns false when the consumer says stop (pager quit / head limit / + /// ^C) — the binary should wrap up promptly. + pub put_line: fn(line: Line) -> bool, + /// Read one line from the command's input stream (only meaningful for + /// input consumers like cat/hexdump; otherwise returns Eof). + /// `out` receives the line when the result is `StreamResult::Line`. + pub get_line: fn(out: &mut Line) -> StreamResult, + + // ---- lifecycle (v1) ---- + /// Log one line to the kernel log ring (/dev/klog). + pub log_line: fn(line: Line), + /// Exit code for the completion message to the spawner. + /// (Calling this is optional; returning from the entry point exits 0.) + pub exit: fn(code: i32) -> !, +} -- cgit v1.2.3