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