diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-21 21:17:44 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-21 21:17:44 +0200 |
| commit | 71b6445b048456bf198d8b622570db95f4665cb9 (patch) | |
| tree | 808e73fa95709ed9c2d409aecaf275fd70365293 /rsapi | |
| parent | input: total keyboard routing + per-terminal discipline (ADR 0006 ph1) (diff) | |
| download | rs-dos-71b6445b048456bf198d8b622570db95f4665cb9.tar.gz rs-dos-71b6445b048456bf198d8b622570db95f4665cb9.zip | |
exec: PE32+ loader, versioned ABI, first external binaries (v0.6.0)
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/<UID>/bin/<kind>/<name>.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.
Diffstat (limited to '')
| -rw-r--r-- | rsapi/Cargo.toml | 7 | ||||
| -rw-r--r-- | rsapi/src/lib.rs | 86 |
2 files changed, 93 insertions, 0 deletions
diff --git a/rsapi/Cargo.toml b/rsapi/Cargo.toml new file mode 100644 index 0000000..fb43004 --- /dev/null +++ b/rsapi/Cargo.toml | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | [package] | ||
| 2 | name = "rsapi" | ||
| 3 | version.workspace = true | ||
| 4 | edition.workspace = true | ||
| 5 | |||
| 6 | [lints] | ||
| 7 | workspace = true | ||
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 @@ | |||
| 1 | //! rsapi — the kernel↔binary ABI (ADR 0006). | ||
| 2 | //! | ||
| 3 | //! One versioned `Exports` table is constructed per invocation and handed | ||
| 4 | //! to the binary's entry point. The header starts with a magic and a | ||
| 5 | //! `version: u32` so a future kernel can keep old binaries working | ||
| 6 | //! (dispatch on the version, never on struct layout). | ||
| 7 | //! | ||
| 8 | //! Everything here is `no_std`-clean: plain structs and function pointers, | ||
| 9 | //! no allocation, no rsnix dependency — binaries link only this crate. | ||
| 10 | |||
| 11 | #![no_std] | ||
| 12 | |||
| 13 | /// Table magic: "RSEX". | ||
| 14 | pub const EXPORTS_MAGIC: u32 = 0x5245_5853; | ||
| 15 | |||
| 16 | /// ABI revision. Bump on any signature change; the kernel refuses to run | ||
| 17 | /// binaries whose required major exceeds its own. | ||
| 18 | pub const ABI_VERSION: u32 = 1; | ||
| 19 | |||
| 20 | /// Entry-point signature. `extern "C"` on the x86_64-unknown-uefi | ||
| 21 | /// target is the Win64 convention: the three arguments arrive in | ||
| 22 | /// RCX/RDX/R8 on both sides of the table. | ||
| 23 | pub type EntryPoint = extern "C" fn(exports: &'static Exports, argc: usize, argv: *const Arg) -> i32; | ||
| 24 | |||
| 25 | /// One C-style argv element: pointer + length (rs-dos strings are not | ||
| 26 | /// NUL-terminated in memory). | ||
| 27 | #[derive(Clone, Copy)] | ||
| 28 | pub struct Arg { | ||
| 29 | pub ptr: *const u8, | ||
| 30 | pub len: usize, | ||
| 31 | } | ||
| 32 | |||
| 33 | /// A line of text handed across the ABI (stream I/O). | ||
| 34 | #[derive(Clone, Copy)] | ||
| 35 | pub struct Line { | ||
| 36 | pub ptr: *const u8, | ||
| 37 | pub len: usize, | ||
| 38 | } | ||
| 39 | |||
| 40 | impl Line { | ||
| 41 | /// Borrow as a &str (lossy is impossible: lines are always UTF-8). | ||
| 42 | pub fn as_str(&self) -> &'static str { | ||
| 43 | unsafe { core::str::from_utf8_unchecked(core::slice::from_raw_parts(self.ptr, self.len)) } | ||
| 44 | } | ||
| 45 | } | ||
| 46 | |||
| 47 | /// Outcome of `get_line`. | ||
| 48 | #[derive(Clone, Copy, PartialEq, Eq)] | ||
| 49 | pub enum StreamResult { | ||
| 50 | /// A line is available (see `out`). | ||
| 51 | Line, | ||
| 52 | /// Input exhausted. | ||
| 53 | Eof, | ||
| 54 | /// Aborted (^C or upstream killed); stop immediately. | ||
| 55 | Abort, | ||
| 56 | } | ||
| 57 | |||
| 58 | // --------------------------------------------------------------------------- | ||
| 59 | // Exports table — v1 | ||
| 60 | // --------------------------------------------------------------------------- | ||
| 61 | |||
| 62 | /// The function table a binary receives. Layout is fixed by `version`; | ||
| 63 | /// new functions are appended, existing ones never move. | ||
| 64 | #[repr(C)] | ||
| 65 | pub struct Exports { | ||
| 66 | // ---- header ---- | ||
| 67 | pub magic: u32, | ||
| 68 | pub version: u32, | ||
| 69 | |||
| 70 | // ---- console / streams (v1) ---- | ||
| 71 | /// Print one line to the command's output stream. | ||
| 72 | /// Returns false when the consumer says stop (pager quit / head limit / | ||
| 73 | /// ^C) — the binary should wrap up promptly. | ||
| 74 | pub put_line: fn(line: Line) -> bool, | ||
| 75 | /// Read one line from the command's input stream (only meaningful for | ||
| 76 | /// input consumers like cat/hexdump; otherwise returns Eof). | ||
| 77 | /// `out` receives the line when the result is `StreamResult::Line`. | ||
| 78 | pub get_line: fn(out: &mut Line) -> StreamResult, | ||
| 79 | |||
| 80 | // ---- lifecycle (v1) ---- | ||
| 81 | /// Log one line to the kernel log ring (/dev/klog). | ||
| 82 | pub log_line: fn(line: Line), | ||
| 83 | /// Exit code for the completion message to the spawner. | ||
| 84 | /// (Calling this is optional; returning from the entry point exits 0.) | ||
| 85 | pub exit: fn(code: i32) -> !, | ||
| 86 | } | ||
