summaryrefslogtreecommitdiffstats
path: root/rsnix/src/exec.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-21 21:17:44 +0200
committersalmonik <salmonik@tuta.io>2026-08-21 21:17:44 +0200
commit71b6445b048456bf198d8b622570db95f4665cb9 (patch)
tree808e73fa95709ed9c2d409aecaf275fd70365293 /rsnix/src/exec.rs
parentinput: total keyboard routing + per-terminal discipline (ADR 0006 ph1) (diff)
downloadrs-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--rsnix/src/exec.rs137
1 files changed, 137 insertions, 0 deletions
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// ---------------------------------------------------------------------------
13use crate::load;
14use crate::sched::{self, TaskMsg};
15use alloc::string::String;
16use alloc::vec::Vec;
17use rsapi::{Arg, EntryPoint, Exports};
18
19/// Per-task invocation context adopted by the trampoline.
20struct 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
31static mut CTX: [Option<ExecCtx>; sched::MAX_TASKS] = [const { None }; sched::MAX_TASKS];
32
33/// Error of exec::resolve / exec::spawn.
34pub 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.
42pub 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)]
59pub 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).
80pub 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.
119fn 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}