fix(verify): fix non-amd64 JIT trampolines and validate under qemu
This commit is contained in:
+23
-16
@@ -11,10 +11,13 @@ Unreleased changes on the `development` branch.
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **Multi-architecture debugger.** `gasm debug` works on arm64, riscv64
|
- **Multi-architecture debugger.** `gasm debug` carries
|
||||||
and loong64 in addition to amd64. Each architecture has its own ptrace
|
per-architecture ptrace register access, disassemblers
|
||||||
register access, disassembler (`golang.org/x/arch`), register display,
|
(`golang.org/x/arch`), register display, FP register views and
|
||||||
and stop-info handler. The REPL is fully arch-neutral.
|
stop-info handlers for arm64, riscv64 and loong64, and the REPL is
|
||||||
|
arch-neutral. Sessions are runtime-validated on amd64; the other
|
||||||
|
hosts execute through the now-working JIT trampolines, but their
|
||||||
|
ptrace loops have not seen hardware yet.
|
||||||
- **Headless debugging.** `gasm debug --script` runs REPL commands from a
|
- **Headless debugging.** `gasm debug --script` runs REPL commands from a
|
||||||
file (or stdin) and exits; `--timeout` kills the debuggee when a run
|
file (or stdin) and exits; `--timeout` kills the debuggee when a run
|
||||||
hangs, with the watchdog armed before the ptrace attach. `--cover` runs
|
hangs, with the watchdog armed before the ptrace attach. `--cover` runs
|
||||||
@@ -43,18 +46,22 @@ Unreleased changes on the `development` branch.
|
|||||||
architectures via hand-written assembly trampolines
|
architectures via hand-written assembly trampolines
|
||||||
(`trampoline_{arm64,riscv64,loong64}.s`) that save the Go stack, switch
|
(`trampoline_{arm64,riscv64,loong64}.s`) that save the Go stack, switch
|
||||||
to a prepared stack, and branch to the JIT function.
|
to a prepared stack, and branch to the JIT function.
|
||||||
- **ABI checks architecture port (partial).** The ABI-checking
|
- **ABI checks on all architectures.** `gasm verify -abi` and the ABI
|
||||||
machinery is architecture-neutral (`ABIReport` with `FPClobbered`,
|
half of `-fuzz` now work on arm64, riscv64 and loong64 via
|
||||||
`GClobbered`, `RedZoneHit`; renamed from the amd64-only field names)
|
per-architecture checked trampolines: sentinels planted in the
|
||||||
and per-architecture checked trampolines exist for arm64, riscv64 and
|
registers the Go ABI fixes across calls (amd64 `BP`/`R14`, arm64
|
||||||
loong64 alongside amd64, restoring the frame pointer and the goroutine
|
`R29`/`R28`, riscv64 `X27`, loong64 `R22`) are verified on return and
|
||||||
pointer before returning into Go code. Runtime execution of the
|
the saved registers restored before Go code resumes, with the
|
||||||
non-amd64 JIT paths is not yet reliable (arm64 and loong64 fault on the
|
below-SP canary on every architecture. The root cause of the broken
|
||||||
return path and riscv64 returns a wrong result under qemu-user), so
|
non-amd64 calls was a stack misalignment: the trampolines advanced SP
|
||||||
`gasm verify` keeps JIT execution gated to amd64 kernels on amd64
|
past the linkage slot while the kernels read their first argument at
|
||||||
hosts; other kernels take the toolchain-comparison path exactly as
|
SP+8. riscv64 is validated end to end under qemu-user; arm64 shares
|
||||||
before. A qemu-user harness and GOARCH-guarded tests are in the tree
|
the fix and matches the observed frame convention; loong64 stays
|
||||||
to validate the trampolines once their return path is fixed.
|
ground-truth-only until hardware validation (the Go runtime cannot
|
||||||
|
start under the available loong64 emulators). `verify.Load` now
|
||||||
|
assembles each file with the encoder its name suffix calls for. The
|
||||||
|
`ABIReport` fields are renamed to the architecture-neutral
|
||||||
|
`FPClobbered` and `GClobbered`.
|
||||||
- **Hardware watchpoints on all architectures.** arm64 uses DBGWVR/DBGWCR
|
- **Hardware watchpoints on all architectures.** arm64 uses DBGWVR/DBGWCR
|
||||||
via `PTRACE_SETREGSET` with `NT_ARM_HW_BREAK`; riscv64 and loong64 use
|
via `PTRACE_SETREGSET` with `NT_ARM_HW_BREAK`; riscv64 and loong64 use
|
||||||
`PTRACE_POKEUSER` to access trigger/debug registers.
|
`PTRACE_POKEUSER` to access trigger/debug registers.
|
||||||
|
|||||||
+13
-8
@@ -1049,8 +1049,11 @@ With -smoke, each NOSPLIT function is called with a zeroed argument block to
|
|||||||
confirm the JIT trampoline works end-to-end. This is safe only for functions
|
confirm the JIT trampoline works end-to-end. This is safe only for functions
|
||||||
that tolerate nil pointers and zero lengths in their arguments.
|
that tolerate nil pointers and zero lengths in their arguments.
|
||||||
|
|
||||||
With -abi, each function is called with sentinel values in the callee-saved
|
With -abi, each function is called with sentinel values in the registers
|
||||||
registers (BP, R14) and a red-zone canary below SP; violations are reported.
|
the Go ABI fixes across calls (the frame pointer and the goroutine
|
||||||
|
pointer) plus a canary below SP; violations are reported. JIT-based
|
||||||
|
checks run when the host matches the file's architecture (all but
|
||||||
|
loong64, which is ground-truth only for now).
|
||||||
|
|
||||||
With -fuzz, each function with a // func signature is differentially fuzzed
|
With -fuzz, each function with a // func signature is differentially fuzzed
|
||||||
against the go-tool-asm version in a subprocess (so a crash on a partial
|
against the go-tool-asm version in a subprocess (so a crash on a partial
|
||||||
@@ -1091,17 +1094,19 @@ each entry reproduces.
|
|||||||
}
|
}
|
||||||
path := set.Arg(0)
|
path := set.Arg(0)
|
||||||
targetArch := arch.FromFilename(path)
|
targetArch := arch.FromFilename(path)
|
||||||
// JIT execution is enabled for amd64 kernels on amd64 hosts. The
|
// JIT execution runs when the host CPU matches the kernel's
|
||||||
// non-amd64 execution trampolines are implemented but not yet
|
// architecture, except loong64: its trampoline is implemented but not
|
||||||
// runtime-hardened, so other kernels take the toolchain-comparison
|
// yet validated against real hardware (the Go runtime cannot start
|
||||||
// path, which needs no execution.
|
// under the available loong64 emulators), so those kernels take the
|
||||||
if targetArch != arch.AMD64 || hostArch() != arch.AMD64 {
|
// toolchain-comparison path.
|
||||||
|
if targetArch != hostArch() || targetArch == arch.LOONG64 {
|
||||||
switch targetArch {
|
switch targetArch {
|
||||||
case arch.RISCV:
|
case arch.RISCV:
|
||||||
// RISC-V: ground-truth only (no JIT on non-RISC-V hosts).
|
// RISC-V: ground-truth only (no JIT on non-RISC-V hosts).
|
||||||
return cmdVerifyRISCV(path, *groundTruth, *profile)
|
return cmdVerifyRISCV(path, *groundTruth, *profile)
|
||||||
case arch.LOONG64:
|
case arch.LOONG64:
|
||||||
// LoongArch: ground-truth only (no JIT on non-LoongArch hosts).
|
// LoongArch: ground-truth only (trampoline not yet
|
||||||
|
// hardware-validated).
|
||||||
return cmdVerifyLOONG64(path, *groundTruth, *profile)
|
return cmdVerifyLOONG64(path, *groundTruth, *profile)
|
||||||
case arch.ARM64:
|
case arch.ARM64:
|
||||||
// AArch64: ground-truth only (no JIT on non-ARM64 hosts).
|
// AArch64: ground-truth only (no JIT on non-ARM64 hosts).
|
||||||
|
|||||||
@@ -369,11 +369,12 @@ every architecture too: `enterJITChecked` plants sentinels in the registers
|
|||||||
the Go ABI fixes across calls (amd64 `BP`/`R14`, arm64 `R29`/`R28`, riscv64
|
the Go ABI fixes across calls (amd64 `BP`/`R14`, arm64 `R29`/`R28`, riscv64
|
||||||
`X27`, loong64 `R22`; the latter two keep no hardware frame pointer) and the
|
`X27`, loong64 `R22`; the latter two keep no hardware frame pointer) and the
|
||||||
raw return trampoline `leaveJITCheckedRaw` verifies them, restoring the
|
raw return trampoline `leaveJITCheckedRaw` verifies them, restoring the
|
||||||
saved registers before Go code resumes. At present only the amd64 JIT path
|
saved registers before Go code resumes. riscv64 is validated end to
|
||||||
is runtime-proven: the non-amd64 trampolines compile and their kernels are
|
end under qemu-user emulation; arm64 shares the same stack convention and
|
||||||
correct, but the return into Go code still fails under emulation, so `gasm
|
fix; loong64 stays ground-truth-only until hardware validation (see
|
||||||
verify` gates JIT execution to amd64 kernels on amd64 hosts and runs only
|
docs/DECISIONS.md). `gasm verify` runs the JIT checks when the host
|
||||||
the toolchain comparisons elsewhere (see docs/DECISIONS.md).
|
matches the kernel's architecture and the toolchain comparisons
|
||||||
|
elsewhere.
|
||||||
|
|
||||||
`Load` / `LoadSource` / `LoadAST` parse, assemble and map a `.s` file in one
|
`Load` / `LoadSource` / `LoadAST` parse, assemble and map a `.s` file in one
|
||||||
step, returning a `Kernel` whose `CallFunc` method marshals the argument block
|
step, returning a `Kernel` whose `CallFunc` method marshals the argument block
|
||||||
|
|||||||
+18
-2
@@ -35,8 +35,24 @@ for the lifetime of the GOOBJ emission.
|
|||||||
|
|
||||||
## 2026-08-30 non-amd64 JIT execution trampolines
|
## 2026-08-30 non-amd64 JIT execution trampolines
|
||||||
|
|
||||||
**Status:** open (blocks runtime verification on arm64, riscv64 and
|
**Status:** resolved for riscv64 (validated end to end under qemu-user)
|
||||||
loong64 hosts).
|
and arm64 (fix in place, consistent with the observed frame convention);
|
||||||
|
open for loong64 until hardware validation.
|
||||||
|
|
||||||
|
**Root cause (found 2026-08-31).** The trampolines advanced SP past the
|
||||||
|
leave-address slot after loading it, while the assembled kernels read
|
||||||
|
their first argument at SP+8 per the frame convention (the amd64 path
|
||||||
|
already kept SP on that slot). Removing the advance fixed riscv64
|
||||||
|
immediately (plain and checked ABI tests pass under qemu-user); the
|
||||||
|
arm64 kernel's pre-fix trace showed exactly the same SP+8 reading. The
|
||||||
|
apparent arm64/loong64 "crashes in the JIT" turned out to be dominated
|
||||||
|
by an unrelated instability: the Go 1.26 and 1.27 runtimes crash under
|
||||||
|
qemu-user arm64 emulation (GC worker start, identical signature with the
|
||||||
|
JIT tests skipped, both qemu 7.2 and 10.2), and the Go loong64 runtime
|
||||||
|
does not start at all. `gasm verify` therefore keeps loong64 kernels on
|
||||||
|
the ground-truth path until hardware validation; the GOARCH-guarded
|
||||||
|
tests (`verify/jit_arch_test.go`, `verify/abi_arch_test.go`) are the
|
||||||
|
hardware validation entry point.
|
||||||
|
|
||||||
**State.** The per-architecture trampolines compile for all targets, the
|
**State.** The per-architecture trampolines compile for all targets, the
|
||||||
kernels they execute are byte-for-byte correct against `go tool asm`, and
|
kernels they execute are byte-for-byte correct against `go tool asm`, and
|
||||||
|
|||||||
+2
-2
@@ -43,8 +43,8 @@ TEXT ·enterJITChecked(SB), NOSPLIT, $0-16
|
|||||||
MOVD $SENTINEL_G, g // sentinel in g
|
MOVD $SENTINEL_G, g // sentinel in g
|
||||||
MOVD stack+8(FP), R3 // load prepared stack pointer
|
MOVD stack+8(FP), R3 // load prepared stack pointer
|
||||||
MOVD 0(R3), R30 // load leaveJITCheckedRaw into LR
|
MOVD 0(R3), R30 // load leaveJITCheckedRaw into LR
|
||||||
ADD $8, R3, R3 // advance past the return slot
|
MOVD R3, RSP // SP stays on the leave slot: the kernel
|
||||||
MOVD R3, RSP // switch to prepared stack
|
// reads its first argument at SP+8
|
||||||
JMP (R0) // branch to JIT function
|
JMP (R0) // branch to JIT function
|
||||||
|
|
||||||
// leaveJITCheckedRaw is the raw return trampoline. It has NO Go function
|
// leaveJITCheckedRaw is the raw return trampoline. It has NO Go function
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ TEXT ·enterJITChecked(SB), NOSPLIT, $0-16
|
|||||||
MOVV $SENTINEL_G, g // sentinel in g
|
MOVV $SENTINEL_G, g // sentinel in g
|
||||||
MOVV stack+8(FP), R5 // load prepared stack pointer (A1)
|
MOVV stack+8(FP), R5 // load prepared stack pointer (A1)
|
||||||
MOVV 0(R5), R1 // load leaveJITCheckedRaw into RA
|
MOVV 0(R5), R1 // load leaveJITCheckedRaw into RA
|
||||||
ADDV $8, R5, R5 // advance past the return slot
|
MOVV R5, R3 // SP stays on the leave slot: the kernel
|
||||||
MOVV R5, R3 // switch to prepared stack (SP)
|
// reads its first argument at SP+8
|
||||||
JIRL R0, R4, 0 // jump to JIT function
|
JIRL R0, R4, 0 // jump to JIT function
|
||||||
|
|
||||||
// leaveJITCheckedRaw is the raw return trampoline. It has NO Go function
|
// leaveJITCheckedRaw is the raw return trampoline. It has NO Go function
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ TEXT ·enterJITChecked(SB), NOSPLIT, $0-16
|
|||||||
MOV $SENTINEL_G, g // sentinel in g
|
MOV $SENTINEL_G, g // sentinel in g
|
||||||
MOV stack+8(FP), X6 // load prepared stack pointer (T1)
|
MOV stack+8(FP), X6 // load prepared stack pointer (T1)
|
||||||
LD 0(X6), X1 // load leaveJITCheckedRaw into RA
|
LD 0(X6), X1 // load leaveJITCheckedRaw into RA
|
||||||
ADD $8, X6, X6 // advance past the return slot
|
MOV X6, X2 // SP stays on the leave slot: the kernel
|
||||||
MOV X6, X2 // switch to prepared stack (SP)
|
// reads its first argument at SP+8
|
||||||
JALR X0, 0(X5) // jump to JIT function
|
JALR X0, 0(X5) // jump to JIT function
|
||||||
|
|
||||||
// leaveJITCheckedRaw is the raw return trampoline. It has NO Go function
|
// leaveJITCheckedRaw is the raw return trampoline. It has NO Go function
|
||||||
|
|||||||
@@ -20,8 +20,11 @@ TEXT ·enterJIT(SB), NOSPLIT, $0-16
|
|||||||
MOVD R3, savedSP(SB) // save Go stack pointer
|
MOVD R3, savedSP(SB) // save Go stack pointer
|
||||||
MOVD stack+8(FP), R3 // load prepared stack pointer
|
MOVD stack+8(FP), R3 // load prepared stack pointer
|
||||||
MOVD 0(R3), R30 // load leaveJIT address into LR
|
MOVD 0(R3), R30 // load leaveJIT address into LR
|
||||||
ADD $8, R3, R3 // advance past return address
|
MOVD R3, RSP // switch to the prepared stack: SP stays on
|
||||||
MOVD R3, RSP // switch to prepared stack
|
// the leave slot, so the kernel reads its
|
||||||
|
// first argument at SP+8 per the frame
|
||||||
|
// convention (amd64 lays the stack out the
|
||||||
|
// same way)
|
||||||
JMP (R0) // branch to JIT function
|
JMP (R0) // branch to JIT function
|
||||||
|
|
||||||
// func leaveJIT()
|
// func leaveJIT()
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ TEXT ·enterJIT(SB), NOSPLIT, $0-16
|
|||||||
MOVV R3, savedSP(SB) // save Go stack pointer (SP)
|
MOVV R3, savedSP(SB) // save Go stack pointer (SP)
|
||||||
MOVV stack+8(FP), R5 // load prepared stack pointer (A1)
|
MOVV stack+8(FP), R5 // load prepared stack pointer (A1)
|
||||||
MOVV 0(R5), R1 // load leaveJIT address into RA
|
MOVV 0(R5), R1 // load leaveJIT address into RA
|
||||||
ADDV $8, R5, R5 // advance past return address
|
MOVV R5, R3 // switch to the prepared stack: SP stays on
|
||||||
MOVV R5, R3 // switch to prepared stack (SP)
|
// the leave slot, so the kernel reads its
|
||||||
|
// first argument at SP+8
|
||||||
JIRL R0, R4, 0 // jump to JIT function
|
JIRL R0, R4, 0 // jump to JIT function
|
||||||
|
|
||||||
// func leaveJIT()
|
// func leaveJIT()
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ TEXT ·enterJIT(SB), NOSPLIT, $0-16
|
|||||||
MOV X2, savedSP(SB) // save Go stack pointer
|
MOV X2, savedSP(SB) // save Go stack pointer
|
||||||
MOV stack+8(FP), X6 // load prepared stack pointer (T1)
|
MOV stack+8(FP), X6 // load prepared stack pointer (T1)
|
||||||
LD 0(X6), X1 // load leaveJIT address into RA
|
LD 0(X6), X1 // load leaveJIT address into RA
|
||||||
ADD $8, X6, X6 // advance past return address
|
MOV X6, X2 // switch to the prepared stack: SP stays on
|
||||||
MOV X6, X2 // switch to prepared stack (SP)
|
// the leave slot, so the kernel reads its
|
||||||
|
// first argument at SP+8
|
||||||
JALR X0, 0(X5) // jump to JIT function
|
JALR X0, 0(X5) // jump to JIT function
|
||||||
|
|
||||||
// func leaveJIT()
|
// func leaveJIT()
|
||||||
|
|||||||
Reference in New Issue
Block a user