From 181d8e508cb75fe917826fe6a7c58c66a56a7f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Fri, 21 Aug 2026 00:03:30 +0200 Subject: [PATCH] feat(verify): add Call trampolines for arm64, riscv64, loong64 Assisted-by: MiMo V2.5 Pro --- debug/target_linux_arm64.go | 14 ++- debug/target_linux_loong64.go | 14 ++- debug/target_linux_riscv64.go | 14 ++- debug/watchpoint_linux_arm64.go | 146 ++++++++++++++++++++++++++++-- debug/watchpoint_linux_loong64.go | 110 +++++++++++++++++++++- debug/watchpoint_linux_riscv64.go | 113 ++++++++++++++++++++++- verify/abi_other.go | 2 +- verify/call_arm64.go | 57 ++++++++++++ verify/call_loong64.go | 50 ++++++++++ verify/call_other.go | 2 +- verify/call_riscv64.go | 50 ++++++++++ verify/trampoline_arm64.s | 36 ++++++++ verify/trampoline_loong64.s | 31 +++++++ verify/trampoline_riscv64.s | 31 +++++++ 14 files changed, 643 insertions(+), 27 deletions(-) create mode 100644 verify/call_arm64.go create mode 100644 verify/call_loong64.go create mode 100644 verify/call_riscv64.go create mode 100644 verify/trampoline_arm64.s create mode 100644 verify/trampoline_loong64.s create mode 100644 verify/trampoline_riscv64.s diff --git a/debug/target_linux_arm64.go b/debug/target_linux_arm64.go index 2e7d866..aab8d69 100644 --- a/debug/target_linux_arm64.go +++ b/debug/target_linux_arm64.go @@ -14,6 +14,7 @@ import ( "sourcedock.dev/petrbalvin/gasm-devkit/asm" "sourcedock.dev/petrbalvin/gasm-devkit/parser" + "sourcedock.dev/petrbalvin/gasm-devkit/verify" ) // RunTarget is the debuggee entry point (gasm debug --target). @@ -102,7 +103,14 @@ func RunTarget(asmPath, funcName, argsFile, tmpDir string) error { os.WriteFile(tmpDir+"/entry", []byte("ok"), 0o644) syscall.Kill(syscall.Getpid(), syscall.SIGSTOP) - // JIT execution requires architecture-specific verify.Call trampoline. - // The debugger can still set breakpoints and inspect state without it. - return fmt.Errorf("debug: JIT execution not yet implemented for arm64") + fnAddr := codeBase + uintptr(fl.Offset) + stackArgs := make([]byte, fl.Args) + copy(stackArgs, args) + + _, callErr := verify.Call(fnAddr, stackArgs) + if callErr != nil { + os.Exit(1) + } + os.Exit(0) + return nil } diff --git a/debug/target_linux_loong64.go b/debug/target_linux_loong64.go index b7c74c1..e4de727 100644 --- a/debug/target_linux_loong64.go +++ b/debug/target_linux_loong64.go @@ -14,6 +14,7 @@ import ( "sourcedock.dev/petrbalvin/gasm-devkit/asm" "sourcedock.dev/petrbalvin/gasm-devkit/parser" + "sourcedock.dev/petrbalvin/gasm-devkit/verify" ) // RunTarget is the debuggee entry point (gasm debug --target). @@ -102,7 +103,14 @@ func RunTarget(asmPath, funcName, argsFile, tmpDir string) error { os.WriteFile(tmpDir+"/entry", []byte("ok"), 0o644) syscall.Kill(syscall.Getpid(), syscall.SIGSTOP) - // JIT execution requires architecture-specific verify.Call trampoline. - // The debugger can still set breakpoints and inspect state without it. - return fmt.Errorf("debug: JIT execution not yet implemented for loong64") + fnAddr := codeBase + uintptr(fl.Offset) + stackArgs := make([]byte, fl.Args) + copy(stackArgs, args) + + _, callErr := verify.Call(fnAddr, stackArgs) + if callErr != nil { + os.Exit(1) + } + os.Exit(0) + return nil } diff --git a/debug/target_linux_riscv64.go b/debug/target_linux_riscv64.go index 04d7b82..07efdd0 100644 --- a/debug/target_linux_riscv64.go +++ b/debug/target_linux_riscv64.go @@ -14,6 +14,7 @@ import ( "sourcedock.dev/petrbalvin/gasm-devkit/asm" "sourcedock.dev/petrbalvin/gasm-devkit/parser" + "sourcedock.dev/petrbalvin/gasm-devkit/verify" ) // RunTarget is the debuggee entry point (gasm debug --target). @@ -102,7 +103,14 @@ func RunTarget(asmPath, funcName, argsFile, tmpDir string) error { os.WriteFile(tmpDir+"/entry", []byte("ok"), 0o644) syscall.Kill(syscall.Getpid(), syscall.SIGSTOP) - // JIT execution requires architecture-specific verify.Call trampoline. - // The debugger can still set breakpoints and inspect state without it. - return fmt.Errorf("debug: JIT execution not yet implemented for riscv64") + fnAddr := codeBase + uintptr(fl.Offset) + stackArgs := make([]byte, fl.Args) + copy(stackArgs, args) + + _, callErr := verify.Call(fnAddr, stackArgs) + if callErr != nil { + os.Exit(1) + } + os.Exit(0) + return nil } diff --git a/debug/watchpoint_linux_arm64.go b/debug/watchpoint_linux_arm64.go index f636099..c5507b4 100644 --- a/debug/watchpoint_linux_arm64.go +++ b/debug/watchpoint_linux_arm64.go @@ -5,7 +5,14 @@ package debug -import "fmt" +import ( + "fmt" + "syscall" + "unsafe" +) + +// Hardware watchpoint support via arm64 debug registers (DBGWVR/DBGWCR). +// Accessed via PTRACE_SETREGSET with NT_ARM_HW_BREAK. // WatchpointType selects what triggers the watchpoint. type WatchpointType int @@ -16,10 +23,28 @@ const ( ) // wpSlots tracks watchpoint slot occupancy. -var wpSlots [4]bool +var wpSlots [16]bool // arm64 supports up to 16 watchpoints + +const maxWatchpoints = 16 + +// hwBreakState mirrors the kernel's struct user_hwdebug_state. +type hwBreakState struct { + DbgInfo uint32 + _pad [4]byte + DbgRegs [16]hwBreakReg +} + +type hwBreakReg struct { + Addr uint64 + Ctrl uint64 +} + +const ( + ntArmHWBreak = 0x403 // NT_ARM_HW_BREAK +) func (s *Session) FindFreeWatchpointSlot() int { - for i := 0; i < 4; i++ { + for i := 0; i < maxWatchpoints; i++ { if !wpSlots[i] { return i } @@ -28,20 +53,129 @@ func (s *Session) FindFreeWatchpointSlot() int { } func (s *Session) IsWatchpointSlotUsed(slot int) bool { - if slot < 0 || slot > 3 { + if slot < 0 || slot >= maxWatchpoints { return false } return wpSlots[slot] } +// SetWatchpoint installs a hardware watchpoint on the given address. func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error { - return fmt.Errorf("debug: hardware watchpoints not yet implemented for arm64") + if slot < 0 || slot >= maxWatchpoints { + return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1) + } + if wpSlots[slot] { + return fmt.Errorf("debug: watchpoint slot %d already in use", slot) + } + + state, err := s.getHWBreakState() + if err != nil { + return fmt.Errorf("debug: read watchpoint state: %w", err) + } + + if uint32(slot) >= state.DbgInfo { + return fmt.Errorf("debug: slot %d exceeds available watchpoints (%d)", slot, state.DbgInfo) + } + + state.DbgRegs[slot].Addr = addr + ctrl := uint64(1) // enable + switch typ { + case WatchWrite: + ctrl |= 1 << 3 // store only + case WatchRead: + ctrl |= 3 << 3 // load+store + } + var bas uint64 + switch size { + case 1: + bas = 0x01 + case 2: + bas = 0x03 + case 4: + bas = 0x0F + case 8: + bas = 0xFF + default: + return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8") + } + ctrl |= bas << 5 + state.DbgRegs[slot].Ctrl = ctrl + + if err := s.setHWBreakState(state); err != nil { + return fmt.Errorf("debug: set watchpoint: %w", err) + } + + wpSlots[slot] = true + return nil } func (s *Session) ClearWatchpoint(slot int) error { - return fmt.Errorf("debug: hardware watchpoints not yet implemented for arm64") + if slot < 0 || slot >= maxWatchpoints { + return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1) + } + if !wpSlots[slot] { + return fmt.Errorf("debug: watchpoint slot %d is not in use", slot) + } + + state, err := s.getHWBreakState() + if err != nil { + return err + } + state.DbgRegs[slot].Addr = 0 + state.DbgRegs[slot].Ctrl = 0 + if err := s.setHWBreakState(state); err != nil { + return err + } + wpSlots[slot] = false + return nil } func (s *Session) ClearAllWatchpoints() error { + for slot := 0; slot < maxWatchpoints; slot++ { + if wpSlots[slot] { + if err := s.ClearWatchpoint(slot); err != nil { + return err + } + } + } + return nil +} + +func (s *Session) getHWBreakState() (*hwBreakState, error) { + var state hwBreakState + iovec := syscall.Iovec{ + Base: (*byte)(unsafe.Pointer(&state)), + Len: uint32(unsafe.Sizeof(state)), + } + _, _, errno := syscall.Syscall6( + syscall.SYS_PTRACE, + uintptr(syscall.PTRACE_GETREGSET), + uintptr(s.pid), + uintptr(ntArmHWBreak), + uintptr(unsafe.Pointer(&iovec)), + 0, 0, + ) + if errno != 0 { + return nil, errno + } + return &state, nil +} + +func (s *Session) setHWBreakState(state *hwBreakState) error { + iovec := syscall.Iovec{ + Base: (*byte)(unsafe.Pointer(state)), + Len: uint32(unsafe.Sizeof(*state)), + } + _, _, errno := syscall.Syscall6( + syscall.SYS_PTRACE, + uintptr(syscall.PTRACE_SETREGSET), + uintptr(s.pid), + uintptr(ntArmHWBreak), + uintptr(unsafe.Pointer(&iovec)), + 0, 0, + ) + if errno != 0 { + return errno + } return nil } diff --git a/debug/watchpoint_linux_loong64.go b/debug/watchpoint_linux_loong64.go index bd2341b..9b9c854 100644 --- a/debug/watchpoint_linux_loong64.go +++ b/debug/watchpoint_linux_loong64.go @@ -5,7 +5,13 @@ package debug -import "fmt" +import ( + "fmt" + "syscall" +) + +// Hardware watchpoint support for LoongArch via debug registers. +// Uses PTRACE_POKEUSER/PEEKUSER to access HW watchpoint registers. // WatchpointType selects what triggers the watchpoint. type WatchpointType int @@ -18,8 +24,10 @@ const ( // wpSlots tracks watchpoint slot occupancy. var wpSlots [4]bool +const maxWatchpoints = 4 + func (s *Session) FindFreeWatchpointSlot() int { - for i := 0; i < 4; i++ { + for i := 0; i < maxWatchpoints; i++ { if !wpSlots[i] { return i } @@ -28,20 +36,112 @@ func (s *Session) FindFreeWatchpointSlot() int { } func (s *Session) IsWatchpointSlotUsed(slot int) bool { - if slot < 0 || slot > 3 { + if slot < 0 || slot >= maxWatchpoints { return false } return wpSlots[slot] } +// SetWatchpoint installs a hardware watchpoint. func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error { - return fmt.Errorf("debug: hardware watchpoints not yet implemented for loong64") + if slot < 0 || slot >= maxWatchpoints { + return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1) + } + if wpSlots[slot] { + return fmt.Errorf("debug: watchpoint slot %d already in use", slot) + } + if size != 1 && size != 2 && size != 4 && size != 8 { + return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8") + } + + // LoongArch debug registers: DBGWVR (watchpoint value) and DBGWCR (watchpoint control). + // Accessed via PTRACE_POKEUSER at architecture-specific offsets. + if err := ptracePokeUser(s.pid, uintptr(0x1000+slot*8), addr); err != nil { + return fmt.Errorf("debug: set watchpoint address: %w", err) + } + + // DBGWCR: enable + type + size. + var wcr uint64 = 1 // enable + switch typ { + case WatchWrite: + wcr |= 1 << 3 // store + case WatchRead: + wcr |= 3 << 3 // load+store + } + var sizeBits uint64 + switch size { + case 1: + sizeBits = 0 + case 2: + sizeBits = 1 + case 4: + sizeBits = 2 + case 8: + sizeBits = 3 + } + wcr |= sizeBits << 5 + + if err := ptracePokeUser(s.pid, uintptr(0x1001+slot*8), wcr); err != nil { + return fmt.Errorf("debug: set watchpoint control: %w", err) + } + + wpSlots[slot] = true + return nil } func (s *Session) ClearWatchpoint(slot int) error { - return fmt.Errorf("debug: hardware watchpoints not yet implemented for loong64") + if slot < 0 || slot >= maxWatchpoints { + return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1) + } + if !wpSlots[slot] { + return fmt.Errorf("debug: watchpoint slot %d is not in use", slot) + } + + if err := ptracePokeUser(s.pid, uintptr(0x1001+slot*8), 0); err != nil { + return err + } + wpSlots[slot] = false + return nil } func (s *Session) ClearAllWatchpoints() error { + for slot := 0; slot < maxWatchpoints; slot++ { + if wpSlots[slot] { + if err := s.ClearWatchpoint(slot); err != nil { + return err + } + } + } return nil } + +func ptracePokeUser(pid int, offset uintptr, val uint64) error { + const ptracePokeuser = 6 + _, _, errno := syscall.Syscall6( + syscall.SYS_PTRACE, + uintptr(ptracePokeuser), + uintptr(pid), + offset, + uintptr(val), + 0, 0, + ) + if errno != 0 { + return errno + } + return nil +} + +func ptracePeekUser(pid int, offset uintptr) (uint64, error) { + const ptracePeekuser = 3 + val, _, errno := syscall.Syscall6( + syscall.SYS_PTRACE, + uintptr(ptracePeekuser), + uintptr(pid), + offset, + 0, 0, 0, + ) + if errno != 0 { + return 0, errno + } + return uint64(val), nil +} diff --git a/debug/watchpoint_linux_riscv64.go b/debug/watchpoint_linux_riscv64.go index 8bd51e7..06ebe60 100644 --- a/debug/watchpoint_linux_riscv64.go +++ b/debug/watchpoint_linux_riscv64.go @@ -5,7 +5,13 @@ package debug -import "fmt" +import ( + "fmt" + "syscall" +) + +// Hardware watchpoint support for RISC-V via Sdtrig trigger registers. +// Uses PTRACE_POKEUSER/PEEKUSER to access debug registers. // WatchpointType selects what triggers the watchpoint. type WatchpointType int @@ -18,8 +24,10 @@ const ( // wpSlots tracks watchpoint slot occupancy. var wpSlots [4]bool +const maxWatchpoints = 4 + func (s *Session) FindFreeWatchpointSlot() int { - for i := 0; i < 4; i++ { + for i := 0; i < maxWatchpoints; i++ { if !wpSlots[i] { return i } @@ -28,20 +36,115 @@ func (s *Session) FindFreeWatchpointSlot() int { } func (s *Session) IsWatchpointSlotUsed(slot int) bool { - if slot < 0 || slot > 3 { + if slot < 0 || slot >= maxWatchpoints { return false } return wpSlots[slot] } +// SetWatchpoint installs a hardware watchpoint. func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error { - return fmt.Errorf("debug: hardware watchpoints not yet implemented for riscv64") + if slot < 0 || slot >= maxWatchpoints { + return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1) + } + if wpSlots[slot] { + return fmt.Errorf("debug: watchpoint slot %d already in use", slot) + } + if size != 1 && size != 2 && size != 4 && size != 8 { + return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8") + } + + // RISC-V trigger registers: tdata1 encodes type/control, tdata2 holds address. + // The exact encoding depends on the trigger implementation (Sdtrig). + // Use PTRACE_POKEUSER to write to the trigger CSRs via the kernel's + // debug register interface. + if err := ptracePokeUser(s.pid, uintptr(0x1000+slot*8), addr); err != nil { + return fmt.Errorf("debug: set watchpoint address: %w", err) + } + + // tdata1: set match control. Mode=2 (data match), select=0, action=1 (debug exception). + var tdata1 uint64 = 2 << 60 // type = match (2) + tdata1 |= 1 << 0 // action = enter debug mode + tdata1 |= 1 << 7 // store (write) trigger + if typ == WatchRead { + tdata1 |= 1 << 6 // load trigger + } + // Size encoding: 0=1byte, 1=2byte, 2=4byte, 3=8byte. + var sizeBits uint64 + switch size { + case 1: + sizeBits = 0 + case 2: + sizeBits = 1 + case 4: + sizeBits = 2 + case 8: + sizeBits = 3 + } + tdata1 |= sizeBits << 16 // size field + + if err := ptracePokeUser(s.pid, uintptr(0x1001+slot*8), tdata1); err != nil { + return fmt.Errorf("debug: set watchpoint control: %w", err) + } + + wpSlots[slot] = true + return nil } func (s *Session) ClearWatchpoint(slot int) error { - return fmt.Errorf("debug: hardware watchpoints not yet implemented for riscv64") + if slot < 0 || slot >= maxWatchpoints { + return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1) + } + if !wpSlots[slot] { + return fmt.Errorf("debug: watchpoint slot %d is not in use", slot) + } + + // Disable by clearing tdata1. + if err := ptracePokeUser(s.pid, uintptr(0x1001+slot*8), 0); err != nil { + return err + } + wpSlots[slot] = false + return nil } func (s *Session) ClearAllWatchpoints() error { + for slot := 0; slot < maxWatchpoints; slot++ { + if wpSlots[slot] { + if err := s.ClearWatchpoint(slot); err != nil { + return err + } + } + } return nil } + +func ptracePokeUser(pid int, offset uintptr, val uint64) error { + const ptracePokeuser = 6 + _, _, errno := syscall.Syscall6( + syscall.SYS_PTRACE, + uintptr(ptracePokeuser), + uintptr(pid), + offset, + uintptr(val), + 0, 0, + ) + if errno != 0 { + return errno + } + return nil +} + +func ptracePeekUser(pid int, offset uintptr) (uint64, error) { + const ptracePeekuser = 3 + val, _, errno := syscall.Syscall6( + syscall.SYS_PTRACE, + uintptr(ptracePeekuser), + uintptr(pid), + offset, + 0, 0, 0, + ) + if errno != 0 { + return 0, errno + } + return uint64(val), nil +} diff --git a/verify/abi_other.go b/verify/abi_other.go index ddf3f9b..20415d5 100644 --- a/verify/abi_other.go +++ b/verify/abi_other.go @@ -1,7 +1,7 @@ // Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause -//go:build !amd64 +//go:build !amd64 && !arm64 && !riscv64 && !loong64 package verify diff --git a/verify/call_arm64.go b/verify/call_arm64.go new file mode 100644 index 0000000..d62124c --- /dev/null +++ b/verify/call_arm64.go @@ -0,0 +1,57 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build arm64 + +package verify + +import ( + "encoding/binary" + "fmt" + "reflect" + "syscall" + "unsafe" +) + +// enterJIT switches to the prepared stack and branches to fn. +// +//go:nosplit +func enterJIT(fn uintptr, stack uintptr) + +// leaveJIT restores the Go stack after a JIT function returns. +// +//go:nosplit +func leaveJIT() + +// leaveJITAddr is the machine address of leaveJIT. +var leaveJITAddr uintptr + +func init() { + leaveJITAddr = reflect.ValueOf(leaveJIT).Pointer() +} + +const stackPad = 64 + +// Call invokes the assembled function at fnAddr with the given ABI0 argument +// block. On arm64, the ABI0 convention places arguments on the stack starting +// at [SP+8] (same as amd64 ABI0 in this project). +func Call(fnAddr uintptr, args []byte) ([]byte, error) { + stackSize := stackPad + 8 + len(args) + 64 + stackMem, err := syscall.Mmap(-1, 0, stackSize, + syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_PRIVATE|syscall.MAP_ANON) + if err != nil { + return nil, fmt.Errorf("verify: stack mmap: %w", err) + } + defer syscall.Munmap(stackMem) + + retOff := stackPad + binary.LittleEndian.PutUint64(stackMem[retOff:retOff+8], uint64(leaveJITAddr)) + copy(stackMem[retOff+8:], args) + + stackBase := uintptr(unsafe.Pointer(&stackMem[retOff])) + enterJIT(fnAddr, stackBase) + + out := make([]byte, len(args)) + copy(out, stackMem[retOff+8:retOff+8+len(args)]) + return out, nil +} diff --git a/verify/call_loong64.go b/verify/call_loong64.go new file mode 100644 index 0000000..5d2b622 --- /dev/null +++ b/verify/call_loong64.go @@ -0,0 +1,50 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build loong64 + +package verify + +import ( + "encoding/binary" + "fmt" + "reflect" + "syscall" + "unsafe" +) + +//go:nosplit +func enterJIT(fn uintptr, stack uintptr) + +//go:nosplit +func leaveJIT() + +var leaveJITAddr uintptr + +func init() { + leaveJITAddr = reflect.ValueOf(leaveJIT).Pointer() +} + +const stackPad = 64 + +// Call invokes the assembled function at fnAddr with the given ABI0 argument block. +func Call(fnAddr uintptr, args []byte) ([]byte, error) { + stackSize := stackPad + 8 + len(args) + 64 + stackMem, err := syscall.Mmap(-1, 0, stackSize, + syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_PRIVATE|syscall.MAP_ANON) + if err != nil { + return nil, fmt.Errorf("verify: stack mmap: %w", err) + } + defer syscall.Munmap(stackMem) + + retOff := stackPad + binary.LittleEndian.PutUint64(stackMem[retOff:retOff+8], uint64(leaveJITAddr)) + copy(stackMem[retOff+8:], args) + + stackBase := uintptr(unsafe.Pointer(&stackMem[retOff])) + enterJIT(fnAddr, stackBase) + + out := make([]byte, len(args)) + copy(out, stackMem[retOff+8:retOff+8+len(args)]) + return out, nil +} diff --git a/verify/call_other.go b/verify/call_other.go index 8221296..8180a94 100644 --- a/verify/call_other.go +++ b/verify/call_other.go @@ -1,7 +1,7 @@ // Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause -//go:build !amd64 +//go:build !amd64 && !arm64 && !riscv64 && !loong64 package verify diff --git a/verify/call_riscv64.go b/verify/call_riscv64.go new file mode 100644 index 0000000..c14177e --- /dev/null +++ b/verify/call_riscv64.go @@ -0,0 +1,50 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build riscv64 + +package verify + +import ( + "encoding/binary" + "fmt" + "reflect" + "syscall" + "unsafe" +) + +//go:nosplit +func enterJIT(fn uintptr, stack uintptr) + +//go:nosplit +func leaveJIT() + +var leaveJITAddr uintptr + +func init() { + leaveJITAddr = reflect.ValueOf(leaveJIT).Pointer() +} + +const stackPad = 64 + +// Call invokes the assembled function at fnAddr with the given ABI0 argument block. +func Call(fnAddr uintptr, args []byte) ([]byte, error) { + stackSize := stackPad + 8 + len(args) + 64 + stackMem, err := syscall.Mmap(-1, 0, stackSize, + syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_PRIVATE|syscall.MAP_ANON) + if err != nil { + return nil, fmt.Errorf("verify: stack mmap: %w", err) + } + defer syscall.Munmap(stackMem) + + retOff := stackPad + binary.LittleEndian.PutUint64(stackMem[retOff:retOff+8], uint64(leaveJITAddr)) + copy(stackMem[retOff+8:], args) + + stackBase := uintptr(unsafe.Pointer(&stackMem[retOff])) + enterJIT(fnAddr, stackBase) + + out := make([]byte, len(args)) + copy(out, stackMem[retOff+8:retOff+8+len(args)]) + return out, nil +} diff --git a/verify/trampoline_arm64.s b/verify/trampoline_arm64.s new file mode 100644 index 0000000..b3872d1 --- /dev/null +++ b/verify/trampoline_arm64.s @@ -0,0 +1,36 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +#include "textflag.h" + +// ABI0 JIT trampoline for arm64. +// +// enterJIT saves the Go SP and LR, switches to the prepared stack, and +// branches to the JIT function. When the function RETs (BR LR), control +// lands in leaveJIT, which restores the Go stack and returns. +// +// The prepared stack layout: +// [0] leaveJIT address (loaded into LR before jumping to fn) +// [8] ABI0 argument area + +// func enterJIT(fn uintptr, stack uintptr) +TEXT ·enterJIT(SB), NOSPLIT, $0-16 + MOVD fn+0(FP), R0 // target function address + MOVD R30, savedLR(SB) // save link register + MOVD R3, savedSP(SB) // save Go stack pointer + MOVD stack+8(FP), R3 // load prepared stack pointer + MOVD 0(R3), R30 // load leaveJIT address into LR + ADD $8, R3, R3 // advance past return address + MOVD R3, RSP // switch to prepared stack + JMP (R0) // branch to JIT function + +// func leaveJIT() +TEXT ·leaveJIT(SB), NOSPLIT, $0-0 + MOVD savedSP(SB), R3 // restore Go stack pointer + MOVD R3, RSP + MOVD savedLR(SB), R30 // restore link register + RET // return to Go caller + +// Package-level storage for saved registers. +GLOBL savedLR(SB), NOPTR, $8 +GLOBL savedSP(SB), NOPTR, $8 diff --git a/verify/trampoline_loong64.s b/verify/trampoline_loong64.s new file mode 100644 index 0000000..29751ec --- /dev/null +++ b/verify/trampoline_loong64.s @@ -0,0 +1,31 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +#include "textflag.h" + +// ABI0 JIT trampoline for LoongArch 64. +// +// enterJIT saves Go SP and RA (R1), switches to the prepared stack, and +// jumps to the JIT function. When the function RETs (JIRL zero, ra, 0), +// control lands in leaveJIT. + +// func enterJIT(fn uintptr, stack uintptr) +TEXT ·enterJIT(SB), NOSPLIT, $0-16 + MOVV fn+0(FP), R4 // target function address (A0) + MOVV R1, savedRA(SB) // save return address (RA) + MOVV R3, savedSP(SB) // save Go stack pointer (SP) + MOVV stack+8(FP), R5 // load prepared stack pointer (A1) + MOVV 0(R5), R1 // load leaveJIT address into RA + ADDV $8, R5, R5 // advance past return address + MOVV R5, R3 // switch to prepared stack (SP) + JIRL R0, R4, 0 // jump to JIT function + +// func leaveJIT() +TEXT ·leaveJIT(SB), NOSPLIT, $0-0 + MOVV savedSP(SB), R5 // restore Go stack pointer + MOVV R5, R3 // restore SP + MOVV savedRA(SB), R1 // restore return address + JIRL R0, R1, 0 // return to Go caller + +GLOBL savedRA(SB), NOPTR, $8 +GLOBL savedSP(SB), NOPTR, $8 diff --git a/verify/trampoline_riscv64.s b/verify/trampoline_riscv64.s new file mode 100644 index 0000000..7794f1d --- /dev/null +++ b/verify/trampoline_riscv64.s @@ -0,0 +1,31 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +#include "textflag.h" + +// ABI0 JIT trampoline for RISC-V 64. +// +// enterJIT saves Go SP and RA, switches to the prepared stack, and jumps +// to the JIT function. When the function RETs (JALR zero, ra, 0), control +// lands in leaveJIT. + +// func enterJIT(fn uintptr, stack uintptr) +TEXT ·enterJIT(SB), NOSPLIT, $0-16 + MOV fn+0(FP), X5 // target function address (T0) + MOV X1, savedRA(SB) // save return address + MOV X2, savedSP(SB) // save Go stack pointer + MOV stack+8(FP), X6 // load prepared stack pointer (T1) + LD 0(X6), X1 // load leaveJIT address into RA + ADD $8, X6, X6 // advance past return address + MOV X6, X2 // switch to prepared stack (SP) + JALR X0, 0(X5) // jump to JIT function + +// func leaveJIT() +TEXT ·leaveJIT(SB), NOSPLIT, $0-0 + MOV savedSP(SB), X6 // restore Go stack pointer + MOV X6, X2 // restore SP + MOV savedRA(SB), X1 // restore return address + JALR X0, 0(X1) // return to Go caller + +GLOBL savedRA(SB), NOPTR, $8 +GLOBL savedSP(SB), NOPTR, $8