// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause //go:build linux && amd64 package debug import ( "fmt" "syscall" ) // Hardware watchpoint support via x86-64 debug registers (DR0-DR3, DR7). // // DR0-DR3 hold the watched addresses. DR7 is the control register: // bits 0,2,4,6: local enable for DR0-DR3 // bits 16-17,20-21,24-25,28-29: R/W type (00=exec, 01=write, 11=read/write) // bits 18-19,22-23,26-27,30-31: length (00=1, 01=2, 10=8, 11=4) // WatchpointType selects what triggers the watchpoint. type WatchpointType int const ( WatchWrite WatchpointType = 1 // trigger on write WatchRead WatchpointType = 3 // trigger on read or write ) // 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 := 0; i < 4; i++ { 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. // slot is 0-3 (four hardware watchpoints available); the slot must be free. 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) } // Determine the length encoding. 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") } // Write the watched address to DR0-DR3. var drAddr uintptr switch slot { case 0: drAddr = 0x0 // DR0 offset in user_regs_struct case 1: drAddr = 0x8 // DR1 case 2: drAddr = 0x10 // DR2 case 3: drAddr = 0x18 // DR3 } // PTRACE_POKEUSER writes to the debuggee's user area (includes debug regs). if err := ptracePokeUser(s.pid, drAddr, addr); err != nil { return fmt.Errorf("debug: set DR%d: %w", slot, err) } // Read the current DR7, set the enable and type bits, write it back. dr7, err := ptracePeekUser(s.pid, 0x38) // DR7 offset if err != nil { return fmt.Errorf("debug: read DR7: %w", err) } enableBit := uint64(1) << (2 * slot) // local enable rwBits := uint64(typ) << (16 + 4*slot) // R/W type lenField := lenBits << (18 + 4*slot) // length // Clear the existing bits for this slot, then set the new ones. mask := ^((uint64(1) << (2 * slot)) | (uint64(3) << (16 + 4*slot)) | (uint64(3) << (18 + 4*slot))) dr7 = (dr7 & mask) | enableBit | rwBits | lenField if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil { return fmt.Errorf("debug: set DR7: %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) } // Read DR7, clear the enable bit for this slot. dr7, err := ptracePeekUser(s.pid, 0x38) if err != nil { return err } dr7 &^= uint64(1) << (2 * slot) // disable if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil { return err } s.wpSlots[slot] = false return nil } // ClearAllWatchpoints removes all hardware watchpoints. func (s *Session) ClearAllWatchpoints() error { for slot := 0; slot < 4; slot++ { if s.wpSlots[slot] { if err := s.ClearWatchpoint(slot); err != nil { return err } } } return nil } // ptracePokeUser writes a value to the debuggee's user area at the given offset. func ptracePokeUser(pid int, offset uintptr, val uint64) error { const ptracePokeuser = 6 // PTRACE_POKEUSER _, _, errno := syscall.Syscall6( syscall.SYS_PTRACE, uintptr(ptracePokeuser), uintptr(pid), offset, uintptr(val), 0, 0, ) if errno != 0 { return errno } return nil } // ptracePeekUser reads a value from the debuggee's user area at the given offset. func ptracePeekUser(pid int, offset uintptr) (uint64, error) { const ptracePeekuser = 3 // PTRACE_PEEKUSER val, _, errno := syscall.Syscall6( syscall.SYS_PTRACE, uintptr(ptracePeekuser), uintptr(pid), offset, 0, 0, 0, ) if errno != 0 { return 0, errno } return uint64(val), nil }