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:
velzieandClaude Opus 5.5 committed 2026-10-08 18:39:27 -04:00
1 parent ad5f7e7fe3
commit 0366efa607
6 files changed
+95 -28

No files matched your search

+16
View File
@@ -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.
+9 -6
View File
@@ -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;
+21 -14
View File
@@ -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)?;
}
+27 -4
View File
@@ -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);
+19 -2
View File
@@ -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);
}
+3 -2
View File
@@ -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());