summaryrefslogtreecommitdiffstats
path: root/rsnix/src
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
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 'rsnix/src')
-rw-r--r--rsnix/src/exec.rs137
-rw-r--r--rsnix/src/lib.rs2
-rw-r--r--rsnix/src/load.rs283
-rw-r--r--rsnix/src/sched.rs46
4 files changed, 467 insertions, 1 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}
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;
12pub mod gop; 12pub mod gop;
13pub mod interrupts; 13pub mod interrupts;
14pub mod klog; 14pub mod klog;
15pub mod exec;
16pub mod load;
15pub mod memory; 17pub mod memory;
16pub mod paging; 18pub mod paging;
17pub mod pci; 19pub 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// ---------------------------------------------------------------------------
15use 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).
21const LOAD_BASE: usize = 0x0C00_0000; // 192 MiB
22const LOAD_LIMIT: usize = 0x0F00_0000; // stop before top-of-RAM
23
24const PAGE_SIZE: usize = 4096;
25
26/// A loaded image: pages to free on exit + the entry point.
27pub 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)]
35pub 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
52fn 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}
55fn 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}
59fn 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
67const DOS_MAGIC: u16 = 0x5A4D; // "MZ"
68const PE_MAGIC: u32 = 0x0000_4550; // "PE\0\0"
69const OPT_PE32PLUS: u16 = 0x20B;
70const DIR_BASERELOC: usize = 5;
71
72/// Map an RVA to its raw file offset via the section table.
73fn 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)]
83struct 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.
91fn 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(&sections, 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.
203pub 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).
251fn 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.
278pub 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).
403pub 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
399unsafe fn reap(id: usize) { 424unsafe 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).
439pub 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
411fn spawn_at(id: usize, entry: fn(), name: &str) -> bool { 449fn 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
486fn task_exit() -> ! { 524pub 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();