206 lines
5.3 KiB
Go
206 lines
5.3 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build linux && loong64
|
|
|
|
package debug
|
|
|
|
import (
|
|
"fmt"
|
|
"syscall"
|
|
"unsafe"
|
|
)
|
|
|
|
// Hardware watchpoint support via the NT_LOONGARCH_HW_WATCH regset.
|
|
//
|
|
// The kernel's PTRACE_POKEUSER on loong64 accepts only the user_pt_regs
|
|
// indices 0-34 (GPRs, orig_a0, era, badv, per
|
|
// arch/loongarch/include/uapi/asm/ptrace.h), so there is no debug-register
|
|
// window to poke. The real interface is PTRACE_GETREGSET/SETREGSET on
|
|
// NT_LOONGARCH_HW_WATCH (0xa06, include/uapi/linux/elf.h) with struct
|
|
// user_watch_state_v2 (arch/loongarch/include/uapi/asm/ptrace.h): a dbg_info
|
|
// word followed by 14 slots of {addr u64, mask u64, ctrl u32, pad u32}.
|
|
// hw_break_get puts the slot count in the low byte of dbg_info
|
|
// (arch/loongarch/kernel/ptrace.c, ptrace_hbp_get_resource_info) and
|
|
// hw_break_set ignores dbg_info, reading addr, mask and ctrl per slot.
|
|
|
|
const ntLoongHWWatch = 0xa06
|
|
|
|
// loongWatchState mirrors the kernel's struct user_watch_state_v2.
|
|
type loongWatchState struct {
|
|
DbgInfo uint64
|
|
DbgRegs [14]loongWatchReg
|
|
}
|
|
|
|
type loongWatchReg struct {
|
|
Addr uint64
|
|
Mask uint64
|
|
Ctrl uint32
|
|
Pad uint32
|
|
}
|
|
|
|
// Control word bit layout (arch/loongarch/include/asm/hw_breakpoint.h):
|
|
// bits 1-4 privilege enables (CTRL_PLV3_ENABLE, 0x10, covers user mode),
|
|
// bits 8-9 access type (LOAD 1<<0, STORE 1<<1), bits 10-11 length
|
|
// (0=8 bytes, 1=4, 2=2, 3=1, inverted like the hardware FWP cfg).
|
|
const (
|
|
loongCtrlPLV3Enable = 0x10
|
|
loongTypeLoad = 1 << 8
|
|
loongTypeStore = 2 << 8
|
|
loongLenShift = 10
|
|
)
|
|
|
|
// WatchpointType selects what triggers the watchpoint.
|
|
type WatchpointType int
|
|
|
|
const (
|
|
WatchWrite WatchpointType = 1
|
|
WatchRead WatchpointType = 3
|
|
)
|
|
|
|
// maxWatchpoints reports the slot capacity of the regset struct; the number
|
|
// the hardware actually provides is read from dbg_info at arm time.
|
|
func maxWatchpoints() int { return len(loongWatchState{}.DbgRegs) }
|
|
|
|
// archWatchpointAddr resolves the address of the watchpoint that fired:
|
|
// the loongarch kernel already reports the accessed address as si_addr.
|
|
func archWatchpointAddr(s *Session, siAddr uint64) uint64 { return siAddr }
|
|
|
|
func (s *Session) FindFreeWatchpointSlot() int {
|
|
for i := range maxWatchpoints() {
|
|
if !s.wpSlots[i] {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
|
if slot < 0 || slot >= maxWatchpoints() {
|
|
return false
|
|
}
|
|
return s.wpSlots[slot]
|
|
}
|
|
|
|
// SetWatchpoint installs a hardware watchpoint on the given address.
|
|
func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error {
|
|
if slot < 0 || slot >= maxWatchpoints() {
|
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints()-1)
|
|
}
|
|
if s.wpSlots[slot] {
|
|
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
|
}
|
|
|
|
var ctrlType uint32
|
|
switch typ {
|
|
case WatchWrite:
|
|
ctrlType = loongTypeStore
|
|
case WatchRead:
|
|
ctrlType = loongTypeLoad | loongTypeStore
|
|
}
|
|
var lenBits uint32
|
|
switch size {
|
|
case 1:
|
|
lenBits = 3
|
|
case 2:
|
|
lenBits = 2
|
|
case 4:
|
|
lenBits = 1
|
|
case 8:
|
|
lenBits = 0
|
|
default:
|
|
return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8")
|
|
}
|
|
|
|
state, err := s.getLoongWatchState()
|
|
if err != nil {
|
|
return fmt.Errorf("debug: read watchpoint state: %w", err)
|
|
}
|
|
if uint64(slot) >= state.DbgInfo&0xff {
|
|
return fmt.Errorf("debug: slot %d exceeds available watchpoints (%d)", slot, state.DbgInfo&0xff)
|
|
}
|
|
|
|
state.DbgRegs[slot].Addr = addr
|
|
state.DbgRegs[slot].Mask = 0
|
|
state.DbgRegs[slot].Ctrl = loongCtrlPLV3Enable | ctrlType | lenBits<<loongLenShift
|
|
|
|
if err := s.setLoongWatchState(state); err != nil {
|
|
return fmt.Errorf("debug: set watchpoint: %w", err)
|
|
}
|
|
|
|
s.wpSlots[slot] = true
|
|
return nil
|
|
}
|
|
|
|
func (s *Session) ClearWatchpoint(slot int) error {
|
|
if slot < 0 || slot >= maxWatchpoints() {
|
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints()-1)
|
|
}
|
|
if !s.wpSlots[slot] {
|
|
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
|
}
|
|
|
|
state, err := s.getLoongWatchState()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
state.DbgRegs[slot].Addr = 0
|
|
state.DbgRegs[slot].Mask = 0
|
|
state.DbgRegs[slot].Ctrl = 0
|
|
if err := s.setLoongWatchState(state); err != nil {
|
|
return err
|
|
}
|
|
s.wpSlots[slot] = false
|
|
return nil
|
|
}
|
|
|
|
func (s *Session) ClearAllWatchpoints() error {
|
|
for slot := range maxWatchpoints() {
|
|
if s.wpSlots[slot] {
|
|
if err := s.ClearWatchpoint(slot); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Session) getLoongWatchState() (*loongWatchState, error) {
|
|
var state loongWatchState
|
|
iovec := syscall.Iovec{
|
|
Base: (*byte)(unsafe.Pointer(&state)),
|
|
Len: uint64(unsafe.Sizeof(state)),
|
|
}
|
|
_, _, errno := syscall.Syscall6(
|
|
syscall.SYS_PTRACE,
|
|
uintptr(syscall.PTRACE_GETREGSET),
|
|
uintptr(s.pid),
|
|
uintptr(ntLoongHWWatch),
|
|
uintptr(unsafe.Pointer(&iovec)),
|
|
0, 0,
|
|
)
|
|
if errno != 0 {
|
|
return nil, errno
|
|
}
|
|
return &state, nil
|
|
}
|
|
|
|
func (s *Session) setLoongWatchState(state *loongWatchState) error {
|
|
iovec := syscall.Iovec{
|
|
Base: (*byte)(unsafe.Pointer(state)),
|
|
Len: uint64(unsafe.Sizeof(*state)),
|
|
}
|
|
_, _, errno := syscall.Syscall6(
|
|
syscall.SYS_PTRACE,
|
|
uintptr(syscall.PTRACE_SETREGSET),
|
|
uintptr(s.pid),
|
|
uintptr(ntLoongHWWatch),
|
|
uintptr(unsafe.Pointer(&iovec)),
|
|
0, 0,
|
|
)
|
|
if errno != 0 {
|
|
return errno
|
|
}
|
|
return nil
|
|
}
|