// Copyright (c) 2026 Petr BalvĂ­n (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< 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 }