summaryrefslogtreecommitdiffstats
path: root/rsnix/src/load.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/load.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/load.rs283
1 files changed, 283 insertions, 0 deletions
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}