// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause //go:build linux && riscv64 package debug import ( "fmt" "syscall" ) // Hardware watchpoint support for RISC-V via Sdtrig trigger registers. // Uses PTRACE_POKEUSER/PEEKUSER to access debug registers. // WatchpointType selects what triggers the watchpoint. type WatchpointType int const ( WatchWrite WatchpointType = 1 WatchRead WatchpointType = 3 ) // wpSlots tracks watchpoint slot occupancy. var wpSlots [4]bool const maxWatchpoints = 4 func (s *Session) FindFreeWatchpointSlot() int { for i := range maxWatchpoints { if !wpSlots[i] { return i } } return -1 } func (s *Session) IsWatchpointSlotUsed(slot int) bool { if slot < 0 || slot >= maxWatchpoints { return false } return wpSlots[slot] } // SetWatchpoint installs a hardware watchpoint. 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 wpSlots[slot] { return fmt.Errorf("debug: watchpoint slot %d already in use", slot) } if size != 1 && size != 2 && size != 4 && size != 8 { return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8") } // RISC-V trigger registers: tdata1 encodes type/control, tdata2 holds address. // The exact encoding depends on the trigger implementation (Sdtrig). // Use PTRACE_POKEUSER to write to the trigger CSRs via the kernel's // debug register interface. if err := ptracePokeUser(s.pid, uintptr(0x1000+slot*8), addr); err != nil { return fmt.Errorf("debug: set watchpoint address: %w", err) } // tdata1: set match control. Mode=2 (data match), select=0, action=1 (debug exception). var tdata1 uint64 = 2 << 60 // type = match (2) tdata1 |= 1 << 0 // action = enter debug mode tdata1 |= 1 << 7 // store (write) trigger if typ == WatchRead { tdata1 |= 1 << 6 // load trigger } // Size encoding: 0=1byte, 1=2byte, 2=4byte, 3=8byte. var sizeBits uint64 switch size { case 1: sizeBits = 0 case 2: sizeBits = 1 case 4: sizeBits = 2 case 8: sizeBits = 3 } tdata1 |= sizeBits << 16 // size field if err := ptracePokeUser(s.pid, uintptr(0x1001+slot*8), tdata1); err != nil { return fmt.Errorf("debug: set watchpoint control: %w", err) } wpSlots[slot] = true return nil } 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 !wpSlots[slot] { return fmt.Errorf("debug: watchpoint slot %d is not in use", slot) } // Disable by clearing tdata1. if err := ptracePokeUser(s.pid, uintptr(0x1001+slot*8), 0); err != nil { return err } wpSlots[slot] = false return nil } func (s *Session) ClearAllWatchpoints() error { for slot := 0; slot < maxWatchpoints; slot++ { if wpSlots[slot] { if err := s.ClearWatchpoint(slot); err != nil { return err } } } return nil } func ptracePokeUser(pid int, offset uintptr, val uint64) error { const ptracePokeuser = 6 _, _, errno := syscall.Syscall6( syscall.SYS_PTRACE, uintptr(ptracePokeuser), uintptr(pid), offset, uintptr(val), 0, 0, ) if errno != 0 { return errno } return nil } func ptracePeekUser(pid int, offset uintptr) (uint64, error) { const ptracePeekuser = 3 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 }