206 lines
5.4 KiB
Go
206 lines
5.4 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
|
|
||
|
|
//go:build freebsd && arm64
|
||
|
|
|
||
|
|
package debug
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"unsafe"
|
||
|
|
|
||
|
|
"golang.org/x/sys/unix"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Hardware watchpoint support via arm64 debug registers, read and written
|
||
|
|
// as one blob through PT_GETDBREGS/PT_SETDBREGS. The FreeBSD struct dbreg
|
||
|
|
// (sys/arm64/include/reg.h) opens with the debug-facility header and then
|
||
|
|
// carries 16 breakpoint and 16 watchpoint pairs of {address, control}.
|
||
|
|
|
||
|
|
// dbreg mirrors FreeBSD's struct dbreg for PT_GETDBREGS/PT_SETDBREGS.
|
||
|
|
type dbreg struct {
|
||
|
|
DbDebugVer uint8
|
||
|
|
DbNbkpts uint8
|
||
|
|
DbNwtpts uint8
|
||
|
|
_ [5]byte
|
||
|
|
DbBreakregs [16]struct {
|
||
|
|
Addr uint64
|
||
|
|
Ctrl uint32
|
||
|
|
_ uint32
|
||
|
|
}
|
||
|
|
DbWatchregs [16]struct {
|
||
|
|
Addr uint64
|
||
|
|
Ctrl uint32
|
||
|
|
_ uint32
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// WatchpointType selects what triggers the watchpoint.
|
||
|
|
type WatchpointType int
|
||
|
|
|
||
|
|
const (
|
||
|
|
WatchWrite WatchpointType = 1 // trigger on write
|
||
|
|
WatchRead WatchpointType = 3 // trigger on read or write
|
||
|
|
)
|
||
|
|
|
||
|
|
// maxWatchpoints reports the number of hardware watchpoint slots the
|
||
|
|
// architecture provides: DBGWVR0-DBGWCR15.
|
||
|
|
func maxWatchpoints() int { return 16 }
|
||
|
|
|
||
|
|
// getDbRegs reads the debug register file of the stopped debuggee.
|
||
|
|
func (s *Session) getDbRegs() (*dbreg, error) {
|
||
|
|
var dr dbreg
|
||
|
|
if _, _, errno := unix.Syscall6(
|
||
|
|
unix.SYS_PTRACE,
|
||
|
|
uintptr(unix.PT_GETDBREGS),
|
||
|
|
uintptr(s.pid),
|
||
|
|
0,
|
||
|
|
uintptr(unsafe.Pointer(&dr)),
|
||
|
|
0, 0,
|
||
|
|
); errno != 0 {
|
||
|
|
return nil, errno
|
||
|
|
}
|
||
|
|
return &dr, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// setDbRegs writes the debug register file of the stopped debuggee.
|
||
|
|
func (s *Session) setDbRegs(dr *dbreg) error {
|
||
|
|
if _, _, errno := unix.Syscall6(
|
||
|
|
unix.SYS_PTRACE,
|
||
|
|
uintptr(unix.PT_SETDBREGS),
|
||
|
|
uintptr(s.pid),
|
||
|
|
0,
|
||
|
|
uintptr(unsafe.Pointer(dr)),
|
||
|
|
0, 0,
|
||
|
|
); errno != 0 {
|
||
|
|
return errno
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// archStopTrace classifies a TRAP_TRACE stop. On arm64 the kernel
|
||
|
|
// delivers both the software single step and the watchpoint hit through
|
||
|
|
// EXCP_SOFTSTP_EL0/EXCP_WATCHPT_EL0 with TRAP_TRACE (sys/arm64/arm64/
|
||
|
|
// trap.c); the watchpoint address rides the FAR register, so a stop whose
|
||
|
|
// reported address falls inside an armed watchpoint's byte range is a
|
||
|
|
// watchpoint and everything else is a single step.
|
||
|
|
func archStopTrace(s *Session, siAddr uint64) (StopReason, uint64) {
|
||
|
|
dr, err := s.getDbRegs()
|
||
|
|
if err != nil {
|
||
|
|
return StopSingleStep, 0
|
||
|
|
}
|
||
|
|
for slot := range 16 {
|
||
|
|
ctrl := uint64(dr.DbWatchregs[slot].Ctrl)
|
||
|
|
if ctrl&1 == 0 || dr.DbWatchregs[slot].Addr == 0 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if bas := (ctrl >> 5) & 0xFF; bas != 0 && siAddr >= dr.DbWatchregs[slot].Addr && siAddr < dr.DbWatchregs[slot].Addr+8 {
|
||
|
|
return StopWatchpoint, siAddr
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return StopSingleStep, 0
|
||
|
|
}
|
||
|
|
|
||
|
|
// FindFreeWatchpointSlot returns the index of the first free watchpoint
|
||
|
|
// slot, or -1 if all of them are in use.
|
||
|
|
func (s *Session) FindFreeWatchpointSlot() int {
|
||
|
|
for i := range maxWatchpoints() {
|
||
|
|
if !s.wpSlots[i] {
|
||
|
|
return i
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return -1
|
||
|
|
}
|
||
|
|
|
||
|
|
// IsWatchpointSlotUsed reports whether slot currently holds a watchpoint.
|
||
|
|
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. The
|
||
|
|
// control word is the architectural DBGWCR (ARM DDI 0487): bit 0 enables,
|
||
|
|
// bits 3-4 select the access type (10 store, 11 load+store) and bits 5-12
|
||
|
|
// are the byte-address select, so the watch stays 8-byte aligned and names
|
||
|
|
// its watched bytes through BAS.
|
||
|
|
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 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")
|
||
|
|
}
|
||
|
|
|
||
|
|
dr, err := s.getDbRegs()
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("debug: read debug registers: %w", err)
|
||
|
|
}
|
||
|
|
if uint8(slot) >= dr.DbNwtpts && dr.DbNwtpts != 0 {
|
||
|
|
return fmt.Errorf("debug: slot %d exceeds available watchpoints (%d)", slot, dr.DbNwtpts)
|
||
|
|
}
|
||
|
|
ctrl := uint64(1) // enable
|
||
|
|
switch typ {
|
||
|
|
case WatchWrite:
|
||
|
|
ctrl |= 2 << 3 // store only
|
||
|
|
case WatchRead:
|
||
|
|
ctrl |= 3 << 3 // load+store
|
||
|
|
}
|
||
|
|
ctrl |= bas << 5
|
||
|
|
dr.DbWatchregs[slot].Addr = addr
|
||
|
|
dr.DbWatchregs[slot].Ctrl = uint32(ctrl)
|
||
|
|
|
||
|
|
if err := s.setDbRegs(dr); err != nil {
|
||
|
|
return fmt.Errorf("debug: set debug registers: %w", err)
|
||
|
|
}
|
||
|
|
s.wpSlots[slot] = true
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// ClearWatchpoint removes a hardware watchpoint.
|
||
|
|
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)
|
||
|
|
}
|
||
|
|
dr, err := s.getDbRegs()
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
dr.DbWatchregs[slot].Addr = 0
|
||
|
|
dr.DbWatchregs[slot].Ctrl = 0
|
||
|
|
if err := s.setDbRegs(dr); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
s.wpSlots[slot] = false
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// ClearAllWatchpoints removes all hardware watchpoints.
|
||
|
|
func (s *Session) ClearAllWatchpoints() error {
|
||
|
|
for slot := range maxWatchpoints() {
|
||
|
|
if s.wpSlots[slot] {
|
||
|
|
if err := s.ClearWatchpoint(slot); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|