feat(verify): add Call trampolines for arm64, riscv64, loong64
Assisted-by: MiMo V2.5 Pro
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunTarget is the debuggee entry point (gasm debug --target).
|
// 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)
|
os.WriteFile(tmpDir+"/entry", []byte("ok"), 0o644)
|
||||||
syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
|
syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
|
||||||
|
|
||||||
// JIT execution requires architecture-specific verify.Call trampoline.
|
fnAddr := codeBase + uintptr(fl.Offset)
|
||||||
// The debugger can still set breakpoints and inspect state without it.
|
stackArgs := make([]byte, fl.Args)
|
||||||
return fmt.Errorf("debug: JIT execution not yet implemented for arm64")
|
copy(stackArgs, args)
|
||||||
|
|
||||||
|
_, callErr := verify.Call(fnAddr, stackArgs)
|
||||||
|
if callErr != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunTarget is the debuggee entry point (gasm debug --target).
|
// 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)
|
os.WriteFile(tmpDir+"/entry", []byte("ok"), 0o644)
|
||||||
syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
|
syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
|
||||||
|
|
||||||
// JIT execution requires architecture-specific verify.Call trampoline.
|
fnAddr := codeBase + uintptr(fl.Offset)
|
||||||
// The debugger can still set breakpoints and inspect state without it.
|
stackArgs := make([]byte, fl.Args)
|
||||||
return fmt.Errorf("debug: JIT execution not yet implemented for loong64")
|
copy(stackArgs, args)
|
||||||
|
|
||||||
|
_, callErr := verify.Call(fnAddr, stackArgs)
|
||||||
|
if callErr != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunTarget is the debuggee entry point (gasm debug --target).
|
// 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)
|
os.WriteFile(tmpDir+"/entry", []byte("ok"), 0o644)
|
||||||
syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
|
syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
|
||||||
|
|
||||||
// JIT execution requires architecture-specific verify.Call trampoline.
|
fnAddr := codeBase + uintptr(fl.Offset)
|
||||||
// The debugger can still set breakpoints and inspect state without it.
|
stackArgs := make([]byte, fl.Args)
|
||||||
return fmt.Errorf("debug: JIT execution not yet implemented for riscv64")
|
copy(stackArgs, args)
|
||||||
|
|
||||||
|
_, callErr := verify.Call(fnAddr, stackArgs)
|
||||||
|
if callErr != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,14 @@
|
|||||||
|
|
||||||
package debug
|
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.
|
// WatchpointType selects what triggers the watchpoint.
|
||||||
type WatchpointType int
|
type WatchpointType int
|
||||||
@@ -16,10 +23,28 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// wpSlots tracks watchpoint slot occupancy.
|
// 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 {
|
func (s *Session) FindFreeWatchpointSlot() int {
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < maxWatchpoints; i++ {
|
||||||
if !wpSlots[i] {
|
if !wpSlots[i] {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
@@ -28,20 +53,129 @@ func (s *Session) FindFreeWatchpointSlot() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
||||||
if slot < 0 || slot > 3 {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return wpSlots[slot]
|
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 {
|
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 {
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,13 @@
|
|||||||
|
|
||||||
package debug
|
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.
|
// WatchpointType selects what triggers the watchpoint.
|
||||||
type WatchpointType int
|
type WatchpointType int
|
||||||
@@ -18,8 +24,10 @@ const (
|
|||||||
// wpSlots tracks watchpoint slot occupancy.
|
// wpSlots tracks watchpoint slot occupancy.
|
||||||
var wpSlots [4]bool
|
var wpSlots [4]bool
|
||||||
|
|
||||||
|
const maxWatchpoints = 4
|
||||||
|
|
||||||
func (s *Session) FindFreeWatchpointSlot() int {
|
func (s *Session) FindFreeWatchpointSlot() int {
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < maxWatchpoints; i++ {
|
||||||
if !wpSlots[i] {
|
if !wpSlots[i] {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
@@ -28,20 +36,112 @@ func (s *Session) FindFreeWatchpointSlot() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
||||||
if slot < 0 || slot > 3 {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return wpSlots[slot]
|
return wpSlots[slot]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetWatchpoint installs a hardware watchpoint.
|
||||||
func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error {
|
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 {
|
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 {
|
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
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,13 @@
|
|||||||
|
|
||||||
package debug
|
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.
|
// WatchpointType selects what triggers the watchpoint.
|
||||||
type WatchpointType int
|
type WatchpointType int
|
||||||
@@ -18,8 +24,10 @@ const (
|
|||||||
// wpSlots tracks watchpoint slot occupancy.
|
// wpSlots tracks watchpoint slot occupancy.
|
||||||
var wpSlots [4]bool
|
var wpSlots [4]bool
|
||||||
|
|
||||||
|
const maxWatchpoints = 4
|
||||||
|
|
||||||
func (s *Session) FindFreeWatchpointSlot() int {
|
func (s *Session) FindFreeWatchpointSlot() int {
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < maxWatchpoints; i++ {
|
||||||
if !wpSlots[i] {
|
if !wpSlots[i] {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
@@ -28,20 +36,115 @@ func (s *Session) FindFreeWatchpointSlot() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
||||||
if slot < 0 || slot > 3 {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return wpSlots[slot]
|
return wpSlots[slot]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetWatchpoint installs a hardware watchpoint.
|
||||||
func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error {
|
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 {
|
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 {
|
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
|
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
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
//go:build !amd64
|
//go:build !amd64 && !arm64 && !riscv64 && !loong64
|
||||||
|
|
||||||
package verify
|
package verify
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
//go:build !amd64
|
//go:build !amd64 && !arm64 && !riscv64 && !loong64
|
||||||
|
|
||||||
package verify
|
package verify
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
|
||||||
Reference in New Issue
Block a user