191 lines
4.9 KiB
Go
191 lines
4.9 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
|
|
||
|
|
//go:build freebsd && amd64
|
||
|
|
|
||
|
|
package debug
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"unsafe"
|
||
|
|
|
||
|
|
"golang.org/x/sys/unix"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Hardware watchpoint support via x86-64 debug registers (DR0-DR3, DR7),
|
||
|
|
// read and written as one blob through PT_GETDBREGS/PT_SETDBREGS. The
|
||
|
|
// FreeBSD struct dbreg is the raw DR file: dr[16], where DR0-DR3 are the
|
||
|
|
// address registers, DR6 the status and DR7 the control (sys/x86/include/
|
||
|
|
// reg.h; the DBREG_DRX accessor indexes the same array).
|
||
|
|
|
||
|
|
// dbreg mirrors FreeBSD's struct dbreg for PT_GETDBREGS/PT_SETDBREGS.
|
||
|
|
type dbreg struct {
|
||
|
|
Dr [16]uint64
|
||
|
|
}
|
||
|
|
|
||
|
|
// dbreg indices of the registers the watchpoint layer drives.
|
||
|
|
const (
|
||
|
|
drStatus = 6 // DR6: the trap status register
|
||
|
|
drControl = 7 // DR7: the debug control register
|
||
|
|
)
|
||
|
|
|
||
|
|
// 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: four address registers, DR0-DR3.
|
||
|
|
func maxWatchpoints() int { return 4 }
|
||
|
|
|
||
|
|
// 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 amd64 the kernel
|
||
|
|
// delivers both the completed single-step and the debug-register hit
|
||
|
|
// through T_TRCTRAP with TRAP_TRACE (sys/amd64/amd64/trap.c), and DR6's
|
||
|
|
// B0-B3 bits name the watchpoint that fired.
|
||
|
|
func archStopTrace(s *Session, siAddr uint64) (StopReason, uint64) {
|
||
|
|
dr, err := s.getDbRegs()
|
||
|
|
if err != nil {
|
||
|
|
return StopSingleStep, 0
|
||
|
|
}
|
||
|
|
if status := dr.Dr[drStatus]; status&0xF != 0 {
|
||
|
|
for slot := range 4 {
|
||
|
|
if status&(1<<slot) != 0 && dr.Dr[slot] != 0 {
|
||
|
|
return StopWatchpoint, dr.Dr[slot]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return StopSingleStep, 0
|
||
|
|
}
|
||
|
|
|
||
|
|
// FindFreeWatchpointSlot returns the index of the first free watchpoint slot
|
||
|
|
// (0-3), or -1 if all four hardware watchpoints are in use.
|
||
|
|
func (s *Session) FindFreeWatchpointSlot() int {
|
||
|
|
for i := range 4 {
|
||
|
|
if !s.wpSlots[i] {
|
||
|
|
return i
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return -1
|
||
|
|
}
|
||
|
|
|
||
|
|
// IsWatchpointSlotUsed reports whether slot (0-3) currently holds a watchpoint.
|
||
|
|
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
||
|
|
if slot < 0 || slot > 3 {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
return s.wpSlots[slot]
|
||
|
|
}
|
||
|
|
|
||
|
|
// SetWatchpoint installs a hardware watchpoint on the given address.
|
||
|
|
// DR7's encoding is architectural: a 2-bit local/global enable pair per
|
||
|
|
// slot at bit 2*slot, the R/W field at 16+4*slot and the length field at
|
||
|
|
// 18+4*slot (Intel SDM vol 3, "Debug Registers").
|
||
|
|
func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error {
|
||
|
|
if slot < 0 || slot > 3 {
|
||
|
|
return fmt.Errorf("debug: watchpoint slot must be 0-3")
|
||
|
|
}
|
||
|
|
if s.wpSlots[slot] {
|
||
|
|
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
||
|
|
}
|
||
|
|
|
||
|
|
var lenBits uint64
|
||
|
|
switch size {
|
||
|
|
case 1:
|
||
|
|
lenBits = 0
|
||
|
|
case 2:
|
||
|
|
lenBits = 1
|
||
|
|
case 4:
|
||
|
|
lenBits = 3
|
||
|
|
case 8:
|
||
|
|
lenBits = 2
|
||
|
|
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)
|
||
|
|
}
|
||
|
|
dr.Dr[slot] = addr
|
||
|
|
|
||
|
|
dr7 := dr.Dr[drControl]
|
||
|
|
enableBit := uint64(1) << (2 * slot)
|
||
|
|
rwBits := uint64(typ) << (16 + 4*slot)
|
||
|
|
lenField := lenBits << (18 + 4*slot)
|
||
|
|
mask := ^((uint64(1) << (2 * slot)) | (uint64(3) << (16 + 4*slot)) | (uint64(3) << (18 + 4*slot)))
|
||
|
|
dr.Dr[drControl] = (dr7 & mask) | enableBit | rwBits | lenField
|
||
|
|
|
||
|
|
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 > 3 {
|
||
|
|
return fmt.Errorf("debug: watchpoint slot must be 0-3")
|
||
|
|
}
|
||
|
|
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.Dr[slot] = 0
|
||
|
|
dr.Dr[drControl] &^= uint64(1) << (2 * slot)
|
||
|
|
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
|
||
|
|
}
|