2026-08-01 09:47:00 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-07 22:27:49 +02:00
|
|
|
// 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]
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 09:47:00 +02:00
|
|
|
// SetWatchpoint installs a hardware watchpoint on the given address.
|
2026-08-07 22:27:49 +02:00
|
|
|
// slot is 0-3 (four hardware watchpoints available); the slot must be free.
|
2026-08-01 09:47:00 +02:00
|
|
|
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")
|
|
|
|
|
}
|
2026-08-07 22:27:49 +02:00
|
|
|
if s.wpSlots[slot] {
|
|
|
|
|
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
|
|
|
|
}
|
2026-08-01 09:47:00 +02:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
2026-08-07 22:27:49 +02:00
|
|
|
s.wpSlots[slot] = true
|
2026-08-01 09:47:00 +02:00
|
|
|
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")
|
|
|
|
|
}
|
2026-08-07 22:27:49 +02:00
|
|
|
if !s.wpSlots[slot] {
|
|
|
|
|
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
|
|
|
|
}
|
2026-08-01 09:47:00 +02:00
|
|
|
// 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
|
2026-08-07 22:27:49 +02:00
|
|
|
if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
s.wpSlots[slot] = false
|
|
|
|
|
return nil
|
2026-08-01 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ClearAllWatchpoints removes all hardware watchpoints.
|
|
|
|
|
func (s *Session) ClearAllWatchpoints() error {
|
|
|
|
|
for slot := 0; slot < 4; slot++ {
|
2026-08-07 22:27:49 +02:00
|
|
|
if s.wpSlots[slot] {
|
|
|
|
|
if err := s.ClearWatchpoint(slot); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-08-01 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
}
|