feat(verify): add Call trampolines for arm64, riscv64, loong64

Assisted-by: MiMo V2.5 Pro
This commit is contained in:
2026-08-21 00:35:21 +02:00
parent 1160c96427
commit 181d8e508c
14 changed files with 643 additions and 27 deletions
+11 -3
View File
@@ -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
}
+11 -3
View File
@@ -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
}
+11 -3
View File
@@ -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
}
+140 -6
View File
@@ -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
}
+105 -5
View File
@@ -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
}
+108 -5
View File
@@ -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
}