2026-08-20 23:57:10 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
//go:build linux && riscv64
|
|
|
|
|
|
|
|
|
|
package debug
|
|
|
|
|
|
2026-08-21 00:03:30 +02:00
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"syscall"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Hardware watchpoint support for RISC-V via Sdtrig trigger registers.
|
|
|
|
|
// Uses PTRACE_POKEUSER/PEEKUSER to access debug registers.
|
2026-08-20 23:57:10 +02:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
2026-08-21 00:03:30 +02:00
|
|
|
const maxWatchpoints = 4
|
|
|
|
|
|
2026-08-20 23:57:10 +02:00
|
|
|
func (s *Session) FindFreeWatchpointSlot() int {
|
2026-08-21 00:03:30 +02:00
|
|
|
for i := 0; i < maxWatchpoints; i++ {
|
2026-08-20 23:57:10 +02:00
|
|
|
if !wpSlots[i] {
|
|
|
|
|
return i
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
2026-08-21 00:03:30 +02:00
|
|
|
if slot < 0 || slot >= maxWatchpoints {
|
2026-08-20 23:57:10 +02:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return wpSlots[slot]
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-21 00:03:30 +02:00
|
|
|
// SetWatchpoint installs a hardware watchpoint.
|
2026-08-20 23:57:10 +02:00
|
|
|
func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error {
|
2026-08-21 00:03:30 +02:00
|
|
|
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
|
2026-08-20 23:57:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Session) ClearWatchpoint(slot int) error {
|
2026-08-21 00:03:30 +02:00
|
|
|
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
|
2026-08-20 23:57:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Session) ClearAllWatchpoints() error {
|
2026-08-21 00:03:30 +02:00
|
|
|
for slot := 0; slot < maxWatchpoints; slot++ {
|
|
|
|
|
if wpSlots[slot] {
|
|
|
|
|
if err := s.ClearWatchpoint(slot); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-20 23:57:10 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
2026-08-21 00:03:30 +02:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|