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/sched.rs | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) (limited to 'rsnix/src/sched.rs') 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 { if ok { Some(id) } else { None } } +/// Reserve an empty task slot without installing a frame. The slot is +/// marked Ready-with-no-frame is NOT valid to schedule — callers must +/// follow with `launch_at` before yielding (exec does both back-to-back +/// under the preemption lock window of the caller). +pub fn reserve_slot() -> Option { + lock(); + let id = unsafe { + let mut slot = None; + for i in 2..MAX_TASKS { + if TASKS[i].state == TaskState::Done && TASKS[i].stack != 0 { + reap(i); + slot = Some(i); + break; + } + if TASKS[i].state == TaskState::Done && TASKS[i].stack == 0 && TASKS[i].name_len == 0 { + slot = Some(i); + break; + } + } + slot + }; + unlock(); + id +} + unsafe fn reap(id: usize) { unsafe { let t = &mut TASKS[id]; @@ -408,6 +433,19 @@ unsafe fn reap(id: usize) { } } +/// Install the initial frame for a reserved slot and mark it Ready. +/// The slot must have come from `reserve_slot` and must not have been +/// scheduled in between (exec stores its context between the two calls). +pub fn launch_at(id: usize, entry: fn(), name: &str) -> bool { + if id >= MAX_TASKS { + return false; + } + lock(); + let ok = spawn_at(id, entry, name); + unlock(); + ok +} + fn spawn_at(id: usize, entry: fn(), name: &str) -> bool { let Some(base) = memory::alloc_pages(STACK_PAGES) else { return false; @@ -483,7 +521,7 @@ unsafe extern "C" fn trampoline(entry: usize) -> ! { task_exit() } -fn task_exit() -> ! { +pub fn task_exit() -> ! { lock(); unsafe { TASKS[CURRENT].state = TaskState::Done; @@ -648,6 +686,12 @@ pub fn abort_current(msg: &str) -> ! { TASKS[id].stack, TASKS[id].guard ); + // An exec'd binary died: unblock the waiting spawner with a + // completion message (exit code -1 encoded as u64::MAX). The + // shell's queue is the only waiter; best-effort by design. + if id >= 2 { + let _ = push_msg(SHELL_TID, TaskMsg::User(u64::MAX)); + } PREEMPT_DEPTH.store(0, Ordering::Relaxed); } yield_int(); -- cgit v1.2.3