mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
Native wasm32 programs on the AArch64 build too
A gexcc/wcc program is an i386 container around a wasm module; it ran only on the x86-64 build, where i386 (compat) processes exist. The AArch64 build now runs them as well, beside translated arm64 programs, so one system can hold Debian's arm64 packages and native ones (docs/gexcc.md §6.1a): - gex-ld accepts an ELF32 i386 image on the AArch64 guest when it is a wasm32 container (Image::is_wasm32: the GEXWASM header below the entry). - gex-abi: ctx::sp_slot(compat) (esp, x(4), for an i386-ABI process in every build; the AArch64 SP slot otherwise) and ctx::COMPAT_SCRATCH (the i386 rewrites' scratch: the x86 scratch area, or the vector registers of an AArch64 context, which wasm32 programs never use). - gex-kernel keys the i386 ABI on compat rather than on the build: the number in eax, i386::translate, its x86-only canonical numbers (dup2, time, alarm, the no-ops), packed epoll_event, i386 signal frames (no x87 area outside x86), clone's child stack. - shims: the initial and exec'd stack pointer, a forked child's stack, and (browser) an adopted child's stack once it is known to be compat. Checked on an AArch64-only gexc: tests/wasm32 5/5, tests/gexcc 15/15, a wasm32 bash running Debian's arm64 xz/python3.13/sqlite3 and python's os.system running the wasm32 dash; browser runtime (aarch64 shim): tests/wasm32 5/5, tests/gexcc as on the x86 shim. Workspace tests a64 469, x86 458. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_1896c5b302a54696aaa92c5f4047f20b
This commit is contained in:
1 parent
ad5f7e7fe3
commit
0366efa607
6 files changed
+95
-28
No files matched your search
@@ -310,6 +310,16 @@ pub mod ctx {
|
||||
/// Program counter. Only meaningful at block boundaries and safepoints.
|
||||
pub const PC: usize = 32 * 8;
|
||||
|
||||
/// The stack pointer of a process on the i386 ABI (`compat`): `esp`,
|
||||
/// `x(4)`, in every build. On x86-64 that is `SP` itself; an AArch64
|
||||
/// build has i386-ABI processes too -- native wasm32 programs, whose
|
||||
/// musl port reads and writes `x(4)` -- and their stack pointer is not
|
||||
/// its `SP`. Everything else on the i386 ABI (the number in `x(0)`, the
|
||||
/// arguments, `PC`) is at the same offsets in both layouts.
|
||||
pub const fn sp_slot(compat: bool) -> usize {
|
||||
if compat && !super::PPC { x(4) } else { SP }
|
||||
}
|
||||
|
||||
/// Condition flags: one **byte** each, inside a single 32-bit slot. One
|
||||
/// flag stores as one `i32.store8`, all four as one `i32.store`; a byte
|
||||
/// (not a bit) each keeps the single-flag store off a read-modify-write.
|
||||
@@ -435,6 +445,12 @@ pub mod ctx {
|
||||
/// `poll` becomes `ppoll` with a timespec that has to live somewhere).
|
||||
pub const SCRATCH: usize = X87_TMP + 16;
|
||||
|
||||
/// Scratch for the i386 ABI's rewritten calls (`gex-kernel`'s `i386`):
|
||||
/// `SCRATCH` where the context has one; an AArch64 context ends at
|
||||
/// `v31`, and its only i386-ABI processes are native wasm32 programs,
|
||||
/// which never use the vector registers, so their first 64 bytes.
|
||||
pub const COMPAT_SCRATCH: usize = if super::X86 || super::PPC { SCRATCH } else { V };
|
||||
|
||||
/// PowerPC only: the area after `v31` (where x86-64 keeps its x87
|
||||
/// stack). The 32 condition-register bits, one byte each: bit `4f + k` is
|
||||
/// field `f`'s LT, GT, EQ, SO for `k` = 0..3.
|
||||
|
||||
@@ -554,7 +554,7 @@ pub fn setup_frame(
|
||||
if gex_abi::PPC {
|
||||
return setup_frame_ppc(signals, m, ctx_addr, signal, fault_address);
|
||||
}
|
||||
if gex_abi::X86 && compat {
|
||||
if compat {
|
||||
return setup_frame_i386(signals, m, ctx_addr, signal, fault_address);
|
||||
}
|
||||
if gex_abi::X86 {
|
||||
@@ -840,7 +840,7 @@ fn setup_frame_i386(
|
||||
fault_address: u64,
|
||||
) -> Result<u64, Errno> {
|
||||
let action = signals.action(signal);
|
||||
let sp = m.read_u64(ctx_addr + ctx::SP as u64)? & 0xffff_ffff;
|
||||
let sp = m.read_u64(ctx_addr + ctx::sp_slot(true) as u64)? & 0xffff_ffff;
|
||||
let rt = action.flags & sa::SIGINFO != 0;
|
||||
signals.legacy_frame = !rt;
|
||||
|
||||
@@ -917,7 +917,10 @@ fn setup_frame_i386(
|
||||
m.write_u32(sc + iframe::SC_CR2, fault_address as u32)?;
|
||||
|
||||
// The FP image: x87 control word, MXCSR and the eight xmm registers.
|
||||
m.write_u32(fpstate, m.read_u32(ctx_addr + ctx::X87_CW as u64)? & 0xffff)?;
|
||||
// (an AArch64 build's i386-ABI processes are wasm32 programs: no x87,
|
||||
// and the context has no x87 area)
|
||||
let cw = if gex_abi::X86 { m.read_u32(ctx_addr + ctx::X87_CW as u64)? & 0xffff } else { 0x37f };
|
||||
m.write_u32(fpstate, cw)?;
|
||||
m.write_u32(fpstate + iframe::FP_MXCSR, m.read_u32(ctx_addr + ctx::MXCSR as u64)?)?;
|
||||
for n in 0..8u8 {
|
||||
for half in 0..2u64 {
|
||||
@@ -944,7 +947,7 @@ fn setup_frame_i386(
|
||||
);
|
||||
}
|
||||
// esp = frame; eax = signo; for an rt frame edx = &info and ecx = &uc.
|
||||
m.write_u64(ctx_addr + ctx::SP as u64, frame_addr)?;
|
||||
m.write_u64(ctx_addr + ctx::sp_slot(true) as u64, frame_addr)?;
|
||||
m.write_u64(ctx_addr + ctx::x(0) as u64, signal as u64)?;
|
||||
if rt {
|
||||
m.write_u64(ctx_addr + ctx::x(2) as u64, frame_addr + iframe::RT_INFO)?;
|
||||
@@ -966,7 +969,7 @@ fn sigreturn_i386(
|
||||
ctx_addr: u64,
|
||||
legacy: bool,
|
||||
) -> Result<u64, Errno> {
|
||||
let esp = m.read_u64(ctx_addr + ctx::SP as u64)? & 0xffff_ffff;
|
||||
let esp = m.read_u64(ctx_addr + ctx::sp_slot(true) as u64)? & 0xffff_ffff;
|
||||
let (sc, mask) = if legacy {
|
||||
let frame_addr = esp - 8;
|
||||
let sc = frame_addr + iframe::SF_SC;
|
||||
@@ -1064,7 +1067,7 @@ pub fn sigreturn(
|
||||
if gex_abi::PPC {
|
||||
return sigreturn_ppc(signals, m, ctx_addr);
|
||||
}
|
||||
if gex_abi::X86 && compat {
|
||||
if compat {
|
||||
// The frame's own kind decides; the entry number is a cross-check.
|
||||
let _ = legacy;
|
||||
let kind = signals.legacy_frame;
|
||||
|
||||
@@ -881,6 +881,9 @@ pub enum PollReport {
|
||||
max: u32,
|
||||
/// The opaque word to hand back for each descriptor.
|
||||
data: Vec<(i32, u64)>,
|
||||
/// `struct epoll_event` packed in 12 bytes (x86-64's, and the i386
|
||||
/// ABI's in any build), rather than 16.
|
||||
packed: bool,
|
||||
},
|
||||
/// `select`'s three `fd_set`s. A zero address means the caller did not
|
||||
/// pass that set.
|
||||
@@ -944,7 +947,7 @@ pub fn poll_finish(fds: &[PollFd], report: &PollReport, m: &mut GuestMem<'_>) ->
|
||||
}
|
||||
Ok(ready)
|
||||
}
|
||||
PollReport::Epoll { out, max, data } => {
|
||||
PollReport::Epoll { out, max, data, packed } => {
|
||||
// Only the ready ones, each with the caller's own token, which is
|
||||
// the whole difference between this and `poll`.
|
||||
let mut n = 0u64;
|
||||
@@ -965,7 +968,7 @@ pub fn poll_finish(fds: &[PollFd], report: &PollReport, m: &mut GuestMem<'_>) ->
|
||||
// on x86-64 (the token at offset four), 16 on aarch64. Xvfb
|
||||
// read its `data.ptr` from the wrong offset after every wait
|
||||
// that had to block, and dereferenced the mask.
|
||||
if gex_abi::X86 {
|
||||
if *packed {
|
||||
let at = out + n * 12;
|
||||
m.write_u32(at, r as u32)?;
|
||||
m.write_u64(at + 4, token)?;
|
||||
@@ -1606,7 +1609,7 @@ impl Kernel {
|
||||
m.write_u64(child + off as u64, v)?;
|
||||
}
|
||||
set_result(m, child, 0)?;
|
||||
m.write_u64(child + ctx::SP as u64, child_stack)?;
|
||||
m.write_u64(child + ctx::sp_slot(self.compat) as u64, child_stack)?;
|
||||
m.write_u64(child + ctx::MON_SIZE as u64, 0)?;
|
||||
m.write_u64(child + ctx::TID as u64, tid as u64)?;
|
||||
if flags & clone_flags::SETTLS != 0 {
|
||||
@@ -1678,8 +1681,11 @@ impl Kernel {
|
||||
|
||||
fn syscall_inner(&mut self, m: &mut GuestMem<'_>, pc: u64) -> SyscallOutcome {
|
||||
self.syscall_pc = pc;
|
||||
// An i386-ABI process has its number in eax (`x(0)`) in every build:
|
||||
// on an AArch64 build that is a native wasm32 program.
|
||||
let nr_slot = if self.compat && !gex_abi::PPC { ctx::x(0) } else { ctx::SYSCALL_NR };
|
||||
let raw = m
|
||||
.read_u64(self.ctx_addr + ctx::SYSCALL_NR as u64)
|
||||
.read_u64(self.ctx_addr + nr_slot as u64)
|
||||
.unwrap_or(!0);
|
||||
let mut a: [u64; 6] = if self.compat && !gex_abi::PPC {
|
||||
let mut a = [0u64; 6];
|
||||
@@ -1717,12 +1723,12 @@ impl Kernel {
|
||||
post = p;
|
||||
ppc_post = pp;
|
||||
n
|
||||
} else if gex_abi::X86 && self.compat {
|
||||
} else if self.compat {
|
||||
let (n, p) = crate::i386::translate(
|
||||
raw & 0xffff_ffff,
|
||||
&mut a,
|
||||
m,
|
||||
self.ctx_addr + ctx::SCRATCH as u64,
|
||||
self.ctx_addr + ctx::COMPAT_SCRATCH as u64,
|
||||
self.ctx_addr,
|
||||
);
|
||||
post = p;
|
||||
@@ -1967,7 +1973,7 @@ impl Kernel {
|
||||
}
|
||||
// An x86-64 number with no mapping reaches `dispatch` as `UNKNOWN`;
|
||||
// record the guest's own number, which is the one worth looking up.
|
||||
let outcome = if (gex_abi::X86 || gex_abi::PPC) && n == crate::x86::UNKNOWN {
|
||||
let outcome = if (gex_abi::X86 || gex_abi::PPC || self.compat) && n == crate::x86::UNKNOWN {
|
||||
let before = self.unimplemented.len();
|
||||
let r = self.dispatch(m, n, a);
|
||||
if self.unimplemented.len() > before {
|
||||
@@ -2130,6 +2136,7 @@ impl Kernel {
|
||||
out: a[1],
|
||||
max: a[2] as u32,
|
||||
data: set.watches().iter().map(|w| (w.fd, w.data)).collect(),
|
||||
packed: gex_abi::X86 || self.compat,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -2480,11 +2487,11 @@ impl Kernel {
|
||||
// -- x86-64 legacy forms (see `crate::x86`) ----------------------------
|
||||
// Only `x86::translate` produces these numbers; an AArch64 guest
|
||||
// issuing one raw falls through to ENOSYS below.
|
||||
nr::DUP2 if gex_abi::X86 || gex_abi::PPC => self.sys_dup2(a[0] as i32, a[1] as i32),
|
||||
nr::TIME if gex_abi::X86 || gex_abi::PPC => self.sys_time(m, a[0]),
|
||||
nr::ALARM if gex_abi::X86 || gex_abi::PPC => self.sys_alarm(m, a[0]),
|
||||
nr::DUP2 if gex_abi::X86 || gex_abi::PPC || self.compat => self.sys_dup2(a[0] as i32, a[1] as i32),
|
||||
nr::TIME if gex_abi::X86 || gex_abi::PPC || self.compat => self.sys_time(m, a[0]),
|
||||
nr::ALARM if gex_abi::X86 || gex_abi::PPC || self.compat => self.sys_alarm(m, a[0]),
|
||||
nr::ARCH_PRCTL if gex_abi::X86 => self.sys_arch_prctl(m, a[0], a[1]),
|
||||
nr::NOOP_OK if gex_abi::X86 || gex_abi::PPC => Ok(0),
|
||||
nr::NOOP_OK if gex_abi::X86 || gex_abi::PPC || self.compat => Ok(0),
|
||||
|
||||
// -- I/O ---------------------------------------------------------
|
||||
nr::WRITE => self.sys_write(m, a[0] as i32, a[1], a[2]),
|
||||
@@ -5814,7 +5821,7 @@ impl Kernel {
|
||||
// `EPOLL_CTL_DEL` is allowed a null event pointer.
|
||||
let (events, data) = if event == 0 {
|
||||
(0, 0)
|
||||
} else if gex_abi::X86 {
|
||||
} else if gex_abi::X86 || self.compat {
|
||||
(m.read_u32(event)?, m.read_u64(event + 4)?)
|
||||
} else {
|
||||
(m.read_u32(event)?, m.read_u64(event + 8)?)
|
||||
@@ -5915,7 +5922,7 @@ impl Kernel {
|
||||
) -> SysResult {
|
||||
let n = ready.len().min(max as usize);
|
||||
for (i, (events, data)) in ready.iter().take(n).enumerate() {
|
||||
if gex_abi::X86 {
|
||||
if gex_abi::X86 || self.compat {
|
||||
// Packed: 12 bytes, the token at offset four.
|
||||
let at = out + i as u64 * 12;
|
||||
m.write_u32(at, *events)?;
|
||||
@@ -6547,7 +6554,7 @@ impl Kernel {
|
||||
/// previous timer had left. Both itimervals live in the thread's scratch
|
||||
/// area, since the call has none of its own.
|
||||
fn sys_alarm(&mut self, m: &mut GuestMem<'_>, secs: u64) -> SysResult {
|
||||
let scratch = self.ctx_addr + ctx::SCRATCH as u64;
|
||||
let scratch = self.ctx_addr + ctx::COMPAT_SCRATCH as u64;
|
||||
for off in 0..4 {
|
||||
m.write_u64(scratch + off * 8, 0)?;
|
||||
}
|
||||
|
||||
@@ -236,7 +236,24 @@ impl DynSym {
|
||||
}
|
||||
}
|
||||
|
||||
/// The header a native wasm32 program's container carries 0x40 bytes below
|
||||
/// its entry (`tools/wine-wasm/wasm-elf.py`; shims' `wasmprog`).
|
||||
pub const WASM32_MAGIC: &[u8; 8] = b"GEXWASM\0";
|
||||
|
||||
impl Image {
|
||||
/// Whether this is a native wasm32 program's container: an i386 image
|
||||
/// with [`WASM32_MAGIC`] 0x40 bytes below the entry.
|
||||
pub fn is_wasm32(&self) -> bool {
|
||||
let at = self.entry.wrapping_sub(0x40);
|
||||
self.class32
|
||||
&& self.segments.iter().any(|s| {
|
||||
at >= s.vaddr && at + 8 <= s.vaddr + s.bytes.len() as u64 && {
|
||||
let o = (at - s.vaddr) as usize;
|
||||
&s.bytes[o..o + 8] == WASM32_MAGIC
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// The defined, non-TLS dynamic symbols in `[lo, hi)` (unbiased) by every
|
||||
/// name a binding may use: `name@@VER`, `name@VER`, and the bare `name`
|
||||
/// for the default version (or an unversioned object), each with its
|
||||
@@ -437,12 +454,14 @@ pub fn load(data: &[u8], pie_base: u64) -> Result<Image, LoadError> {
|
||||
if r.u8(5)? != order {
|
||||
return Err(LoadError::NotElf64Le);
|
||||
}
|
||||
// A 32-bit i386 image is accepted on the x86-64 guest only: it runs in
|
||||
// the same process model as everything else, in compatibility mode. The
|
||||
// PowerPC guest is 32-bit only.
|
||||
// A 32-bit i386 image runs in the same process model as everything
|
||||
// else, on the i386 ABI: on the x86-64 guest any i386 program, in
|
||||
// compatibility mode; on the AArch64 guest only a native wasm32 program
|
||||
// (an i386 container around a wasm module, checked below), which has no
|
||||
// i386 code to translate. The PowerPC guest is 32-bit only.
|
||||
let class32 = match r.u8(4)? {
|
||||
ELFCLASS64 if !gex_abi::PPC => false,
|
||||
ELFCLASS32 if gex_abi::X86 || gex_abi::PPC => true,
|
||||
ELFCLASS32 => true,
|
||||
_ => return Err(LoadError::NotElf64Le),
|
||||
};
|
||||
let e_type = r.u16(16)?;
|
||||
@@ -609,6 +628,10 @@ pub fn load(data: &[u8], pie_base: u64) -> Result<Image, LoadError> {
|
||||
runpath: Vec::new(),
|
||||
};
|
||||
|
||||
if class32 && !gex_abi::X86 && !gex_abi::PPC && !image.is_wasm32() {
|
||||
return Err(LoadError::NotAarch64);
|
||||
}
|
||||
|
||||
if let (Some((dyn_vaddr, dyn_size)), false) = (dynamic, class32) {
|
||||
apply_relative_relocations(&mut image, dyn_vaddr, dyn_size);
|
||||
image.named_relocs = read_named_relocs(&image, dyn_vaddr, dyn_size);
|
||||
|
||||
@@ -3912,7 +3912,8 @@ pub extern "C" fn gex_load(ptr: u32, len: u32, argv: u32, argv_len: u32) -> u64
|
||||
for i in 0..31u8 {
|
||||
write_u64(data, c + ctx::x(i), 0);
|
||||
}
|
||||
write_u64(data, c + ctx::SP, layout.sp);
|
||||
// (an i386-ABI process's is `esp` in every build: `ctx::sp_slot`)
|
||||
write_u64(data, c + ctx::sp_slot(program.compat()), layout.sp);
|
||||
write_u64(data, c + ctx::PC, program.entry());
|
||||
data[c + ctx::SAFEPOINT_COUNTER..c + ctx::SAFEPOINT_COUNTER + 4]
|
||||
.copy_from_slice(&1024i32.to_le_bytes());
|
||||
@@ -7989,7 +7990,8 @@ fn do_exec(h: &mut Host, path: &str, argv: &[String], envp: &[String]) -> u64 {
|
||||
// `sp` is the stack just built, not the recorded one: the two are the
|
||||
// same address by construction -- see `BLOB_RESERVE` -- and the argv on
|
||||
// it is this process's.
|
||||
write_u64(data, c + ctx::SP, layout.sp);
|
||||
let compat = program.as_ref().is_some_and(|p| p.compat());
|
||||
write_u64(data, c + ctx::sp_slot(compat), layout.sp);
|
||||
let entry = match (&program, &restored) {
|
||||
(Some(p), _) => p.entry(),
|
||||
(None, Some(l)) => l.entry,
|
||||
@@ -10070,6 +10072,21 @@ pub extern "C" fn gex_compat() -> i32 {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_set_compat(on: i32) {
|
||||
with_host(|h| h.kernel.compat = on != 0);
|
||||
// An adopted child's stack (`gex_adopt_child`, before this) went to
|
||||
// `ctx::SP`; an i386-ABI process's is `esp` (`ctx::sp_slot`), which on
|
||||
// an AArch64 build is another slot (its i386-ABI processes are wasm32
|
||||
// programs: posix_spawn's child starts on that stack).
|
||||
if on != 0 && !gex_abi::X86 && !gex_abi::PPC {
|
||||
with_host(|h| {
|
||||
let mut gm = GuestMem::new(guest_bytes());
|
||||
let c = h.kernel.ctx_addr;
|
||||
let sp = gm.read_u64(c + ctx::SP as u64).unwrap_or(0);
|
||||
if sp != 0 {
|
||||
let _ = gm.write_u64(c + ctx::sp_slot(true) as u64, sp);
|
||||
let _ = gm.write_u64(c + ctx::SP as u64, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
gex_xlate::set_compat(on != 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -1934,7 +1934,7 @@ fn fork_process(store: &impl wasmtime::AsContext<Data = HostState>, req: ForkReq
|
||||
let mut gm = GuestMem::new(dst);
|
||||
let _ = gex_kernel::syscalls::set_result(&mut gm, ctx_addr, 0);
|
||||
if req.child_stack != 0 {
|
||||
let _ = gm.write_u64(ctx_addr + ctx::SP as u64, req.child_stack);
|
||||
let _ = gm.write_u64(ctx_addr + ctx::sp_slot(child_kernel.compat) as u64, req.child_stack);
|
||||
}
|
||||
if let Some(tls) = req.tls {
|
||||
// PowerPC's thread pointer is r2.
|
||||
@@ -2978,7 +2978,8 @@ fn place_image(
|
||||
for i in 0..31u8 {
|
||||
write_u64(data, ctx_addr + ctx::x(i), 0);
|
||||
}
|
||||
write_u64(data, ctx_addr + ctx::SP, layout.sp);
|
||||
// (an i386-ABI process's is `esp` in every build: `ctx::sp_slot`)
|
||||
write_u64(data, ctx_addr + ctx::sp_slot(program.compat()), layout.sp);
|
||||
write_u64(data, ctx_addr + ctx::PC, image.entry);
|
||||
data[ctx_addr + ctx::SAFEPOINT_COUNTER..ctx_addr + ctx::SAFEPOINT_COUNTER + 4]
|
||||
.copy_from_slice(&SAFEPOINT_INTERVAL.to_le_bytes());
|
||||
|
||||
Reference in new issue
Block a user