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 | |
| 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.
| -rw-r--r-- | Cargo.lock | 28 | ||||
| -rw-r--r-- | Cargo.toml | 4 | ||||
| -rw-r--r-- | bins/boom/Cargo.toml | 10 | ||||
| -rw-r--r-- | bins/boom/src/main.rs | 31 | ||||
| -rw-r--r-- | bins/hello/Cargo.toml | 10 | ||||
| -rw-r--r-- | bins/hello/src/main.rs | 33 | ||||
| -rw-r--r-- | rsapi/Cargo.toml | 7 | ||||
| -rw-r--r-- | rsapi/src/lib.rs | 86 | ||||
| -rw-r--r-- | rsnix/Cargo.toml | 3 | ||||
| -rw-r--r-- | rsnix/src/exec.rs | 137 | ||||
| -rw-r--r-- | rsnix/src/lib.rs | 2 | ||||
| -rw-r--r-- | rsnix/src/load.rs | 283 | ||||
| -rw-r--r-- | rsnix/src/sched.rs | 46 | ||||
| -rw-r--r-- | shell/Cargo.toml | 1 | ||||
| -rw-r--r-- | shell/src/lib.rs | 111 |
15 files changed, 786 insertions, 6 deletions
| @@ -3,8 +3,26 @@ | |||
| 3 | version = 4 | 3 | version = 4 |
| 4 | 4 | ||
| 5 | [[package]] | 5 | [[package]] |
| 6 | name = "boom" | ||
| 7 | version = "0.6.0" | ||
| 8 | dependencies = [ | ||
| 9 | "rsapi", | ||
| 10 | ] | ||
| 11 | |||
| 12 | [[package]] | ||
| 13 | name = "hello" | ||
| 14 | version = "0.6.0" | ||
| 15 | dependencies = [ | ||
| 16 | "rsapi", | ||
| 17 | ] | ||
| 18 | |||
| 19 | [[package]] | ||
| 20 | name = "rsapi" | ||
| 21 | version = "0.6.0" | ||
| 22 | |||
| 23 | [[package]] | ||
| 6 | name = "rsdos" | 24 | name = "rsdos" |
| 7 | version = "0.5.11" | 25 | version = "0.6.0" |
| 8 | dependencies = [ | 26 | dependencies = [ |
| 9 | "rsnix", | 27 | "rsnix", |
| 10 | "shell", | 28 | "shell", |
| @@ -19,7 +37,10 @@ dependencies = [ | |||
| 19 | 37 | ||
| 20 | [[package]] | 38 | [[package]] |
| 21 | name = "rsnix" | 39 | name = "rsnix" |
| 22 | version = "0.5.11" | 40 | version = "0.6.0" |
| 41 | dependencies = [ | ||
| 42 | "rsapi", | ||
| 43 | ] | ||
| 23 | 44 | ||
| 24 | [[package]] | 45 | [[package]] |
| 25 | name = "rspaint" | 46 | name = "rspaint" |
| @@ -30,8 +51,9 @@ dependencies = [ | |||
| 30 | 51 | ||
| 31 | [[package]] | 52 | [[package]] |
| 32 | name = "shell" | 53 | name = "shell" |
| 33 | version = "0.5.11" | 54 | version = "0.6.0" |
| 34 | dependencies = [ | 55 | dependencies = [ |
| 56 | "rsapi", | ||
| 35 | "rsdosedit", | 57 | "rsdosedit", |
| 36 | "rsnix", | 58 | "rsnix", |
| 37 | "rspaint", | 59 | "rspaint", |
| @@ -1,9 +1,9 @@ | |||
| 1 | [workspace] | 1 | [workspace] |
| 2 | resolver = "2" | 2 | resolver = "2" |
| 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit", "rspaint"] | 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit", "rspaint", "rsapi", "bins/hello", "bins/boom"] |
| 4 | 4 | ||
| 5 | [workspace.package] | 5 | [workspace.package] |
| 6 | version = "0.5.11" | 6 | version = "0.6.0" |
| 7 | edition = "2024" | 7 | edition = "2024" |
| 8 | 8 | ||
| 9 | # no_std UEFI kernel: no unwinder available, so panic must abort. | 9 | # no_std UEFI kernel: no unwinder available, so panic must abort. |
diff --git a/bins/boom/Cargo.toml b/bins/boom/Cargo.toml new file mode 100644 index 0000000..2d31886 --- /dev/null +++ b/bins/boom/Cargo.toml | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | [package] | ||
| 2 | name = "boom" | ||
| 3 | version.workspace = true | ||
| 4 | edition.workspace = true | ||
| 5 | |||
| 6 | [lints] | ||
| 7 | workspace = true | ||
| 8 | |||
| 9 | [dependencies] | ||
| 10 | rsapi = { path = "../../rsapi" } | ||
diff --git a/bins/boom/src/main.rs b/bins/boom/src/main.rs new file mode 100644 index 0000000..0c3201b --- /dev/null +++ b/bins/boom/src/main.rs | |||
| @@ -0,0 +1,31 @@ | |||
| 1 | //! boom — crash-containment probe (ADR 0006 Phase 2 gate). | ||
| 2 | //! | ||
| 3 | //! Deliberately faults on an unmapped address. The kernel must kill only | ||
| 4 | //! this task; the shell survives and `jobs` shows the corpse. | ||
| 5 | |||
| 6 | #![no_std] | ||
| 7 | #![no_main] | ||
| 8 | |||
| 9 | #[panic_handler] | ||
| 10 | fn panic(_info: &core::panic::PanicInfo) -> ! { | ||
| 11 | loop { | ||
| 12 | core::hint::spin_loop(); | ||
| 13 | } | ||
| 14 | } | ||
| 15 | |||
| 16 | /// The image entry point — the loader jumps here with the ABI arguments. | ||
| 17 | #[unsafe(no_mangle)] | ||
| 18 | pub extern "C" fn efi_main( | ||
| 19 | _exports: &'static rsapi::Exports, | ||
| 20 | _argc: usize, | ||
| 21 | _argv: *const rsapi::Arg, | ||
| 22 | ) -> i32 { | ||
| 23 | // Canonical-but-unmapped address (below 2^47, above any RAM/MMIO): | ||
| 24 | // the page walk terminates at a not-present entry → #PF with | ||
| 25 | // CR2 = address. Same trick as the built-in `fault` command. | ||
| 26 | unsafe { | ||
| 27 | let p = 0x4000_0000_0000usize as *mut u64; | ||
| 28 | p.write_volatile(0xDEAD_BEEF); | ||
| 29 | } | ||
| 30 | 0 | ||
| 31 | } | ||
diff --git a/bins/hello/Cargo.toml b/bins/hello/Cargo.toml new file mode 100644 index 0000000..ac71ded --- /dev/null +++ b/bins/hello/Cargo.toml | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | [package] | ||
| 2 | name = "hello" | ||
| 3 | version.workspace = true | ||
| 4 | edition.workspace = true | ||
| 5 | |||
| 6 | [lints] | ||
| 7 | workspace = true | ||
| 8 | |||
| 9 | [dependencies] | ||
| 10 | rsapi = { path = "../../rsapi" } | ||
diff --git a/bins/hello/src/main.rs b/bins/hello/src/main.rs new file mode 100644 index 0000000..d1d58df --- /dev/null +++ b/bins/hello/src/main.rs | |||
| @@ -0,0 +1,33 @@ | |||
| 1 | //! hello — the first rs-dos external binary (ADR 0006 Phase 2 gate). | ||
| 2 | //! | ||
| 3 | //! Prints one line through the injected Exports table and exits 0. | ||
| 4 | //! Exercises the whole chain: PE32+ load, relocation, ABI call, task exit. | ||
| 5 | |||
| 6 | #![no_std] | ||
| 7 | #![no_main] | ||
| 8 | |||
| 9 | use rsapi::Exports; | ||
| 10 | |||
| 11 | #[panic_handler] | ||
| 12 | fn panic(_info: &core::panic::PanicInfo) -> ! { | ||
| 13 | loop { | ||
| 14 | core::hint::spin_loop(); | ||
| 15 | } | ||
| 16 | } | ||
| 17 | |||
| 18 | /// The image entry point — the loader jumps here with the ABI arguments | ||
| 19 | /// (exports table, argc, argv). The conventional UEFI name is kept so | ||
| 20 | /// the PE looks normal; the signature is the rs-dos ABI. | ||
| 21 | #[unsafe(no_mangle)] | ||
| 22 | pub extern "C" fn efi_main(exports: &'static Exports, _argc: usize, _argv: *const rsapi::Arg) -> i32 { | ||
| 23 | let msg = b"hello, world - from an external binary"; | ||
| 24 | let line = rsapi::Line { | ||
| 25 | ptr: msg.as_ptr(), | ||
| 26 | len: msg.len(), | ||
| 27 | }; | ||
| 28 | if (exports.put_line)(line) { | ||
| 29 | 0 | ||
| 30 | } else { | ||
| 31 | 1 | ||
| 32 | } | ||
| 33 | } | ||
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 | } | ||
diff --git a/rsnix/Cargo.toml b/rsnix/Cargo.toml index 5225b1a..f0dbad1 100644 --- a/rsnix/Cargo.toml +++ b/rsnix/Cargo.toml | |||
| @@ -5,3 +5,6 @@ edition.workspace = true | |||
| 5 | 5 | ||
| 6 | [lints] | 6 | [lints] |
| 7 | workspace = true | 7 | workspace = true |
| 8 | |||
| 9 | [dependencies] | ||
| 10 | rsapi = { path = "../rsapi" } | ||
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 @@ | |||
| 1 | // --------------------------------------------------------------------------- | ||
| 2 | // exec — run an external binary (ADR 0006 Phase 2). | ||
| 3 | // | ||
| 4 | // Resolution: name → \bin\<kind>\<name> on each mounted volume → None. | ||
| 5 | // The shell calls exec::spawn with a fully constructed Exports table | ||
| 6 | // (streams point at its pipeline/tty) and clean argv. The binary runs as | ||
| 7 | // its own task; on exit it posts TaskMsg::User(exit_code) to the spawner. | ||
| 8 | // | ||
| 9 | // The Exports table and argv block are heap boxes moved into static slots | ||
| 10 | // indexed by task id — the trampoline adopts them before jumping to the | ||
| 11 | // image entry. One slot per MAX_TASKS; spawn fails if the slot is busy. | ||
| 12 | // --------------------------------------------------------------------------- | ||
| 13 | use crate::load; | ||
| 14 | use crate::sched::{self, TaskMsg}; | ||
| 15 | use alloc::string::String; | ||
| 16 | use alloc::vec::Vec; | ||
| 17 | use rsapi::{Arg, EntryPoint, Exports}; | ||
| 18 | |||
| 19 | /// Per-task invocation context adopted by the trampoline. | ||
| 20 | struct ExecCtx { | ||
| 21 | exports: &'static Exports, | ||
| 22 | /// Heap block holding the argv strings (kept alive until task exit — | ||
| 23 | /// the Arg pointers reference into it). | ||
| 24 | #[allow(dead_code)] | ||
| 25 | argv_buf: Vec<u8>, | ||
| 26 | args: Vec<Arg>, | ||
| 27 | /// Image pages to free when the task dies. | ||
| 28 | image: load::Image, | ||
| 29 | } | ||
| 30 | |||
| 31 | static mut CTX: [Option<ExecCtx>; sched::MAX_TASKS] = [const { None }; sched::MAX_TASKS]; | ||
| 32 | |||
| 33 | /// Error of exec::resolve / exec::spawn. | ||
| 34 | pub enum ExecError { | ||
| 35 | NotFound, | ||
| 36 | LoadFailed(&'static str), | ||
| 37 | Busy, | ||
| 38 | } | ||
| 39 | |||
| 40 | /// Scan every mounted volume for `\bin\<kind>\<name>` (LFN exact match). | ||
| 41 | /// Returns the VFS handle of the first hit. | ||
| 42 | pub fn resolve(kind: &str, name: &str) -> Option<usize> { | ||
| 43 | for vol in 0..crate::fat::volume_count() { | ||
| 44 | if !crate::fat::is_mounted(vol) { | ||
| 45 | continue; | ||
| 46 | } | ||
| 47 | let uid = String::from(crate::fat::uid(vol)); | ||
| 48 | let path = alloc::format!("/disk/{}/bin/{}/{}.efi", uid, kind, name); | ||
| 49 | if let Some(h) = crate::fs::resolve(&path) { | ||
| 50 | return Some(h); | ||
| 51 | } | ||
| 52 | } | ||
| 53 | None | ||
| 54 | } | ||
| 55 | |||
| 56 | /// Read the whole file into a heap buffer (public: the shell uses this | ||
| 57 | /// to fetch binary bytes before calling load::load_image). | ||
| 58 | #[allow(dead_code)] | ||
| 59 | pub fn read_file(h: usize) -> Result<Vec<u8>, ()> { | ||
| 60 | let size = crate::fs::size(h) as usize; | ||
| 61 | let mut buf = Vec::with_capacity(size); | ||
| 62 | let mut chunk = [0u8; 512]; | ||
| 63 | let mut pos = 0u64; | ||
| 64 | loop { | ||
| 65 | let n = crate::fs::read(h, &mut chunk, pos)?; | ||
| 66 | if n == 0 { | ||
| 67 | break; | ||
| 68 | } | ||
| 69 | buf.extend_from_slice(&chunk[..n]); | ||
| 70 | pos += n as u64; | ||
| 71 | } | ||
| 72 | Ok(buf) | ||
| 73 | } | ||
| 74 | |||
| 75 | /// Spawn `image` as a new task running entry(exports, argc, argv). | ||
| 76 | /// | ||
| 77 | /// The caller keeps `exports` alive (it is constructed per invocation in | ||
| 78 | /// shell-owned memory that outlives the command — the pipeline/tty live | ||
| 79 | /// in the blocked spawner). | ||
| 80 | pub fn spawn( | ||
| 81 | image: load::Image, | ||
| 82 | exports: &'static Exports, | ||
| 83 | argv: &[String], | ||
| 84 | name: &str, | ||
| 85 | ) -> Result<usize, ExecError> { | ||
| 86 | // Copy the argv strings into one contiguous block owned by the task. | ||
| 87 | let mut argv_buf: Vec<u8> = Vec::new(); | ||
| 88 | let mut args: Vec<Arg> = Vec::with_capacity(argv.len()); | ||
| 89 | for a in argv { | ||
| 90 | let off = argv_buf.len(); | ||
| 91 | argv_buf.extend_from_slice(a.as_bytes()); | ||
| 92 | args.push(Arg { | ||
| 93 | ptr: unsafe { argv_buf.as_ptr().add(off) }, | ||
| 94 | len: a.len(), | ||
| 95 | }); | ||
| 96 | } | ||
| 97 | |||
| 98 | // Reserve a task slot first so the context is in place before the | ||
| 99 | // task can possibly run (spawn may schedule immediately). | ||
| 100 | let id = sched::reserve_slot().ok_or(ExecError::Busy)?; | ||
| 101 | unsafe { | ||
| 102 | CTX[id] = Some(ExecCtx { | ||
| 103 | exports, | ||
| 104 | argv_buf, | ||
| 105 | args, | ||
| 106 | image, | ||
| 107 | }); | ||
| 108 | } | ||
| 109 | if !sched::launch_at(id, trampoline, name) { | ||
| 110 | unsafe { | ||
| 111 | CTX[id] = None; | ||
| 112 | } | ||
| 113 | return Err(ExecError::Busy); | ||
| 114 | } | ||
| 115 | Ok(id) | ||
| 116 | } | ||
| 117 | |||
| 118 | /// Trampoline: adopt the exec context, call the entry, report completion. | ||
| 119 | fn trampoline() { | ||
| 120 | let id = sched::current(); | ||
| 121 | let taken = unsafe { | ||
| 122 | let ctx_ptr = core::ptr::addr_of_mut!(CTX); | ||
| 123 | (*ctx_ptr)[id].take() | ||
| 124 | }; | ||
| 125 | let Some(ctx) = taken else { | ||
| 126 | sched::abort_current("exec: no context") | ||
| 127 | }; | ||
| 128 | // Call the image entry with the table; the return value is the exit code. | ||
| 129 | let code = unsafe { | ||
| 130 | let f: EntryPoint = core::mem::transmute(ctx.image.entry); | ||
| 131 | f(ctx.exports, ctx.args.len(), ctx.args.as_ptr()) | ||
| 132 | }; | ||
| 133 | // Report completion to the spawner (the shell), then die. The spawner | ||
| 134 | // frees the image pages after reaping the message. | ||
| 135 | let _ = sched::send(sched::SHELL_TID, TaskMsg::User(code as u64)); | ||
| 136 | drop(ctx); | ||
| 137 | } | ||
diff --git a/rsnix/src/lib.rs b/rsnix/src/lib.rs index a422057..376d3b5 100644 --- a/rsnix/src/lib.rs +++ b/rsnix/src/lib.rs | |||
| @@ -12,6 +12,8 @@ pub mod gdt; | |||
| 12 | pub mod gop; | 12 | pub mod gop; |
| 13 | pub mod interrupts; | 13 | pub mod interrupts; |
| 14 | pub mod klog; | 14 | pub mod klog; |
| 15 | pub mod exec; | ||
| 16 | pub mod load; | ||
| 15 | pub mod memory; | 17 | pub mod memory; |
| 16 | pub mod paging; | 18 | pub mod paging; |
| 17 | pub mod pci; | 19 | pub mod pci; |
diff --git a/rsnix/src/load.rs b/rsnix/src/load.rs new file mode 100644 index 0000000..cebca89 --- /dev/null +++ b/rsnix/src/load.rs | |||
| @@ -0,0 +1,283 @@ | |||
| 1 | // --------------------------------------------------------------------------- | ||
| 2 | // load — PE32+ executable loader (ADR 0006 Phase 2). | ||
| 3 | // | ||
| 4 | // Parses a PE32+ image read through the VFS, allocates identity-mapped | ||
| 5 | // pages for it (the kernel runs identity-mapped, so "user" code lives at | ||
| 6 | // its link address), applies base relocations, and returns the entry | ||
| 7 | // point. The spawn path (exec.rs glue in sched) builds an Exports table, | ||
| 8 | // copies the argv block into the image's memory, and starts a task whose | ||
| 9 | // trampoline calls entry(&Exports, argc, argv). | ||
| 10 | // | ||
| 11 | // Scope: what rustc/lld emits for a no_std UEFI-ish binary — DOS header, | ||
| 12 | // COFF header, optional header PE32+, section table, .reloc base | ||
| 13 | // relocations. No imports, no TLS, no exceptions. | ||
| 14 | // --------------------------------------------------------------------------- | ||
| 15 | use crate::paging; | ||
| 16 | |||
| 17 | /// Where binaries are loaded: above the heap, inside conventional RAM. | ||
| 18 | /// The page allocator's span covers the conventional-memory range of the | ||
| 19 | /// boot memory map; LOAD_BASE must sit inside it (heap ends ~0xbb6c000 | ||
| 20 | /// with 256 MiB guests; conventional RAM typically ends near 0x0FF00000). | ||
| 21 | const LOAD_BASE: usize = 0x0C00_0000; // 192 MiB | ||
| 22 | const LOAD_LIMIT: usize = 0x0F00_0000; // stop before top-of-RAM | ||
| 23 | |||
| 24 | const PAGE_SIZE: usize = 4096; | ||
| 25 | |||
| 26 | /// A loaded image: pages to free on exit + the entry point. | ||
| 27 | pub struct Image { | ||
| 28 | /// First allocated page (free this..+page_count on exit). | ||
| 29 | pub base: usize, | ||
| 30 | pub page_count: usize, | ||
| 31 | pub entry: usize, | ||
| 32 | } | ||
| 33 | |||
| 34 | #[derive(Debug)] | ||
| 35 | pub enum LoadError { | ||
| 36 | ReadFailed, | ||
| 37 | NotPe, | ||
| 38 | NotPe32Plus, | ||
| 39 | BadSectionTable, | ||
| 40 | NoEntryPoint, | ||
| 41 | AllocFailed, | ||
| 42 | MapFailed, | ||
| 43 | BadReloc, | ||
| 44 | TooBig, | ||
| 45 | } | ||
| 46 | |||
| 47 | // --------------------------------------------------------------------------- | ||
| 48 | // PE file-format structures (packed reads via byte slices — no unaligned | ||
| 49 | // struct pointer derefs). | ||
| 50 | // --------------------------------------------------------------------------- | ||
| 51 | |||
| 52 | fn u16_at(b: &[u8], off: usize) -> Option<u16> { | ||
| 53 | b.get(off..off + 2).map(|s| u16::from_le_bytes([s[0], s[1]])) | ||
| 54 | } | ||
| 55 | fn u32_at(b: &[u8], off: usize) -> Option<u32> { | ||
| 56 | b.get(off..off + 4) | ||
| 57 | .map(|s| u32::from_le_bytes([s[0], s[1], s[2], s[3]])) | ||
| 58 | } | ||
| 59 | fn u64_at(b: &[u8], off: usize) -> Option<u64> { | ||
| 60 | b.get(off..off + 8).map(|s| { | ||
| 61 | let mut t = [0u8; 8]; | ||
| 62 | t.copy_from_slice(s); | ||
| 63 | u64::from_le_bytes(t) | ||
| 64 | }) | ||
| 65 | } | ||
| 66 | |||
| 67 | const DOS_MAGIC: u16 = 0x5A4D; // "MZ" | ||
| 68 | const PE_MAGIC: u32 = 0x0000_4550; // "PE\0\0" | ||
| 69 | const OPT_PE32PLUS: u16 = 0x20B; | ||
| 70 | const DIR_BASERELOC: usize = 5; | ||
| 71 | |||
| 72 | /// Map an RVA to its raw file offset via the section table. | ||
| 73 | fn rva_to_file(sections: &[Option<Section>], count: usize, rva: usize) -> Option<usize> { | ||
| 74 | for s in sections.iter().take(count).flatten() { | ||
| 75 | if rva >= s.virt_addr && rva < s.virt_addr + s.raw_size.max(s.virt_size) { | ||
| 76 | return Some(s.raw_ptr + (rva - s.virt_addr)); | ||
| 77 | } | ||
| 78 | } | ||
| 79 | None | ||
| 80 | } | ||
| 81 | |||
| 82 | #[derive(Clone, Copy)] | ||
| 83 | struct Section { | ||
| 84 | virt_addr: usize, | ||
| 85 | virt_size: usize, | ||
| 86 | raw_ptr: usize, | ||
| 87 | raw_size: usize, | ||
| 88 | } | ||
| 89 | |||
| 90 | /// Parse headers, apply relocations into `mem`, return the entry RVA. | ||
| 91 | fn parse_and_relocate(img: &[u8], mem: usize) -> Result<usize, LoadError> { | ||
| 92 | // ---- DOS header ---- | ||
| 93 | if u16_at(img, 0) != Some(DOS_MAGIC) { | ||
| 94 | return Err(LoadError::NotPe); | ||
| 95 | } | ||
| 96 | let e_lfanew = u32_at(img, 0x3C).ok_or(LoadError::NotPe)? as usize; | ||
| 97 | |||
| 98 | // ---- COFF header ---- | ||
| 99 | if u32_at(img, e_lfanew) != Some(PE_MAGIC) { | ||
| 100 | return Err(LoadError::NotPe); | ||
| 101 | } | ||
| 102 | let coff = e_lfanew + 4; | ||
| 103 | let num_sections = u16_at(img, coff + 2).ok_or(LoadError::NotPe)? as usize; | ||
| 104 | let opt_size = u16_at(img, coff + 16).ok_or(LoadError::NotPe)? as usize; | ||
| 105 | if opt_size == 0 { | ||
| 106 | return Err(LoadError::NotPe); | ||
| 107 | } | ||
| 108 | |||
| 109 | // ---- Optional header (PE32+) ---- | ||
| 110 | let opt = coff + 20; | ||
| 111 | if u16_at(img, opt) != Some(OPT_PE32PLUS) { | ||
| 112 | return Err(LoadError::NotPe32Plus); | ||
| 113 | } | ||
| 114 | let entry_rva = u32_at(img, opt + 16).ok_or(LoadError::NotPe32Plus)? as usize; | ||
| 115 | let image_base = u64_at(img, opt + 24).ok_or(LoadError::NotPe32Plus)? as usize; | ||
| 116 | |||
| 117 | let num_dirs = u32_at(img, opt + 108).ok_or(LoadError::NotPe32Plus)? as usize; | ||
| 118 | let dir_base = opt + 112; | ||
| 119 | let (reloc_rva, reloc_size) = if num_dirs > DIR_BASERELOC { | ||
| 120 | ( | ||
| 121 | u32_at(img, dir_base + DIR_BASERELOC * 8).ok_or(LoadError::BadReloc)? as usize, | ||
| 122 | u32_at(img, dir_base + DIR_BASERELOC * 8 + 4).ok_or(LoadError::BadReloc)? as usize, | ||
| 123 | ) | ||
| 124 | } else { | ||
| 125 | (0, 0) | ||
| 126 | }; | ||
| 127 | |||
| 128 | // ---- Section table ---- | ||
| 129 | let sect_tab = opt + opt_size; | ||
| 130 | let mut sections: [Option<Section>; 96] = [None; 96]; | ||
| 131 | if num_sections > sections.len() { | ||
| 132 | return Err(LoadError::BadSectionTable); | ||
| 133 | } | ||
| 134 | for i in 0..num_sections { | ||
| 135 | let s = sect_tab + i * 40; | ||
| 136 | let virt_size = u32_at(img, s + 8).ok_or(LoadError::BadSectionTable)? as usize; | ||
| 137 | let virt_addr = u32_at(img, s + 12).ok_or(LoadError::BadSectionTable)? as usize; | ||
| 138 | let raw_size = u32_at(img, s + 16).ok_or(LoadError::BadSectionTable)? as usize; | ||
| 139 | let raw_ptr = u32_at(img, s + 20).ok_or(LoadError::BadSectionTable)? as usize; | ||
| 140 | sections[i] = Some(Section { | ||
| 141 | virt_addr, | ||
| 142 | virt_size, | ||
| 143 | raw_ptr, | ||
| 144 | raw_size, | ||
| 145 | }); | ||
| 146 | } | ||
| 147 | |||
| 148 | // ---- Copy section contents ---- | ||
| 149 | for s in sections.iter_mut().take(num_sections).flatten() { | ||
| 150 | let n = s.raw_size.min(s.virt_size); | ||
| 151 | if n == 0 { | ||
| 152 | continue; | ||
| 153 | } | ||
| 154 | let src = img.get(s.raw_ptr..s.raw_ptr + n).ok_or(LoadError::BadSectionTable)?; | ||
| 155 | unsafe { | ||
| 156 | let dst = (mem + s.virt_addr) as *mut u8; | ||
| 157 | core::ptr::copy_nonoverlapping(src.as_ptr(), dst, n); | ||
| 158 | } | ||
| 159 | } | ||
| 160 | |||
| 161 | // ---- Apply base relocations ---- | ||
| 162 | if reloc_size > 0 && reloc_rva != 0 { | ||
| 163 | // The .reloc directory is an RVA; the section table maps it to a | ||
| 164 | // raw file offset. | ||
| 165 | let mut off = rva_to_file(§ions, num_sections, reloc_rva).ok_or(LoadError::BadReloc)?; | ||
| 166 | let end = off + reloc_size; | ||
| 167 | while off + 8 <= end { | ||
| 168 | let page_rva = u32_at(img, off).ok_or(LoadError::BadReloc)? as usize; | ||
| 169 | let block_size = u32_at(img, off + 4).ok_or(LoadError::BadReloc)? as usize; | ||
| 170 | if block_size < 8 || off + block_size > end { | ||
| 171 | break; | ||
| 172 | } | ||
| 173 | let entries = (block_size - 8) / 2; | ||
| 174 | for i in 0..entries { | ||
| 175 | let e = u16_at(img, off + 8 + i * 2).ok_or(LoadError::BadReloc)?; | ||
| 176 | let r#type = e >> 12; | ||
| 177 | let pos = e & 0xFFF; | ||
| 178 | if r#type == 0 { | ||
| 179 | continue; // IMAGE_REL_BASED_ABSOLUTE — padding | ||
| 180 | } | ||
| 181 | let target = mem + page_rva + pos as usize; | ||
| 182 | match r#type { | ||
| 183 | 10 => { | ||
| 184 | // DIR64: 8-bit image base delta | ||
| 185 | unsafe { | ||
| 186 | let p = target as *mut u64; | ||
| 187 | let v = p.read_unaligned(); | ||
| 188 | p.write_unaligned(v.wrapping_add((mem as u64).wrapping_sub(image_base as u64))); | ||
| 189 | } | ||
| 190 | } | ||
| 191 | _ => return Err(LoadError::BadReloc), | ||
| 192 | } | ||
| 193 | } | ||
| 194 | off += block_size; | ||
| 195 | } | ||
| 196 | } | ||
| 197 | |||
| 198 | Ok(entry_rva) | ||
| 199 | } | ||
| 200 | |||
| 201 | /// Load a PE32+ image from the given bytes. Returns the loaded image | ||
| 202 | /// (identity-mapped at LOAD_BASE+) with its entry point resolved. | ||
| 203 | pub fn load_image(bytes: &[u8]) -> Result<Image, LoadError> { | ||
| 204 | if bytes.len() > 8 * 1024 * 1024 { | ||
| 205 | return Err(LoadError::TooBig); | ||
| 206 | } | ||
| 207 | // Quick header sanity before allocating. | ||
| 208 | if u16_at(bytes, 0) != Some(DOS_MAGIC) { | ||
| 209 | return Err(LoadError::NotPe); | ||
| 210 | } | ||
| 211 | let e_lfanew = u32_at(bytes, 0x3C).unwrap_or(0) as usize; | ||
| 212 | if u32_at(bytes, e_lfanew) != Some(PE_MAGIC) { | ||
| 213 | return Err(LoadError::NotPe); | ||
| 214 | } | ||
| 215 | |||
| 216 | let page_count = bytes.len().div_ceil(PAGE_SIZE); | ||
| 217 | // Allocate physical pages at a fixed window (identity mapping makes | ||
| 218 | // the link address = runtime address after relocation). | ||
| 219 | let base = crate::memory::alloc_pages_above(page_count, LOAD_BASE).ok_or(LoadError::AllocFailed)?; | ||
| 220 | if base >= LOAD_LIMIT { | ||
| 221 | crate::memory::free_pages(base, page_count); | ||
| 222 | return Err(LoadError::TooBig); | ||
| 223 | } | ||
| 224 | // Identity-map them user+writable+nx-default (relocations need write; | ||
| 225 | // we do not enforce W^X here — single-privilege-level kernel). | ||
| 226 | if !paging::map_range(base, page_count, paging::PAGE_USER | paging::PAGE_WRITABLE | paging::PAGE_NX) { | ||
| 227 | crate::memory::free_pages(base, page_count); | ||
| 228 | return Err(LoadError::MapFailed); | ||
| 229 | } | ||
| 230 | |||
| 231 | let entry_rva = match parse_and_relocate(bytes, base) { | ||
| 232 | Ok(e) => e, | ||
| 233 | Err(e) => { | ||
| 234 | crate::memory::free_pages(base, page_count); | ||
| 235 | return Err(e); | ||
| 236 | } | ||
| 237 | }; | ||
| 238 | // Clear BSS tails: sections were copied raw-size bytes each; anything | ||
| 239 | // between raw_size and virt_size is zero per PE spec. parse_and_relocate | ||
| 240 | // only copied min(raw,virt); zero-fill the remainder now. | ||
| 241 | zero_bss_tails(bytes, base); | ||
| 242 | |||
| 243 | Ok(Image { | ||
| 244 | base, | ||
| 245 | page_count, | ||
| 246 | entry: base + entry_rva, | ||
| 247 | }) | ||
| 248 | } | ||
| 249 | |||
| 250 | /// Zero the part of each section between raw_size and virt_size (BSS). | ||
| 251 | fn zero_bss_tails(img: &[u8], mem: usize) { | ||
| 252 | // Re-walk the section table minimally (headers are small). | ||
| 253 | let Some(e_lfanew) = u32_at(img, 0x3C) else { return }; | ||
| 254 | let coff = e_lfanew as usize + 4; | ||
| 255 | let Some(num_sections) = u16_at(img, coff + 2) else { return }; | ||
| 256 | let Some(opt_size) = u16_at(img, coff + 16) else { return }; | ||
| 257 | let sect_tab = coff + 20 + opt_size as usize; | ||
| 258 | for i in 0..num_sections as usize { | ||
| 259 | let Some(virt_size) = u32_at(img, sect_tab + i * 40 + 8) else { return }; | ||
| 260 | let Some(raw_size) = u32_at(img, sect_tab + i * 40 + 16) else { return }; | ||
| 261 | let Some(virt_addr) = u32_at(img, sect_tab + i * 40 + 12) else { return }; | ||
| 262 | if virt_size > raw_size { | ||
| 263 | unsafe { | ||
| 264 | core::ptr::write_bytes( | ||
| 265 | (mem + virt_addr as usize + raw_size as usize) as *mut u8, | ||
| 266 | 0, | ||
| 267 | virt_size as usize - raw_size as usize, | ||
| 268 | ); | ||
| 269 | } | ||
| 270 | } | ||
| 271 | } | ||
| 272 | } | ||
| 273 | |||
| 274 | /// Free a previously loaded image. | ||
| 275 | /// | ||
| 276 | /// The task that ran the image must be dead — no code may still execute | ||
| 277 | /// from or reference these pages. | ||
| 278 | pub unsafe fn unload_image(img: &Image) { | ||
| 279 | for i in 0..img.page_count { | ||
| 280 | let _ = paging::unmap_page(img.base + i * PAGE_SIZE); | ||
| 281 | } | ||
| 282 | crate::memory::free_pages(img.base, img.page_count); | ||
| 283 | } | ||
diff --git a/rsnix/src/sched.rs b/rsnix/src/sched.rs index 0c4938e..107dba6 100644 --- a/rsnix/src/sched.rs +++ b/rsnix/src/sched.rs | |||
| @@ -396,6 +396,31 @@ pub fn spawn(entry: fn(), name: &str) -> Option<usize> { | |||
| 396 | if ok { Some(id) } else { None } | 396 | if ok { Some(id) } else { None } |
| 397 | } | 397 | } |
| 398 | 398 | ||
| 399 | /// Reserve an empty task slot without installing a frame. The slot is | ||
| 400 | /// marked Ready-with-no-frame is NOT valid to schedule — callers must | ||
| 401 | /// follow with `launch_at` before yielding (exec does both back-to-back | ||
| 402 | /// under the preemption lock window of the caller). | ||
| 403 | pub fn reserve_slot() -> Option<usize> { | ||
| 404 | lock(); | ||
| 405 | let id = unsafe { | ||
| 406 | let mut slot = None; | ||
| 407 | for i in 2..MAX_TASKS { | ||
| 408 | if TASKS[i].state == TaskState::Done && TASKS[i].stack != 0 { | ||
| 409 | reap(i); | ||
| 410 | slot = Some(i); | ||
| 411 | break; | ||
| 412 | } | ||
| 413 | if TASKS[i].state == TaskState::Done && TASKS[i].stack == 0 && TASKS[i].name_len == 0 { | ||
| 414 | slot = Some(i); | ||
| 415 | break; | ||
| 416 | } | ||
| 417 | } | ||
| 418 | slot | ||
| 419 | }; | ||
| 420 | unlock(); | ||
| 421 | id | ||
| 422 | } | ||
| 423 | |||
| 399 | unsafe fn reap(id: usize) { | 424 | unsafe fn reap(id: usize) { |
| 400 | unsafe { | 425 | unsafe { |
| 401 | let t = &mut TASKS[id]; | 426 | let t = &mut TASKS[id]; |
| @@ -408,6 +433,19 @@ unsafe fn reap(id: usize) { | |||
| 408 | } | 433 | } |
| 409 | } | 434 | } |
| 410 | 435 | ||
| 436 | /// Install the initial frame for a reserved slot and mark it Ready. | ||
| 437 | /// The slot must have come from `reserve_slot` and must not have been | ||
| 438 | /// scheduled in between (exec stores its context between the two calls). | ||
| 439 | pub fn launch_at(id: usize, entry: fn(), name: &str) -> bool { | ||
| 440 | if id >= MAX_TASKS { | ||
| 441 | return false; | ||
| 442 | } | ||
| 443 | lock(); | ||
| 444 | let ok = spawn_at(id, entry, name); | ||
| 445 | unlock(); | ||
| 446 | ok | ||
| 447 | } | ||
| 448 | |||
| 411 | fn spawn_at(id: usize, entry: fn(), name: &str) -> bool { | 449 | fn spawn_at(id: usize, entry: fn(), name: &str) -> bool { |
| 412 | let Some(base) = memory::alloc_pages(STACK_PAGES) else { | 450 | let Some(base) = memory::alloc_pages(STACK_PAGES) else { |
| 413 | return false; | 451 | return false; |
| @@ -483,7 +521,7 @@ unsafe extern "C" fn trampoline(entry: usize) -> ! { | |||
| 483 | task_exit() | 521 | task_exit() |
| 484 | } | 522 | } |
| 485 | 523 | ||
| 486 | fn task_exit() -> ! { | 524 | pub fn task_exit() -> ! { |
| 487 | lock(); | 525 | lock(); |
| 488 | unsafe { | 526 | unsafe { |
| 489 | TASKS[CURRENT].state = TaskState::Done; | 527 | TASKS[CURRENT].state = TaskState::Done; |
| @@ -648,6 +686,12 @@ pub fn abort_current(msg: &str) -> ! { | |||
| 648 | TASKS[id].stack, | 686 | TASKS[id].stack, |
| 649 | TASKS[id].guard | 687 | TASKS[id].guard |
| 650 | ); | 688 | ); |
| 689 | // An exec'd binary died: unblock the waiting spawner with a | ||
| 690 | // completion message (exit code -1 encoded as u64::MAX). The | ||
| 691 | // shell's queue is the only waiter; best-effort by design. | ||
| 692 | if id >= 2 { | ||
| 693 | let _ = push_msg(SHELL_TID, TaskMsg::User(u64::MAX)); | ||
| 694 | } | ||
| 651 | PREEMPT_DEPTH.store(0, Ordering::Relaxed); | 695 | PREEMPT_DEPTH.store(0, Ordering::Relaxed); |
| 652 | } | 696 | } |
| 653 | yield_int(); | 697 | yield_int(); |
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 | |||
| 8 | 8 | ||
| 9 | [dependencies] | 9 | [dependencies] |
| 10 | rsnix = { path = "../rsnix" } | 10 | rsnix = { path = "../rsnix" } |
| 11 | rsapi = { path = "../rsapi" } | ||
| 11 | rsdosedit = { path = "../rsdosedit" } | 12 | rsdosedit = { path = "../rsdosedit" } |
| 12 | rspaint = { path = "../rspaint" } | 13 | 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 `{} <path> TIL <marker>`", | |||
| 745 | } | 745 | } |
| 746 | } | 746 | } |
| 747 | 747 | ||
| 748 | // Resolution rule (ADR 0006): \bin\<kind>\<name> first, else the | ||
| 749 | // built-in. The frozen core (help/clear/echo/poweroff/exit/halt/ | ||
| 750 | // rescan) is exempt — it exists to fix the system and never migrates. | ||
| 751 | if !matches!(cmd, "help" | "clear" | "echo" | "halt" | "poweroff" | "exit" | "rescan") | ||
| 752 | && exec_external(&parsed) | ||
| 753 | { | ||
| 754 | return; | ||
| 755 | } | ||
| 756 | |||
| 748 | match cmd { | 757 | match cmd { |
| 749 | "help" => cmd_help(&parsed), | 758 | "help" => cmd_help(&parsed), |
| 750 | "echo" => cmd_echo(&parsed), | 759 | "echo" => cmd_echo(&parsed), |
| @@ -792,6 +801,108 @@ write `{} <path> TIL <marker>`", | |||
| 792 | } | 801 | } |
| 793 | 802 | ||
| 794 | // --------------------------------------------------------------------------- | 803 | // --------------------------------------------------------------------------- |
| 804 | // External binaries (ADR 0006 Phase 2) | ||
| 805 | // | ||
| 806 | // Resolution: \bin\cli\<name> on each mounted volume; else "unknown | ||
| 807 | // command". The Exports table is constructed per invocation: v1 wires | ||
| 808 | // put_line to the active terminal and get_line to EOF (stream injection | ||
| 809 | // through the filter pipeline lands in Phase 3). | ||
| 810 | // --------------------------------------------------------------------------- | ||
| 811 | |||
| 812 | static mut LAST_EXIT_CODE: i32 = 0; | ||
| 813 | |||
| 814 | /// Last exit code of an external command ($? equivalent for scripts). | ||
| 815 | pub fn last_exit_code() -> i32 { | ||
| 816 | unsafe { core::ptr::addr_of!(LAST_EXIT_CODE).read() } | ||
| 817 | } | ||
| 818 | |||
| 819 | /// ABI callback: print one line to the invoking terminal. | ||
| 820 | fn abi_put_line(line: rsapi::Line) -> bool { | ||
| 821 | term_println(line.as_str()); | ||
| 822 | true | ||
| 823 | } | ||
| 824 | |||
| 825 | /// ABI callback: read one input line — no stream source wired yet (Phase 3). | ||
| 826 | fn abi_get_line(_out: &mut rsapi::Line) -> rsapi::StreamResult { | ||
| 827 | rsapi::StreamResult::Eof | ||
| 828 | } | ||
| 829 | |||
| 830 | /// ABI callback: kernel log. | ||
| 831 | fn abi_log_line(line: rsapi::Line) { | ||
| 832 | rsnix::klog::append(line.as_str(), 0); | ||
| 833 | } | ||
| 834 | |||
| 835 | /// ABI callback: exit with a code. | ||
| 836 | fn abi_exit(code: i32) -> ! { | ||
| 837 | unsafe { | ||
| 838 | LAST_EXIT_CODE = code; | ||
| 839 | } | ||
| 840 | // Unwind the binary by longjmp-style task abort: the trampoline never | ||
| 841 | // returns here. We report the code via the static and kill the task. | ||
| 842 | rsnix::sched::abort_current("exit") | ||
| 843 | } | ||
| 844 | |||
| 845 | static EXPORTS_V1: rsapi::Exports = rsapi::Exports { | ||
| 846 | magic: rsapi::EXPORTS_MAGIC, | ||
| 847 | version: rsapi::ABI_VERSION, | ||
| 848 | put_line: abi_put_line, | ||
| 849 | get_line: abi_get_line, | ||
| 850 | log_line: abi_log_line, | ||
| 851 | exit: abi_exit, | ||
| 852 | }; | ||
| 853 | |||
| 854 | /// Run `cmd` as an external PE32+ binary from /bin/cli/. Returns false | ||
| 855 | /// when no external binary resolved (the caller falls back to built-ins). | ||
| 856 | fn exec_external(p: &Parsed) -> bool { | ||
| 857 | let Some(h) = rsnix::exec::resolve("cli", &p.cmd) else { | ||
| 858 | return false; | ||
| 859 | }; | ||
| 860 | let bytes = match rsnix::exec::read_file(h) { | ||
| 861 | Ok(b) => b, | ||
| 862 | Err(_) => { | ||
| 863 | term_err(&alloc::format!("{}: read failed", p.cmd)); | ||
| 864 | return true; | ||
| 865 | } | ||
| 866 | }; | ||
| 867 | let image = match rsnix::load::load_image(&bytes) { | ||
| 868 | Ok(img) => img, | ||
| 869 | Err(e) => { | ||
| 870 | term_err(&alloc::format!("{}: cannot load ({:?})", p.cmd, e)); | ||
| 871 | return true; | ||
| 872 | } | ||
| 873 | }; | ||
| 874 | let mut argv: Vec<String> = Vec::with_capacity(p.args.len() + 1); | ||
| 875 | argv.push(p.cmd.clone()); | ||
| 876 | argv.extend(p.args.iter().cloned()); | ||
| 877 | let name = alloc::format!("{}", p.cmd); | ||
| 878 | match rsnix::exec::spawn(image, &EXPORTS_V1, &argv, &name) { | ||
| 879 | Ok(id) => { | ||
| 880 | // Wait for the completion message (TaskMsg::User(exit_code)). | ||
| 881 | loop { | ||
| 882 | match rsnix::sched::recv_blocking() { | ||
| 883 | rsnix::sched::TaskMsg::User(code) => { | ||
| 884 | unsafe { | ||
| 885 | LAST_EXIT_CODE = code as i32; | ||
| 886 | } | ||
| 887 | break; | ||
| 888 | } | ||
| 889 | rsnix::sched::TaskMsg::Key(rsnix::ps2::KeyEvent::Ctrl('c')) => { | ||
| 890 | // ^C while waiting: kill the command task. | ||
| 891 | let _ = rsnix::sched::kill(id); | ||
| 892 | term_err("^C"); | ||
| 893 | break; | ||
| 894 | } | ||
| 895 | _ => {} | ||
| 896 | } | ||
| 897 | } | ||
| 898 | } | ||
| 899 | Err(rsnix::exec::ExecError::Busy) => term_err(&alloc::format!("{}: task table full", p.cmd)), | ||
| 900 | Err(_) => term_err(&alloc::format!("{}: cannot start", p.cmd)), | ||
| 901 | } | ||
| 902 | true | ||
| 903 | } | ||
| 904 | |||
| 905 | // --------------------------------------------------------------------------- | ||
| 795 | // Built-in commands | 906 | // Built-in commands |
| 796 | // --------------------------------------------------------------------------- | 907 | // --------------------------------------------------------------------------- |
| 797 | 908 | ||
