fix(debug): hardware watchpoints, signal stops and breakpoint restore
Assisted-by: GLM 5.3
This commit is contained in:
@@ -53,7 +53,7 @@ REPL commands:
|
||||
bufSpec := fs.String("buf", "", "buffer specification: name:size:pattern[,name:size:pattern...] where pattern is zero, ones, seq, or hex")
|
||||
script := fs.String("script", "", "run REPL commands from a file (one per line) and exit; '-' reads stdin")
|
||||
cover := fs.Bool("cover", false, "run to completion with a breakpoint on every instruction and report which executed and how often")
|
||||
timeout := fs.Duration("timeout", 0, "kill the debuggee after this duration (e.g. 30s); for headless --script runs")
|
||||
timeout := fs.Duration("timeout", 0, "kill the debuggee after this duration (e.g. 30s); for headless --script runs; a timeout exits 3")
|
||||
fs.Parse(args)
|
||||
|
||||
// --- Debuggee mode (internal, spawned by the debugger) ---
|
||||
@@ -231,6 +231,13 @@ REPL commands:
|
||||
if sess.Exited() {
|
||||
break
|
||||
}
|
||||
// A genuine signal-delivery-stop (a fault in the kernel): the
|
||||
// run cannot make progress, because resuming would restart the
|
||||
// faulting instruction and fault forever. Report and stop.
|
||||
if sig := sess.LastSignal(); sig != 0 {
|
||||
fmt.Printf("gasm debug: cover: stopped on signal %v\n", sig)
|
||||
break
|
||||
}
|
||||
regs, rerr := sess.GetRegs()
|
||||
if rerr != nil {
|
||||
break
|
||||
|
||||
+99
-44
@@ -9,11 +9,11 @@ import "strings"
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Breakpoint is one INT3 breakpoint in the debuggee.
|
||||
// Breakpoint is one software breakpoint in the debuggee.
|
||||
type Breakpoint struct {
|
||||
Addr uint64 // absolute address in the debuggee
|
||||
Label string // source label ("" for raw addresses)
|
||||
Orig byte // original byte at Addr (restored on removal)
|
||||
Orig []byte // original bytes at Addr (restored on removal)
|
||||
Enabled bool
|
||||
Cond *Condition // optional condition (nil = unconditional)
|
||||
hits int
|
||||
@@ -32,8 +32,12 @@ type Condition struct {
|
||||
MemAddr uint64 // memory address (for register-memory comparison, prefixed with *)
|
||||
}
|
||||
|
||||
// Eval checks the condition against the current registers.
|
||||
func (c *Condition) Eval(regs *Regs) bool {
|
||||
// Eval checks the condition against the current registers. For the
|
||||
// register-memory form, mem reads an 8-byte little-endian word from the
|
||||
// debuggee; it may be nil when no reader is available. Anything that cannot
|
||||
// be decided (unknown register or operator, unreadable memory) does not
|
||||
// block the breakpoint.
|
||||
func (c *Condition) Eval(regs *Regs, mem func(addr uint64) (uint64, bool)) bool {
|
||||
actual, ok := regs.RegValue(c.Reg)
|
||||
if !ok {
|
||||
return true // unknown register, don't block
|
||||
@@ -48,9 +52,16 @@ func (c *Condition) Eval(regs *Regs) bool {
|
||||
}
|
||||
expected = v
|
||||
case c.MemAddr != 0:
|
||||
// Register-memory comparison, requires a Session, not available here.
|
||||
// Fall back to treating as constant (the caller should resolve).
|
||||
expected = c.Value
|
||||
// Register-memory comparison, resolved in the debuggee at
|
||||
// evaluation time.
|
||||
if mem == nil {
|
||||
return true
|
||||
}
|
||||
v, ok := mem(c.MemAddr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
expected = v
|
||||
default:
|
||||
expected = c.Value
|
||||
}
|
||||
@@ -72,6 +83,18 @@ func (c *Condition) Eval(regs *Regs) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// String renders the condition for display.
|
||||
func (c *Condition) String() string {
|
||||
switch {
|
||||
case c.Reg2 != "":
|
||||
return fmt.Sprintf("%s %s %s", c.Reg, c.Op, c.Reg2)
|
||||
case c.MemAddr != 0:
|
||||
return fmt.Sprintf("%s %s *%#x", c.Reg, c.Op, c.MemAddr)
|
||||
default:
|
||||
return fmt.Sprintf("%s %s %#x", c.Reg, c.Op, c.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// Breakpoints manages the software breakpoints of one Session.
|
||||
type Breakpoints struct {
|
||||
t tracer
|
||||
@@ -83,6 +106,18 @@ func NewBreakpoints(t tracer) *Breakpoints {
|
||||
return &Breakpoints{t: t, bps: make(map[uint64]*Breakpoint)}
|
||||
}
|
||||
|
||||
// breakpointMask is the byte mask of the breakpoint instruction inside a
|
||||
// peeked word: the low len(breakpointInsn) bytes, because every supported
|
||||
// architecture is little-endian and patches the instruction at the lowest
|
||||
// address of the word.
|
||||
func breakpointMask() uint64 {
|
||||
var mask uint64
|
||||
for range breakpointInsn {
|
||||
mask = (mask << 8) | 0xFF
|
||||
}
|
||||
return mask
|
||||
}
|
||||
|
||||
// Set installs a breakpoint at addr (replaces any existing one).
|
||||
func (bm *Breakpoints) Set(addr uint64, label string) (*Breakpoint, error) {
|
||||
return bm.SetWithCond(addr, label, nil)
|
||||
@@ -100,13 +135,12 @@ func (bm *Breakpoints) SetWithCond(addr uint64, label string, cond *Condition) (
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orig := byte(word)
|
||||
// Patch with the breakpoint instruction, preserving the rest of the word.
|
||||
mask := uint64(0)
|
||||
for range breakpointInsn {
|
||||
mask = (mask << 8) | 0xFF
|
||||
orig := make([]byte, len(breakpointInsn))
|
||||
for i := range orig {
|
||||
orig[i] = byte(word >> (8 * i))
|
||||
}
|
||||
patched := (word &^ mask) | breakpointWord(breakpointInsn)
|
||||
// Patch with the breakpoint instruction, preserving the rest of the word.
|
||||
patched := (word &^ breakpointMask()) | breakpointWord(breakpointInsn)
|
||||
if err := bm.t.Poke(addr, patched); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -134,26 +168,40 @@ func (bm *Breakpoints) Info() string {
|
||||
}
|
||||
cond := ""
|
||||
if bp.Cond != nil {
|
||||
cond = fmt.Sprintf(" if %s %s %#x", bp.Cond.Reg, bp.Cond.Op, bp.Cond.Value)
|
||||
cond = " if " + bp.Cond.String()
|
||||
}
|
||||
result.WriteString(fmt.Sprintf(" %d: %s at %#x [%s, %d hits]%s\n", i, label, bp.Addr, status, bp.hits, cond))
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// Clear removes the breakpoint at addr, restoring the original byte.
|
||||
// restore writes the saved original bytes back over the breakpoint
|
||||
// instruction, preserving the rest of the peeked word. It reports whether
|
||||
// both the peek and the poke succeeded.
|
||||
func (bm *Breakpoints) restore(addr uint64, bp *Breakpoint) bool {
|
||||
word, err := bm.t.Peek(addr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
orig := uint64(0)
|
||||
for i, b := range bp.Orig {
|
||||
orig |= uint64(b) << (8 * i)
|
||||
}
|
||||
return bm.t.Poke(addr, (word&^breakpointMask())|orig) == nil
|
||||
}
|
||||
|
||||
// Clear removes the breakpoint at addr, restoring the original bytes.
|
||||
func (bm *Breakpoints) Clear(addr uint64) error {
|
||||
bp, ok := bm.bps[addr]
|
||||
if !ok {
|
||||
return fmt.Errorf("debug: no breakpoint at %#x", addr)
|
||||
}
|
||||
word, err := bm.t.Peek(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
||||
if err := bm.t.Poke(addr, restored); err != nil {
|
||||
return err
|
||||
if !bm.restore(addr, bp) {
|
||||
word, err := bm.t.Peek(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("debug: restore breakpoint at %#x failed, word is %#x", addr, word)
|
||||
}
|
||||
delete(bm.bps, addr)
|
||||
return nil
|
||||
@@ -185,43 +233,54 @@ func (bm *Breakpoints) All() []*Breakpoint {
|
||||
|
||||
// HandleTrap is called after the debuggee stops on SIGTRAP. It checks
|
||||
// whether the trap was caused by one of our breakpoints (PC-adjust matches
|
||||
// a breakpoint address), restores the original byte, rewinds PC, and
|
||||
// a breakpoint address), restores the original bytes, rewinds PC, and
|
||||
// returns the breakpoint that was hit (or nil if it was a single-step).
|
||||
// Hits returns how many times the breakpoint has been hit.
|
||||
func (bp *Breakpoint) Hits() int { return bp.hits }
|
||||
|
||||
func (bm *Breakpoints) HandleTrap(regs *Regs) *Breakpoint {
|
||||
// After a breakpoint trap, PC points past the breakpoint instruction.
|
||||
// On amd64 the kernel reports the trap with RIP past the INT3; on the
|
||||
// other supported architectures the PC still stands on the trap
|
||||
// instruction, which breakpointPCAdjust encodes per architecture.
|
||||
trapAddr := regs.GetPC() - uint64(breakpointPCAdjust)
|
||||
bp, ok := bm.bps[trapAddr]
|
||||
if !ok || !bp.Enabled {
|
||||
return nil // single-step trap or unknown
|
||||
}
|
||||
// Check the condition (if any).
|
||||
if bp.Cond != nil && !bp.Cond.Eval(regs) {
|
||||
// Condition not met, restore the byte but do NOT rewind RIP.
|
||||
// The process continues from the next instruction (past the INT3).
|
||||
word, err := bm.t.Peek(trapAddr)
|
||||
if err == nil {
|
||||
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
||||
bm.t.Poke(trapAddr, restored)
|
||||
if bp.Cond != nil && !bp.Cond.Eval(regs, bm.peekValue) {
|
||||
// Condition not met: step the original instruction and re-arm the
|
||||
// breakpoint, leaving the debuggee stopped just past it, ready to
|
||||
// resume silently. The PC must be rewound first: on architectures
|
||||
// that report the trap past the instruction (amd64) it would
|
||||
// otherwise sit on the second byte of the replaced instruction.
|
||||
if !bm.restore(trapAddr, bp) {
|
||||
return nil
|
||||
}
|
||||
// RIP is already past the INT3 (trapAddr + 1). Don't rewind.
|
||||
regs.SetPC(trapAddr)
|
||||
if err := bm.t.SetRegs(regs); err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := bm.t.Step(); err != nil {
|
||||
return nil
|
||||
}
|
||||
bm.Reinsert(trapAddr)
|
||||
return nil
|
||||
}
|
||||
bp.hits++
|
||||
// Restore the original byte.
|
||||
word, err := bm.t.Peek(trapAddr)
|
||||
if err == nil {
|
||||
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
||||
bm.t.Poke(trapAddr, restored)
|
||||
}
|
||||
// Rewind PC to re-execute the original instruction.
|
||||
// Restore the original bytes and rewind PC to re-execute them.
|
||||
bm.restore(trapAddr, bp)
|
||||
regs.SetPC(trapAddr)
|
||||
bm.t.SetRegs(regs)
|
||||
return bp
|
||||
}
|
||||
|
||||
// peekValue adapts tracer.Peek to the Condition value reader.
|
||||
func (bm *Breakpoints) peekValue(addr uint64) (uint64, bool) {
|
||||
v, err := bm.t.Peek(addr)
|
||||
return v, err == nil
|
||||
}
|
||||
|
||||
// Reinsert re-inserts the breakpoint at addr after a single-step past it.
|
||||
// Called after Step() when we want the breakpoint to fire again on the
|
||||
// next Continue().
|
||||
@@ -234,11 +293,7 @@ func (bm *Breakpoints) Reinsert(addr uint64) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mask := uint64(0)
|
||||
for range breakpointInsn {
|
||||
mask = (mask << 8) | 0xFF
|
||||
}
|
||||
patched := (word &^ mask) | breakpointWord(breakpointInsn)
|
||||
patched := (word &^ breakpointMask()) | breakpointWord(breakpointInsn)
|
||||
return bm.t.Poke(addr, patched)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package debug
|
||||
|
||||
// Architecture-neutral tests: label and line tables, and the breakpoint
|
||||
// manager against the mock tracer. These do not launch a debuggee, so they
|
||||
// build on every supported linux architecture.
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLineAt(t *testing.T) {
|
||||
lines := []SourceLine{
|
||||
{Offset: 0, Line: 5},
|
||||
{Offset: 5, Line: 6},
|
||||
{Offset: 10, Line: 7},
|
||||
{Offset: 15, Line: 8},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
offset int
|
||||
want int
|
||||
}{
|
||||
{0, 5},
|
||||
{1, 5},
|
||||
{4, 5},
|
||||
{5, 6},
|
||||
{7, 6},
|
||||
{10, 7},
|
||||
{12, 7},
|
||||
{15, 8},
|
||||
{20, 8},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := lineAt(lines, tt.offset)
|
||||
if got != tt.want {
|
||||
t.Errorf("lineAt(lines, %d) = %d, want %d", tt.offset, got, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
// Empty table.
|
||||
if lineAt(nil, 5) != 0 {
|
||||
t.Error("lineAt(nil, 5) should return 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOffsetForLine(t *testing.T) {
|
||||
lines := []SourceLine{
|
||||
{Offset: 0, Line: 5},
|
||||
{Offset: 5, Line: 6},
|
||||
{Offset: 10, Line: 7},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
line int
|
||||
want int
|
||||
}{
|
||||
{5, 0},
|
||||
{6, 5},
|
||||
{7, 10},
|
||||
{99, -1}, // not found
|
||||
{0, -1}, // not found
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := offsetForLine(lines, tt.line)
|
||||
if got != tt.want {
|
||||
t.Errorf("offsetForLine(lines, %d) = %d, want %d", tt.line, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNearestLabel(t *testing.T) {
|
||||
labels := []Label{
|
||||
{Name: "start", Offset: 0},
|
||||
{Name: "loop", Offset: 10},
|
||||
{Name: "done", Offset: 20},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
offset int
|
||||
want string
|
||||
}{
|
||||
{0, "start"},
|
||||
{5, "start"},
|
||||
{10, "loop"},
|
||||
{15, "loop"},
|
||||
{20, "done"},
|
||||
{25, "done"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := nearestLabel(labels, tt.offset)
|
||||
if got != tt.want {
|
||||
t.Errorf("nearestLabel(labels, %d) = %q, want %q", tt.offset, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakpointsSetAndClear(t *testing.T) {
|
||||
tr := newMockTracer()
|
||||
bm := NewBreakpoints(tr)
|
||||
|
||||
// Set a breakpoint at address 0x1000.
|
||||
bp, err := bm.Set(0x1000, "test")
|
||||
if err != nil {
|
||||
t.Fatalf("Set: %v", err)
|
||||
}
|
||||
if !bp.Enabled {
|
||||
t.Error("breakpoint not enabled")
|
||||
}
|
||||
if bp.Label != "test" {
|
||||
t.Errorf("label = %q, want test", bp.Label)
|
||||
}
|
||||
|
||||
// Verify Peek was called.
|
||||
if len(tr.peeks) != 1 || tr.peeks[0] != 0x1000 {
|
||||
t.Errorf("peeks = %v, want [0x1000]", tr.peeks)
|
||||
}
|
||||
|
||||
// Verify Poke wrote the breakpoint instruction's bytes.
|
||||
if len(tr.pokes) != 1 || tr.pokes[0].addr != 0x1000 {
|
||||
t.Errorf("pokes = %v", tr.pokes)
|
||||
}
|
||||
if got := tr.pokes[0].val & breakpointMask(); got != breakpointWord(breakpointInsn) {
|
||||
t.Errorf("patched bytes %#x, want %#x", got, breakpointWord(breakpointInsn))
|
||||
}
|
||||
|
||||
// At should find it.
|
||||
if bm.At(0x1000) == nil {
|
||||
t.Error("At(0x1000) returned nil")
|
||||
}
|
||||
|
||||
// All should return it.
|
||||
all := bm.All()
|
||||
if len(all) != 1 {
|
||||
t.Errorf("All() = %d breakpoints, want 1", len(all))
|
||||
}
|
||||
|
||||
// Clear it.
|
||||
if err := bm.Clear(0x1000); err != nil {
|
||||
t.Fatalf("Clear: %v", err)
|
||||
}
|
||||
if bm.At(0x1000) != nil {
|
||||
t.Error("At(0x1000) after Clear should be nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBreakpointRestoreWidth proves the restore path writes back every
|
||||
// byte of the breakpoint instruction's width, not just the first byte: on
|
||||
// arm64, riscv64 and loong64 the instruction is four bytes, and restoring
|
||||
// one byte would leave three bytes of the trap instruction in place.
|
||||
func TestBreakpointRestoreWidth(t *testing.T) {
|
||||
tr := newMockTracer()
|
||||
bm := NewBreakpoints(tr)
|
||||
tr.mem[0x3000] = 0x11
|
||||
tr.mem[0x3001] = 0x22
|
||||
tr.mem[0x3002] = 0x33
|
||||
tr.mem[0x3003] = 0x44
|
||||
|
||||
if _, err := bm.Set(0x3000, "width"); err != nil {
|
||||
t.Fatalf("Set: %v", err)
|
||||
}
|
||||
for i, b := range breakpointInsn {
|
||||
if tr.mem[0x3000+uint64(i)] != b {
|
||||
t.Fatalf("byte %d after Set = %#x, want the breakpoint byte %#x", i, tr.mem[0x3000+uint64(i)], b)
|
||||
}
|
||||
}
|
||||
if len(bm.At(0x3000).Orig) != len(breakpointInsn) {
|
||||
t.Fatalf("Orig holds %d bytes, want %d", len(bm.At(0x3000).Orig), len(breakpointInsn))
|
||||
}
|
||||
|
||||
if err := bm.Clear(0x3000); err != nil {
|
||||
t.Fatalf("Clear: %v", err)
|
||||
}
|
||||
want := []byte{0x11, 0x22, 0x33, 0x44}
|
||||
for i, b := range want {
|
||||
if tr.mem[0x3000+uint64(i)] != b {
|
||||
t.Errorf("byte %d after Clear = %#x, want %#x (restore must cover the full instruction width)", i, tr.mem[0x3000+uint64(i)], b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakpointsSetWithCond(t *testing.T) {
|
||||
tr := newMockTracer()
|
||||
bm := NewBreakpoints(tr)
|
||||
|
||||
cond := &Condition{Reg: "rax", Op: "==", Value: 42}
|
||||
bp, err := bm.SetWithCond(0x2000, "cond_test", cond)
|
||||
if err != nil {
|
||||
t.Fatalf("SetWithCond: %v", err)
|
||||
}
|
||||
if bp.Cond == nil || bp.Cond.Value != 42 {
|
||||
t.Error("condition not set")
|
||||
}
|
||||
|
||||
// Re-setting the same address should update the condition.
|
||||
cond2 := &Condition{Reg: "rbx", Op: "<", Value: 100}
|
||||
bp2, err := bm.SetWithCond(0x2000, "cond_test2", cond2)
|
||||
if err != nil {
|
||||
t.Fatalf("SetWithCond (update): %v", err)
|
||||
}
|
||||
if bp2.Cond.Value != 100 {
|
||||
t.Error("condition not updated")
|
||||
}
|
||||
// Should have only 1 Peek (first Set), second is update (no Peek needed).
|
||||
if len(tr.peeks) != 1 {
|
||||
t.Errorf("expected 1 Peek, got %d", len(tr.peeks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakpointsClearAll(t *testing.T) {
|
||||
tr := newMockTracer()
|
||||
bm := NewBreakpoints(tr)
|
||||
|
||||
bm.Set(0x1000, "a")
|
||||
bm.Set(0x2000, "b")
|
||||
bm.Set(0x3000, "c")
|
||||
|
||||
if len(bm.All()) != 3 {
|
||||
t.Fatalf("expected 3 breakpoints, got %d", len(bm.All()))
|
||||
}
|
||||
|
||||
bm.ClearAll()
|
||||
if len(bm.All()) != 0 {
|
||||
t.Errorf("ClearAll: expected 0 breakpoints, got %d", len(bm.All()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakpointInfo(t *testing.T) {
|
||||
tr := newMockTracer()
|
||||
bm := NewBreakpoints(tr)
|
||||
bm.Set(0x4000, "info_test")
|
||||
|
||||
info := bm.Info()
|
||||
if info == "" {
|
||||
t.Error("Info returned empty string")
|
||||
}
|
||||
if !strings.Contains(info, "info_test") {
|
||||
t.Errorf("Info %q does not contain label", info)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConditionString covers the display of all three condition forms.
|
||||
func TestConditionString(t *testing.T) {
|
||||
tests := []struct {
|
||||
cond Condition
|
||||
want string
|
||||
}{
|
||||
{Condition{Reg: "rax", Op: "==", Value: 42}, "rax == 0x2a"},
|
||||
{Condition{Reg: "rax", Op: "!=", Reg2: "rbx"}, "rax != rbx"},
|
||||
{Condition{Reg: "rax", Op: "<", MemAddr: 0x5000}, "rax < *0x5000"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := tt.cond.String(); got != tt.want {
|
||||
t.Errorf("Condition.String() = %q, want %q", got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
-188
@@ -6,7 +6,6 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -48,7 +47,7 @@ func TestConditionEval(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := tt.cond.Eval(regs)
|
||||
got := tt.cond.Eval(regs, nil)
|
||||
if got != tt.want {
|
||||
t.Errorf("Condition{%q %q %d}.Eval() = %v, want %v",
|
||||
tt.cond.Reg, tt.cond.Op, tt.cond.Value, got, tt.want)
|
||||
@@ -56,65 +55,33 @@ func TestConditionEval(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLineAt(t *testing.T) {
|
||||
lines := []SourceLine{
|
||||
{Offset: 0, Line: 5},
|
||||
{Offset: 5, Line: 6},
|
||||
{Offset: 10, Line: 7},
|
||||
{Offset: 15, Line: 8},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
offset int
|
||||
want int
|
||||
}{
|
||||
{0, 5},
|
||||
{1, 5},
|
||||
{4, 5},
|
||||
{5, 6},
|
||||
{7, 6},
|
||||
{10, 7},
|
||||
{12, 7},
|
||||
{15, 8},
|
||||
{20, 8},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := lineAt(lines, tt.offset)
|
||||
if got != tt.want {
|
||||
t.Errorf("lineAt(lines, %d) = %d, want %d", tt.offset, got, tt.want)
|
||||
// TestConditionEvalMem covers the register-memory form: the value is read
|
||||
// through the supplied reader, and a missing or failing reader must not
|
||||
// block the breakpoint.
|
||||
func TestConditionEvalMem(t *testing.T) {
|
||||
regs := &Regs{RAX: 7}
|
||||
mem := func(addr uint64) (uint64, bool) {
|
||||
if addr == 0x5000 {
|
||||
return 7, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// Empty table.
|
||||
if lineAt(nil, 5) != 0 {
|
||||
t.Error("lineAt(nil, 5) should return 0")
|
||||
eq := Condition{Reg: "rax", Op: "==", MemAddr: 0x5000}
|
||||
if !eq.Eval(regs, mem) {
|
||||
t.Error("register-memory comparison with matching word should hold")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOffsetForLine(t *testing.T) {
|
||||
lines := []SourceLine{
|
||||
{Offset: 0, Line: 5},
|
||||
{Offset: 5, Line: 6},
|
||||
{Offset: 10, Line: 7},
|
||||
ne := Condition{Reg: "rax", Op: "!=", MemAddr: 0x5000}
|
||||
if ne.Eval(regs, mem) {
|
||||
t.Error("register-memory comparison with mismatching word should not hold")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
line int
|
||||
want int
|
||||
}{
|
||||
{5, 0},
|
||||
{6, 5},
|
||||
{7, 10},
|
||||
{99, -1}, // not found
|
||||
{0, -1}, // not found
|
||||
bad := Condition{Reg: "rax", Op: "==", MemAddr: 0x6000}
|
||||
if !bad.Eval(regs, mem) {
|
||||
t.Error("unreadable memory must not block the breakpoint")
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := offsetForLine(lines, tt.line)
|
||||
if got != tt.want {
|
||||
t.Errorf("offsetForLine(lines, %d) = %d, want %d", tt.line, got, tt.want)
|
||||
}
|
||||
noReader := Condition{Reg: "rax", Op: "==", MemAddr: 0x5000}
|
||||
if !noReader.Eval(regs, nil) {
|
||||
t.Error("missing memory reader must not block the breakpoint")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,139 +108,6 @@ func TestDecodeRflags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNearestLabel(t *testing.T) {
|
||||
labels := []Label{
|
||||
{Name: "start", Offset: 0},
|
||||
{Name: "loop", Offset: 10},
|
||||
{Name: "done", Offset: 20},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
offset int
|
||||
want string
|
||||
}{
|
||||
{0, "start"},
|
||||
{5, "start"},
|
||||
{10, "loop"},
|
||||
{15, "loop"},
|
||||
{20, "done"},
|
||||
{25, "done"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := nearestLabel(labels, tt.offset)
|
||||
if got != tt.want {
|
||||
t.Errorf("nearestLabel(labels, %d) = %q, want %q", tt.offset, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakpointsSetAndClear(t *testing.T) {
|
||||
tr := newMockTracer()
|
||||
bm := NewBreakpoints(tr)
|
||||
|
||||
// Set a breakpoint at address 0x1000.
|
||||
bp, err := bm.Set(0x1000, "test")
|
||||
if err != nil {
|
||||
t.Fatalf("Set: %v", err)
|
||||
}
|
||||
if !bp.Enabled {
|
||||
t.Error("breakpoint not enabled")
|
||||
}
|
||||
if bp.Label != "test" {
|
||||
t.Errorf("label = %q, want test", bp.Label)
|
||||
}
|
||||
|
||||
// Verify Peek was called.
|
||||
if len(tr.peeks) != 1 || tr.peeks[0] != 0x1000 {
|
||||
t.Errorf("peeks = %v, want [0x1000]", tr.peeks)
|
||||
}
|
||||
|
||||
// Verify Poke wrote INT3.
|
||||
if len(tr.pokes) != 1 || tr.pokes[0].addr != 0x1000 {
|
||||
t.Errorf("pokes = %v", tr.pokes)
|
||||
}
|
||||
|
||||
// At should find it.
|
||||
if bm.At(0x1000) == nil {
|
||||
t.Error("At(0x1000) returned nil")
|
||||
}
|
||||
|
||||
// All should return it.
|
||||
all := bm.All()
|
||||
if len(all) != 1 {
|
||||
t.Errorf("All() = %d breakpoints, want 1", len(all))
|
||||
}
|
||||
|
||||
// Clear it.
|
||||
if err := bm.Clear(0x1000); err != nil {
|
||||
t.Fatalf("Clear: %v", err)
|
||||
}
|
||||
if bm.At(0x1000) != nil {
|
||||
t.Error("At(0x1000) after Clear should be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakpointsSetWithCond(t *testing.T) {
|
||||
tr := newMockTracer()
|
||||
bm := NewBreakpoints(tr)
|
||||
|
||||
cond := &Condition{Reg: "rax", Op: "==", Value: 42}
|
||||
bp, err := bm.SetWithCond(0x2000, "cond_test", cond)
|
||||
if err != nil {
|
||||
t.Fatalf("SetWithCond: %v", err)
|
||||
}
|
||||
if bp.Cond == nil || bp.Cond.Value != 42 {
|
||||
t.Error("condition not set")
|
||||
}
|
||||
|
||||
// Re-setting the same address should update the condition.
|
||||
cond2 := &Condition{Reg: "rbx", Op: "<", Value: 100}
|
||||
bp2, err := bm.SetWithCond(0x2000, "cond_test2", cond2)
|
||||
if err != nil {
|
||||
t.Fatalf("SetWithCond (update): %v", err)
|
||||
}
|
||||
if bp2.Cond.Value != 100 {
|
||||
t.Error("condition not updated")
|
||||
}
|
||||
// Should have only 1 Peek (first Set), second is update (no Peek needed).
|
||||
if len(tr.peeks) != 1 {
|
||||
t.Errorf("expected 1 Peek, got %d", len(tr.peeks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakpointsClearAll(t *testing.T) {
|
||||
tr := newMockTracer()
|
||||
bm := NewBreakpoints(tr)
|
||||
|
||||
bm.Set(0x1000, "a")
|
||||
bm.Set(0x2000, "b")
|
||||
bm.Set(0x3000, "c")
|
||||
|
||||
if len(bm.All()) != 3 {
|
||||
t.Fatalf("expected 3 breakpoints, got %d", len(bm.All()))
|
||||
}
|
||||
|
||||
bm.ClearAll()
|
||||
if len(bm.All()) != 0 {
|
||||
t.Errorf("ClearAll: expected 0 breakpoints, got %d", len(bm.All()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakpointInfo(t *testing.T) {
|
||||
tr := newMockTracer()
|
||||
bm := NewBreakpoints(tr)
|
||||
bm.Set(0x4000, "info_test")
|
||||
|
||||
info := bm.Info()
|
||||
if info == "" {
|
||||
t.Error("Info returned empty string")
|
||||
}
|
||||
if !strings.Contains(info, "info_test") {
|
||||
t.Errorf("Info %q does not contain label", info)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchpointSlotTracking(t *testing.T) {
|
||||
s := &Session{} // per-session slots start free
|
||||
|
||||
@@ -323,3 +157,19 @@ func TestWatchpointSlotTracking(t *testing.T) {
|
||||
t.Errorf("FindFreeWatchpointSlot() with all slots used = %d, want -1", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnwatchSlotBound checks the bound the REPL parses against: it must
|
||||
// cover the architecture's whole slot range, not a hardcoded 0-3.
|
||||
func TestUnwatchSlotBound(t *testing.T) {
|
||||
max := maxWatchpoints()
|
||||
if max < 4 {
|
||||
t.Fatalf("maxWatchpoints() = %d, want at least 4", max)
|
||||
}
|
||||
s := &Session{}
|
||||
if s.IsWatchpointSlotUsed(max - 1) {
|
||||
t.Errorf("slot %d should be free initially", max-1)
|
||||
}
|
||||
if s.IsWatchpointSlotUsed(max) {
|
||||
t.Errorf("slot %d must be out of range", max)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,3 +46,11 @@ func (s *Session) DisassembleN(addr uint64, n int) string {
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// isCallInsn reports whether disassembled text (x86asm.IntelSyntax) is a
|
||||
// call. The first token must match exactly: a prefix test would also catch
|
||||
// unrelated mnemonics.
|
||||
func isCallInsn(text string) bool {
|
||||
m, _, _ := strings.Cut(text, " ")
|
||||
return strings.ToLower(m) == "call"
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
||||
@@ -44,3 +45,15 @@ func (s *Session) DisassembleN(addr uint64, n int) string {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// isCallInsn reports whether disassembled text (arm64asm.GoSyntax) is a
|
||||
// call. GoSyntax renders bl as CALL; the native mnemonic is accepted too.
|
||||
// The first token must match exactly so branches never match.
|
||||
func isCallInsn(text string) bool {
|
||||
m, _, _ := strings.Cut(text, " ")
|
||||
switch strings.ToLower(m) {
|
||||
case "call", "bl":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
||||
@@ -44,3 +45,16 @@ func (s *Session) DisassembleN(addr uint64, n int) string {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// isCallInsn reports whether disassembled text (loong64asm.GoSyntax) is a
|
||||
// call. GoSyntax renders bl and jirl calls as CALL (jirl returns print
|
||||
// RET); the native mnemonics are accepted too. The first token must match
|
||||
// exactly: a "bl" prefix would catch bltz and other branches.
|
||||
func isCallInsn(text string) bool {
|
||||
m, _, _ := strings.Cut(text, " ")
|
||||
switch strings.ToLower(m) {
|
||||
case "call", "bl", "jirl":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
||||
@@ -44,3 +45,17 @@ func (s *Session) DisassembleN(addr uint64, n int) string {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// isCallInsn reports whether disassembled text (riscv64asm.GoSyntax) is a
|
||||
// call. GoSyntax renders jal and jalr calls as CALL; the native mnemonics
|
||||
// are accepted too. The first token must match exactly: a prefix test on
|
||||
// "bl" would catch branches on other architectures, and jalr as ret prints
|
||||
// RET, which must not be stepped over.
|
||||
func isCallInsn(text string) bool {
|
||||
m, _, _ := strings.Cut(text, " ")
|
||||
switch strings.ToLower(m) {
|
||||
case "call", "jal", "jalr":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -73,9 +73,29 @@ func decodeRflags(f uint64) string {
|
||||
return flags[:len(flags)-1]
|
||||
}
|
||||
|
||||
// archReturnAddr reads the return address from the stack (amd64 ABI0 convention).
|
||||
// archReturnAddr reads the return address of the current frame (amd64
|
||||
// ABI0 convention). A function that contains a CALL (or has a frame) is
|
||||
// assembled with the prologue PUSHQ BP; MOVQ SP, BP, so mid-function the
|
||||
// word at SP is the saved caller BP, a stack address, and the return
|
||||
// address sits further up. Walk the stack from SP and take the first word
|
||||
// that lies in an executable mapping: stack and data words never do, a
|
||||
// return address always does.
|
||||
func archReturnAddr(s *Session, regs *Regs) (uint64, error) {
|
||||
return s.Peek(regs.GetSP())
|
||||
ranges := execRanges(s.pid)
|
||||
for off := uint64(0); off < 512; off += 8 {
|
||||
word, err := s.Peek(regs.RSP + off)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
for _, r := range ranges {
|
||||
if word >= r.lo && word < r.hi {
|
||||
return word, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
// No mapping available or nothing code-like on the stack: fall back to
|
||||
// the raw entry convention, [SP] before any push.
|
||||
return s.Peek(regs.RSP)
|
||||
}
|
||||
|
||||
// archSPLabel returns the SP register name for display.
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
|
||||
package debug
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func printRegs(regs *Regs, codeBase, funcOff uint64) {
|
||||
fmt.Printf(" PC = %#016x (func+%#x)\n", regs.PC, regs.PC-codeBase-funcOff)
|
||||
@@ -31,8 +34,8 @@ func printRegs(regs *Regs, codeBase, funcOff uint64) {
|
||||
func printVectorRegs(v *VectorRegs) {
|
||||
fmt.Println("\n Vector registers (V0-V31):")
|
||||
for i := 0; i < 32; i += 2 {
|
||||
fmt.Printf(" V%-2d = %016x%016x\n", i, v.V[i][8], v.V[i][0])
|
||||
fmt.Printf(" V%-2d = %016x%016x\n", i+1, v.V[i+1][8], v.V[i+1][0])
|
||||
fmt.Printf(" V%-2d = %016x%016x\n", i, binary.LittleEndian.Uint64(v.V[i][8:16]), binary.LittleEndian.Uint64(v.V[i][0:8]))
|
||||
fmt.Printf(" V%-2d = %016x%016x\n", i+1, binary.LittleEndian.Uint64(v.V[i+1][8:16]), binary.LittleEndian.Uint64(v.V[i+1][0:8]))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||
)
|
||||
|
||||
// Integration tests beyond the basic entry breakpoint: hardware watchpoints,
|
||||
// conditional breakpoints, next/finish over a CALL, faulting kernels and the
|
||||
// xstate vector-register readout. All drive a real ptrace session, so they
|
||||
// run on amd64 hosts only.
|
||||
|
||||
// writeKernel writes an assembly source to a temporary file with the
|
||||
// architecture suffix the assembler dispatcher expects.
|
||||
func writeKernel(t *testing.T, src string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "kernel_amd64.s")
|
||||
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
|
||||
t.Fatalf("write kernel: %v", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// launchKernel launches a session for the kernel source and returns the
|
||||
// session, its breakpoint manager and the function layout.
|
||||
func launchKernel(t *testing.T, bin, path, funcName string, args []byte) (*Session, *Breakpoints, asm.FuncLayout) {
|
||||
t.Helper()
|
||||
k, err := verify.Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
t.Cleanup(k.Close)
|
||||
fl, err := k.Func(funcName)
|
||||
if err != nil {
|
||||
t.Fatalf("Func: %v", err)
|
||||
}
|
||||
if len(args) < fl.Args {
|
||||
padded := make([]byte, fl.Args)
|
||||
copy(padded, args)
|
||||
args = padded
|
||||
}
|
||||
sess, err := Launch(bin, path, funcName, args)
|
||||
if err != nil {
|
||||
t.Fatalf("Launch: %v", err)
|
||||
}
|
||||
t.Cleanup(sess.Kill)
|
||||
bm := NewBreakpoints(sess)
|
||||
return sess, bm, fl
|
||||
}
|
||||
|
||||
// runToEntry resumes the freshly launched debuggee until the breakpoint at
|
||||
// the function entry traps, mirroring the REPL continue loop: the debuggee
|
||||
// SIGSTOPs twice (launch barrier and entry barrier) before entering the JIT
|
||||
// call.
|
||||
func runToEntry(t *testing.T, sess *Session, bm *Breakpoints, entry uint64) {
|
||||
t.Helper()
|
||||
for range 50 {
|
||||
for _, bp := range bm.All() {
|
||||
bm.Reinsert(bp.Addr)
|
||||
}
|
||||
if err := sess.Continue(); err != nil {
|
||||
t.Fatalf("Continue: %v", err)
|
||||
}
|
||||
if sess.Exited() {
|
||||
t.Fatal("debuggee exited before the entry breakpoint trapped")
|
||||
}
|
||||
regs, err := sess.GetRegs()
|
||||
if err != nil {
|
||||
t.Fatalf("GetRegs: %v", err)
|
||||
}
|
||||
if bm.HandleTrap(®s) != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("no entry breakpoint trap after 50 resumes")
|
||||
}
|
||||
|
||||
// captureStdout runs fn with os.Stdout redirected to a pipe and returns
|
||||
// what it printed (the REPL writes its reports to stdout).
|
||||
func captureStdout(t *testing.T, fn func()) string {
|
||||
t.Helper()
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("pipe: %v", err)
|
||||
}
|
||||
old := os.Stdout
|
||||
os.Stdout = w
|
||||
done := make(chan string, 1)
|
||||
go func() {
|
||||
b, _ := io.ReadAll(r)
|
||||
done <- string(b)
|
||||
}()
|
||||
defer func() { os.Stdout = old }()
|
||||
fn()
|
||||
w.Close()
|
||||
return <-done
|
||||
}
|
||||
|
||||
// TestWatchpointArmRunHit proves the debug-register offsets: the watchpoint
|
||||
// must fire on the store, with si_addr naming the watched address. The
|
||||
// kernel writes its return value to ret+0(FP), which is the 8-byte word
|
||||
// right above the stack pointer at entry.
|
||||
func TestWatchpointArmRunHit(t *testing.T) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
bin := buildGasm(t)
|
||||
|
||||
const kernel = `#include "textflag.h"
|
||||
|
||||
// func wpret() int64
|
||||
TEXT ·wpret(SB), NOSPLIT, $0-8
|
||||
MOVQ $0x5a5a5a5a5a5a5a5a, AX
|
||||
MOVQ AX, ret+0(FP)
|
||||
RET
|
||||
`
|
||||
path := writeKernel(t, kernel)
|
||||
sess, bm, fl := launchKernel(t, bin, path, "wpret", nil)
|
||||
|
||||
entry := sess.CodeBase() + uint64(fl.Offset)
|
||||
if _, err := bm.Set(entry, "entry"); err != nil {
|
||||
t.Fatalf("Set: %v", err)
|
||||
}
|
||||
runToEntry(t, sess, bm, entry)
|
||||
|
||||
regs, err := sess.GetRegs()
|
||||
if err != nil {
|
||||
t.Fatalf("GetRegs: %v", err)
|
||||
}
|
||||
watched := regs.RSP + 8 // ret+0(FP): the store target
|
||||
|
||||
slot := sess.FindFreeWatchpointSlot()
|
||||
if slot < 0 {
|
||||
t.Fatal("no free watchpoint slot")
|
||||
}
|
||||
if err := sess.SetWatchpoint(slot, watched, WatchWrite, 8); err != nil {
|
||||
t.Fatalf("SetWatchpoint: %v (wrong debug-register offsets?)", err)
|
||||
}
|
||||
|
||||
if err := sess.Continue(); err != nil {
|
||||
t.Fatalf("Continue: %v", err)
|
||||
}
|
||||
reason, addr := sess.StopInfo()
|
||||
if reason != StopWatchpoint {
|
||||
t.Fatalf("stop reason = %v, want StopWatchpoint (DR0-DR3/DR7 offsets are wrong)", reason)
|
||||
}
|
||||
if addr != watched {
|
||||
t.Fatalf("watchpoint address = %#x, want %#x", addr, watched)
|
||||
}
|
||||
|
||||
// The watched word holds the stored value: x86 data breakpoints are
|
||||
// reported with the access complete.
|
||||
if word, err := sess.Peek(watched); err != nil || word != 0x5a5a5a5a5a5a5a5a {
|
||||
t.Errorf("watched word = %#x (err %v), want 0x5a5a5a5a5a5a5a5a", word, err)
|
||||
}
|
||||
if err := sess.ClearWatchpoint(slot); err != nil {
|
||||
t.Fatalf("ClearWatchpoint: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConditionalBreakpointFalseThenTrue proves the false-condition path:
|
||||
// the breakpoint steps over the original instruction, re-arms itself and
|
||||
// keeps running silently, and the true condition stops exactly once with the
|
||||
// register in the expected state.
|
||||
func TestConditionalBreakpointFalseThenTrue(t *testing.T) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
bin := buildGasm(t)
|
||||
|
||||
const kernel = `#include "textflag.h"
|
||||
|
||||
// func countdown(n int64) int64
|
||||
TEXT ·countdown(SB), NOSPLIT, $0-16
|
||||
MOVQ n+0(FP), CX
|
||||
loop:
|
||||
DECQ CX
|
||||
CMPQ CX, $0
|
||||
JNE loop
|
||||
MOVQ CX, ret+8(FP)
|
||||
RET
|
||||
`
|
||||
path := writeKernel(t, kernel)
|
||||
sess, bm, fl := launchKernel(t, bin, path, "countdown", []byte{8})
|
||||
|
||||
loopAddr := sess.CodeBase() + uint64(fl.Offset) + uint64(fl.Labels["loop"])
|
||||
// The length of the breakpointed instruction, from a disassembly taken
|
||||
// before the INT3 is patched in.
|
||||
_, insnLen, err := sess.Disassemble(loopAddr)
|
||||
if err != nil || insnLen <= 0 {
|
||||
t.Fatalf("Disassemble at %#x: len=%d err=%v", loopAddr, insnLen, err)
|
||||
}
|
||||
cond := &Condition{Reg: "rcx", Op: "==", Value: 1}
|
||||
bp, err := bm.SetWithCond(loopAddr, "loop", cond)
|
||||
if err != nil {
|
||||
t.Fatalf("SetWithCond: %v", err)
|
||||
}
|
||||
|
||||
hits := 0
|
||||
exited := false
|
||||
for range 200 {
|
||||
for _, b := range bm.All() {
|
||||
bm.Reinsert(b.Addr)
|
||||
}
|
||||
if err := sess.Continue(); err != nil {
|
||||
exited = true
|
||||
break // the debuggee finished
|
||||
}
|
||||
if sess.Exited() {
|
||||
exited = true
|
||||
break
|
||||
}
|
||||
if sig := sess.LastSignal(); sig != 0 {
|
||||
t.Fatalf("unexpected signal stop %v", sig)
|
||||
}
|
||||
regs, err := sess.GetRegs()
|
||||
if err != nil {
|
||||
t.Fatalf("GetRegs: %v", err)
|
||||
}
|
||||
if hit := bm.HandleTrap(®s); hit != nil {
|
||||
hits++
|
||||
if regs.RCX != 1 {
|
||||
t.Fatalf("hit with RCX=%d, want 1", regs.RCX)
|
||||
}
|
||||
// Park after the instruction, as the REPL does.
|
||||
if err := sess.Step(); err != nil {
|
||||
t.Fatalf("Step: %v", err)
|
||||
}
|
||||
} else {
|
||||
// A false evaluation must leave the debuggee past the whole
|
||||
// original instruction: a PC inside it (trapAddr+1 on amd64)
|
||||
// means the resume happens mid-instruction.
|
||||
fresh, err := sess.GetRegs()
|
||||
if err != nil {
|
||||
t.Fatalf("GetRegs: %v", err)
|
||||
}
|
||||
if fresh.RIP > loopAddr && fresh.RIP < loopAddr+uint64(insnLen) {
|
||||
t.Fatalf("false evaluation left the PC at %#x, inside the %d-byte instruction at %#x",
|
||||
fresh.RIP, insnLen, loopAddr)
|
||||
}
|
||||
}
|
||||
}
|
||||
if hits != 1 {
|
||||
t.Fatalf("conditional breakpoint hit %d times, want exactly 1 (false evaluations must run through silently)", hits)
|
||||
}
|
||||
if bp.Hits() != 1 {
|
||||
t.Errorf("bp.Hits() = %d, want 1", bp.Hits())
|
||||
}
|
||||
if !exited || !sess.Exited() {
|
||||
t.Fatal("debuggee did not run to completion after the conditional hit")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNextAndFinishOverCall proves next and finish evaluate the trap with
|
||||
// registers fetched after the stop: next lands exactly on the instruction
|
||||
// after the CALL, and finish stops exactly on the return address.
|
||||
func TestNextAndFinishOverCall(t *testing.T) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
bin := buildGasm(t)
|
||||
|
||||
const kernel = `#include "textflag.h"
|
||||
|
||||
// func caller(x int64) int64
|
||||
// The argument travels in AX: FP argument slots of CALL-bearing functions
|
||||
// are an assembler concern outside this test's scope.
|
||||
TEXT ·caller(SB), NOSPLIT, $0-16
|
||||
MOVQ $5, AX
|
||||
CALL ·bump(SB)
|
||||
aftercall:
|
||||
MOVQ AX, ret+8(FP)
|
||||
RET
|
||||
|
||||
// func bump(x int64) int64
|
||||
TEXT ·bump(SB), NOSPLIT, $0-0
|
||||
ADDQ $3, AX
|
||||
RET
|
||||
`
|
||||
path := writeKernel(t, kernel)
|
||||
|
||||
// next: step the prologue and the constant load (3 instructions), then
|
||||
// step over the CALL and check the landing address and RAX.
|
||||
sess, bm, fl := launchKernel(t, bin, path, "caller", nil)
|
||||
entry := sess.CodeBase() + uint64(fl.Offset)
|
||||
if _, err := bm.Set(entry, "entry"); err != nil {
|
||||
t.Fatalf("Set: %v", err)
|
||||
}
|
||||
runToEntry(t, sess, bm, entry)
|
||||
afterOff := uint64(fl.Labels["aftercall"])
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
REPL(sess, bm, sess.CodeBase(), fl.Offset, fl.Size, fl.Args, nil, nil,
|
||||
strings.NewReader("step 3\nnext\nregs\nquit\n"))
|
||||
})
|
||||
if !strings.Contains(out, fmt.Sprintf("func+%#x", afterOff)) {
|
||||
t.Errorf("next did not land on the instruction after the CALL (func+%#x); output:\n%s", afterOff, out)
|
||||
}
|
||||
if !strings.Contains(out, "RAX = 0x0000000000000008") {
|
||||
t.Errorf("callee did not run exactly once under next (want RAX=8); output:\n%s", out)
|
||||
}
|
||||
|
||||
// finish: run to the return address read off the stack at entry.
|
||||
sess2, bm2, fl2 := launchKernel(t, bin, path, "caller", nil)
|
||||
entry2 := sess2.CodeBase() + uint64(fl2.Offset)
|
||||
if _, err := bm2.Set(entry2, "entry"); err != nil {
|
||||
t.Fatalf("Set: %v", err)
|
||||
}
|
||||
runToEntry(t, sess2, bm2, entry2)
|
||||
regs, err := sess2.GetRegs()
|
||||
if err != nil {
|
||||
t.Fatalf("GetRegs: %v", err)
|
||||
}
|
||||
retAddr, err := sess2.Peek(regs.RSP)
|
||||
if err != nil {
|
||||
t.Fatalf("Peek return address: %v", err)
|
||||
}
|
||||
|
||||
out2 := captureStdout(t, func() {
|
||||
REPL(sess2, bm2, sess2.CodeBase(), fl2.Offset, fl2.Size, fl2.Args, nil, nil,
|
||||
strings.NewReader("step 1\nfinish\nquit\n"))
|
||||
})
|
||||
want := fmt.Sprintf("finished, now at %#x\n", retAddr)
|
||||
if !strings.Contains(out2, want) {
|
||||
t.Errorf("finish stopped at the wrong PC; want %q in output:\n%s", want, out2)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignalStopSurfaced proves a faulting kernel surfaces as a reported
|
||||
// stop instead of an infinite fault loop. A regression here hangs, so a
|
||||
// watchdog fails the run rather than letting CI stall.
|
||||
func TestSignalStopSurfaced(t *testing.T) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
bin := buildGasm(t)
|
||||
|
||||
const kernel = `#include "textflag.h"
|
||||
|
||||
// func crash() int64
|
||||
TEXT ·crash(SB), NOSPLIT, $0-8
|
||||
XORQ AX, AX
|
||||
MOVQ (AX), AX
|
||||
MOVQ AX, ret+0(FP)
|
||||
RET
|
||||
`
|
||||
path := writeKernel(t, kernel)
|
||||
sess, bm, _ := launchKernel(t, bin, path, "crash", nil)
|
||||
|
||||
timer := time.AfterFunc(time.Minute, func() {
|
||||
panic("watchdog: the debugger hung on the faulting kernel instead of reporting the signal stop")
|
||||
})
|
||||
defer timer.Stop()
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
REPL(sess, bm, sess.CodeBase(), 0, 0, 0, nil, nil,
|
||||
strings.NewReader("continue\nquit\n"))
|
||||
})
|
||||
if !strings.Contains(out, "stopped on signal") {
|
||||
t.Errorf("SIGSEGV did not surface as a reported stop; output:\n%s", out)
|
||||
}
|
||||
if !sess.Exited() {
|
||||
t.Error("debuggee should be killed by quit after the signal stop")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetVectorRegsXState proves the NT_X86_XSTATE readout: the request
|
||||
// succeeds on a normal process and the XMM halves agree with
|
||||
// PTRACE_GETFPREGS.
|
||||
func TestGetVectorRegsXState(t *testing.T) {
|
||||
// The FPRegs layout must mirror the kernel's user_fpregs_struct
|
||||
// exactly: PTRACE_GETFPREGS fills all 512 bytes, so a short struct
|
||||
// overflows the caller's memory.
|
||||
if got := unsafe.Sizeof(FPRegs{}); got != 512 {
|
||||
t.Fatalf("sizeof(FPRegs) = %d, want 512", got)
|
||||
}
|
||||
if got := unsafe.Offsetof(FPRegs{}.XMM); got != 160 {
|
||||
t.Fatalf("offsetof(FPRegs.XMM) = %d, want 160", got)
|
||||
}
|
||||
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
bin := buildGasm(t)
|
||||
|
||||
const kernel = `#include "textflag.h"
|
||||
|
||||
// func vprobe() int64
|
||||
TEXT ·vprobe(SB), NOSPLIT, $0-8
|
||||
MOVQ $1, AX
|
||||
MOVQ AX, ret+0(FP)
|
||||
RET
|
||||
`
|
||||
path := writeKernel(t, kernel)
|
||||
sess, bm, fl := launchKernel(t, bin, path, "vprobe", nil)
|
||||
|
||||
entry := sess.CodeBase() + uint64(fl.Offset)
|
||||
if _, err := bm.Set(entry, "entry"); err != nil {
|
||||
t.Fatalf("Set: %v", err)
|
||||
}
|
||||
runToEntry(t, sess, bm, entry)
|
||||
|
||||
v, err := sess.GetVectorRegs()
|
||||
if err != nil {
|
||||
t.Fatalf("GetVectorRegs: %v", err)
|
||||
}
|
||||
fp, err := sess.GetFPRegs()
|
||||
if err != nil {
|
||||
t.Fatalf("GetFPRegs: %v", err)
|
||||
}
|
||||
for i := range 16 {
|
||||
if !bytes.Equal(v.YMM[i][:16], fp.XMM[i][:]) {
|
||||
t.Errorf("YMM%d low half %x, want the FPRegs XMM half %x", i, v.YMM[i][:16], fp.XMM[i][:])
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
-28
@@ -23,7 +23,13 @@ type Session struct {
|
||||
stopped bool
|
||||
exited bool
|
||||
codeBase uint64 // base address of the JIT code in the debuggee
|
||||
wpSlots [16]bool // hardware watchpoint slots in use (DR0-DR3, arm64 BADVR0-15)
|
||||
tmpDir string // scratch directory of the session, removed on Kill
|
||||
wpSlots [16]bool // hardware watchpoint slots in use (DR0-DR3, arm64 DBGWVR0-15)
|
||||
// lastSignal holds the signal of the most recent stop when that stop
|
||||
// was a genuine signal-delivery-stop the caller must see (a fault such
|
||||
// as SIGSEGV, SIGBUS, SIGFPE or SIGILL); 0 for breakpoint traps,
|
||||
// single-steps, SIGSTOP and suppressed runtime signals.
|
||||
lastSignal syscall.Signal
|
||||
}
|
||||
|
||||
// Launch starts the debuggee subprocess (gasm debug --target ...) and
|
||||
@@ -78,7 +84,7 @@ func LaunchWithBuffers(gasmBin, asmPath, funcName string, args []byte, bufSpec s
|
||||
return nil, nil, fmt.Errorf("debug: start debuggee: %w", err)
|
||||
}
|
||||
|
||||
s := &Session{pid: cmd.Process.Pid, cmd: cmd}
|
||||
s := &Session{pid: cmd.Process.Pid, cmd: cmd, tmpDir: tmpDir}
|
||||
|
||||
readyFile := filepath.Join(tmpDir, "ready")
|
||||
for range 500 {
|
||||
@@ -125,29 +131,17 @@ func LaunchWithBuffers(gasmBin, asmPath, funcName string, args []byte, bufSpec s
|
||||
return s, bufAddrs, nil
|
||||
}
|
||||
|
||||
// wait waits for the debuggee to stop and returns the wait status.
|
||||
func (s *Session) wait() error {
|
||||
var ws syscall.WaitStatus
|
||||
_, err := syscall.Wait4(s.pid, &ws, 0, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ws.Exited() {
|
||||
s.exited = true
|
||||
return fmt.Errorf("debuggee exited with status %d", ws.ExitStatus())
|
||||
}
|
||||
s.stopped = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// waitStopped consumes ptrace-stop events until one the debugger cares
|
||||
// about arrives: SIGTRAP (a breakpoint or a completed single-step) or the
|
||||
// debuggee's own SIGSTOP. A Go tracee's runtime raises SIGURG for
|
||||
// asynchronous preemption, and every signal on a traced thread surfaces as
|
||||
// a signal-delivery-stop, so those are suppressed and the tracee resumed
|
||||
// without them. Runtime noise is why a single wait can return in the
|
||||
// middle of runtime code and a resume can then fail: the event stream must
|
||||
// be drained by the tracer.
|
||||
// about arrives: SIGTRAP (a breakpoint or a completed single-step), the
|
||||
// debuggee's own SIGSTOP, or a genuine signal-delivery-stop. A Go tracee's
|
||||
// runtime raises SIGURG for asynchronous preemption, and every signal on a
|
||||
// traced thread surfaces as a signal-delivery-stop, so SIGURG is suppressed
|
||||
// and the tracee resumed without it. Every other signal (SIGSEGV, SIGBUS,
|
||||
// SIGFPE, SIGILL, ...) is returned to the caller: resuming with signal 0
|
||||
// would restart the faulting instruction and fault forever, so a faulting
|
||||
// kernel must surface as a stop the caller reports. Runtime noise is also
|
||||
// why a single wait can return in the middle of runtime code and a resume
|
||||
// can then fail: the event stream must be drained by the tracer.
|
||||
func (s *Session) waitStopped() (syscall.Signal, error) {
|
||||
for {
|
||||
var ws syscall.WaitStatus
|
||||
@@ -165,10 +159,12 @@ func (s *Session) waitStopped() (syscall.Signal, error) {
|
||||
switch sig := ws.StopSignal(); sig {
|
||||
case syscall.SIGTRAP, syscall.SIGSTOP:
|
||||
s.stopped = true
|
||||
s.lastSignal = 0
|
||||
return sig, nil
|
||||
default:
|
||||
// Runtime noise (SIGURG preemption and friends): resume the
|
||||
// tracee without delivering the signal.
|
||||
case syscall.SIGURG:
|
||||
// Go runtime asynchronous preemption: resume the tracee
|
||||
// without delivering the signal.
|
||||
s.lastSignal = 0
|
||||
if _, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_CONT),
|
||||
@@ -177,10 +173,22 @@ func (s *Session) waitStopped() (syscall.Signal, error) {
|
||||
); errno != 0 {
|
||||
return 0, fmt.Errorf("debug: PTRACE_CONT: %w", errno)
|
||||
}
|
||||
default:
|
||||
// A genuine signal-delivery-stop. Report it; the caller
|
||||
// decides how to proceed.
|
||||
s.stopped = true
|
||||
s.lastSignal = sig
|
||||
return sig, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LastSignal returns the signal of the most recent stop when that stop was
|
||||
// a genuine signal-delivery-stop (a fault such as SIGSEGV, SIGFPE, SIGILL
|
||||
// or SIGBUS), and 0 for breakpoint traps, single-steps, SIGSTOP and
|
||||
// suppressed runtime signals.
|
||||
func (s *Session) LastSignal() syscall.Signal { return s.lastSignal }
|
||||
|
||||
// Peek reads a word (8 bytes) from the debuggee's memory at addr.
|
||||
func (s *Session) Peek(addr uint64) (uint64, error) {
|
||||
mem, err := os.OpenFile(fmt.Sprintf("/proc/%d/mem", s.pid), os.O_RDONLY, 0)
|
||||
@@ -294,7 +302,8 @@ func (s *Session) Pid() int { return s.pid }
|
||||
// CodeBase returns the base address of the JIT code in the debuggee.
|
||||
func (s *Session) CodeBase() uint64 { return s.codeBase }
|
||||
|
||||
// Kill terminates the debuggee.
|
||||
// Kill terminates the debuggee and removes the session's scratch
|
||||
// directory, so a successful session leaves no gasm-debug-* debris behind.
|
||||
func (s *Session) Kill() {
|
||||
if !s.exited {
|
||||
syscall.Kill(s.pid, syscall.SIGKILL)
|
||||
@@ -304,6 +313,35 @@ func (s *Session) Kill() {
|
||||
if s.cmd != nil && s.cmd.Process != nil {
|
||||
s.cmd.Wait()
|
||||
}
|
||||
if s.tmpDir != "" {
|
||||
os.RemoveAll(s.tmpDir)
|
||||
s.tmpDir = ""
|
||||
}
|
||||
}
|
||||
|
||||
// execRange is one executable mapping of the debuggee.
|
||||
type execRange struct {
|
||||
lo, hi uint64
|
||||
}
|
||||
|
||||
// execRanges parses the debuggee's executable mappings from /proc/pid/maps.
|
||||
func execRanges(pid int) []execRange {
|
||||
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/maps", pid))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var out []execRange
|
||||
for line := range strings.SplitSeq(string(data), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 || !strings.Contains(fields[1], "x") {
|
||||
continue
|
||||
}
|
||||
var lo, hi uint64
|
||||
if _, err := fmt.Sscanf(fields[0], "%x-%x", &lo, &hi); err == nil {
|
||||
out = append(out, execRange{lo, hi})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// findRWXMapping reads /proc/pid/maps and returns the base address of the
|
||||
|
||||
+68
-11
@@ -6,6 +6,7 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
@@ -44,20 +45,24 @@ func (s *Session) SetRegs(regs *Regs) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// FPRegs holds the x87 FPU and SSE (XMM) register state from PTRACE_GETFPREGS.
|
||||
// FPRegs holds the x87 FPU and SSE (XMM) register state from
|
||||
// PTRACE_GETFPREGS. The layout is the kernel's struct user_fpregs_struct
|
||||
// (sys/user.h), the FXSAVE image: 512 bytes with XMM0-15 at offset 160.
|
||||
// The i387 fcs/ds segment fields do not exist in the 64-bit layout. The
|
||||
// size matters: the copy fills all 512 bytes, so a short or misaligned
|
||||
// struct makes PTRACE_GETFPREGS overflow the caller's memory.
|
||||
type FPRegs struct {
|
||||
FCW uint16
|
||||
FSW uint16
|
||||
FTW byte
|
||||
FTW uint16
|
||||
FOP uint16
|
||||
FIP uint64
|
||||
FCS uint16
|
||||
FDP uint64
|
||||
FDS uint16
|
||||
MXCSR uint32
|
||||
MXCSRMask uint32
|
||||
ST [8][16]byte // x87 stack (10 bytes per reg, padded to 16)
|
||||
XMM [16][16]byte // XMM0-15
|
||||
XMM [16][16]byte // XMM0-15, struct offset 160
|
||||
Reserved [96]byte // FXSAVE padding, to the full 512 bytes
|
||||
}
|
||||
|
||||
// GetFPRegs retrieves the FPU/SSE register state of the stopped debuggee.
|
||||
@@ -82,16 +87,68 @@ type VectorRegs struct {
|
||||
YMM [16][32]byte // YMM0-15 (full 256-bit values)
|
||||
}
|
||||
|
||||
// GetVectorRegs retrieves the YMM registers via PTRACE_GETREGSET + XSAVE.
|
||||
// NT_X86_XSTATE (0x202), the xsave extended-state regset
|
||||
// (include/uapi/linux/elf.h).
|
||||
const ntX86XState = 0x202
|
||||
|
||||
// Layout of the buffer PTRACE_GETREGSET returns for NT_X86_XSTATE: the
|
||||
// 512-byte legacy fxsave image (x87 state in 0-159, XMM0-15 in 160-511),
|
||||
// then the 64-byte xsave header whose first 8 bytes are xstate_bv, then one
|
||||
// component per set feature bit, each 64-byte aligned. The YMM high halves
|
||||
// are the first extended component, at offset 576; that offset is fixed by
|
||||
// the ISA on AVX-capable x86-64. XFEATURE_MASK_YMM is bit 2 of xstate_bv
|
||||
// (arch/x86/include/asm/fpu/types.h); the high halves are zero when the bit
|
||||
// is clear.
|
||||
const (
|
||||
xsaveXMMOffset = 160
|
||||
xsaveXMMSize = 256
|
||||
xsaveHeaderOffset = 512
|
||||
xsaveBVOffset = xsaveHeaderOffset
|
||||
ymmOffset = xsaveHeaderOffset + 64 // 576
|
||||
ymmSize = 256 // 16 registers, 16 bytes each
|
||||
xfeatureMaskYMM = 1 << 2
|
||||
xstateMaxBuffer = 4096 // CPUID(0xD).xsave_size is far below this
|
||||
)
|
||||
|
||||
// GetVectorRegs retrieves the YMM registers via PTRACE_GETREGSET on
|
||||
// NT_X86_XSTATE. The low (XMM) halves always come from the legacy image;
|
||||
// the high halves are copied only when xstate_bv reports the YMM feature,
|
||||
// and read as zero otherwise. When the regset request fails the FP image
|
||||
// still provides correct XMM halves, so that is the fallback.
|
||||
func (s *Session) GetVectorRegs() (VectorRegs, error) {
|
||||
var v VectorRegs
|
||||
fp, err := s.GetFPRegs()
|
||||
if err != nil {
|
||||
return v, err
|
||||
buf := make([]byte, xstateMaxBuffer)
|
||||
iovec := syscall.Iovec{
|
||||
Base: &buf[0],
|
||||
Len: uint64(len(buf)),
|
||||
}
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_GETREGSET),
|
||||
uintptr(s.pid),
|
||||
uintptr(ntX86XState),
|
||||
uintptr(unsafe.Pointer(&iovec)),
|
||||
0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
fp, err := s.GetFPRegs()
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
for i := range 16 {
|
||||
copy(v.YMM[i][:16], fp.XMM[i][:])
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
n := int(iovec.Len)
|
||||
for i := range 16 {
|
||||
for j := range 16 {
|
||||
v.YMM[i][j] = fp.XMM[i][j]
|
||||
copy(v.YMM[i][:16], buf[xsaveXMMOffset+16*i:xsaveXMMOffset+16*i+16])
|
||||
}
|
||||
if n >= ymmOffset+ymmSize {
|
||||
if binary.LittleEndian.Uint64(buf[xsaveBVOffset:xsaveBVOffset+8])&xfeatureMaskYMM != 0 {
|
||||
for i := range 16 {
|
||||
copy(v.YMM[i][16:], buf[ymmOffset+16*i:ymmOffset+16*i+16])
|
||||
}
|
||||
}
|
||||
}
|
||||
return v, nil
|
||||
|
||||
@@ -91,5 +91,9 @@ func (r *Regs) RegValue(name string) (uint64, bool) {
|
||||
// breakpointInsn is the software breakpoint instruction.
|
||||
var breakpointInsn = []byte{0xCC} // INT3
|
||||
|
||||
// breakpointPCAdjust is how far PC is past the breakpoint instruction after a trap.
|
||||
// breakpointPCAdjust is how far PC is past the breakpoint instruction after
|
||||
// a trap. x86-64 reports the #DB for INT3 with RIP on the byte after the
|
||||
// INT3 (Intel SDM vol 3, "Debug Exceptions"), so the trap address is
|
||||
// PC-1. The other supported architectures leave the PC on the trap
|
||||
// instruction and use 0 there.
|
||||
const breakpointPCAdjust = 1
|
||||
|
||||
@@ -130,5 +130,11 @@ func (r *Regs) RegValue(name string) (uint64, bool) {
|
||||
// breakpointInsn is the software breakpoint instruction (BRK #0).
|
||||
var breakpointInsn = []byte{0x00, 0x00, 0x20, 0xD4} // BRK #0
|
||||
|
||||
// breakpointPCAdjust is how far PC is past the breakpoint instruction after a trap.
|
||||
const breakpointPCAdjust = 4
|
||||
// breakpointPCAdjust is how far PC is past the breakpoint instruction after
|
||||
// a trap: 0, because the arm64 kernel delivers the BRK SIGTRAP with the PC
|
||||
// still on the BRK. do_el0_brk64 calls send_user_sigtrap, which uses
|
||||
// instruction_pointer(regs) unmodified (arch/arm64/kernel/debug-monitors.c);
|
||||
// only the kernel-internal skip paths advance the PC. GDB history agrees:
|
||||
// decr_pc_after_break on aarch64 Linux is 0 (the +4 variant was a QEMU bug,
|
||||
// sourceware PR 17280).
|
||||
const breakpointPCAdjust = 0
|
||||
|
||||
@@ -126,5 +126,9 @@ func (r *Regs) RegValue(name string) (uint64, bool) {
|
||||
// breakpointInsn is the software breakpoint instruction (BRK $0).
|
||||
var breakpointInsn = []byte{0x05, 0x00, 0x2a, 0x00} // break 0
|
||||
|
||||
// breakpointPCAdjust is how far PC is past the breakpoint instruction after a trap.
|
||||
const breakpointPCAdjust = 4
|
||||
// breakpointPCAdjust is how far PC is past the breakpoint instruction after
|
||||
// a trap: 0, because the kernel delivers the break SIGTRAP with csr_era
|
||||
// still on the break instruction. do_bp passes regs->csr_era straight to
|
||||
// force_sig_fault(SIGTRAP, TRAP_BRKPT, ...) and never adjusts era on the
|
||||
// signal path (arch/loongarch/kernel/traps.c).
|
||||
const breakpointPCAdjust = 0
|
||||
|
||||
@@ -126,5 +126,9 @@ func (r *Regs) RegValue(name string) (uint64, bool) {
|
||||
// breakpointInsn is the software breakpoint instruction (EBREAK).
|
||||
var breakpointInsn = []byte{0x73, 0x00, 0x10, 0x00} // ebreak
|
||||
|
||||
// breakpointPCAdjust is how far PC is past the breakpoint instruction after a trap.
|
||||
const breakpointPCAdjust = 4
|
||||
// breakpointPCAdjust is how far PC is past the breakpoint instruction after
|
||||
// a trap: 0, because the kernel delivers the EBREAK SIGTRAP with sepc still
|
||||
// on the ebreak. handle_break passes regs->epc straight to
|
||||
// force_sig_fault(SIGTRAP, TRAP_BRKPT, ...) and only the kernel-internal
|
||||
// WARN/CFI paths advance epc (arch/riscv/kernel/traps.c).
|
||||
const breakpointPCAdjust = 0
|
||||
|
||||
+74
-18
@@ -7,9 +7,10 @@ package debug
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"cmp"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -93,7 +94,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
regs, _ := s.GetRegs()
|
||||
pc := regs.GetPC()
|
||||
text, instLen, _ := s.Disassemble(pc)
|
||||
if strings.HasPrefix(strings.ToLower(text), "call") || strings.HasPrefix(strings.ToLower(text), "bl") {
|
||||
if isCallInsn(text) {
|
||||
afterAddr := pc + uint64(instLen)
|
||||
_, err := bm.Set(afterAddr, "(next)")
|
||||
if err != nil {
|
||||
@@ -108,6 +109,21 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
bm.Clear(afterAddr)
|
||||
continue
|
||||
}
|
||||
if s.Exited() {
|
||||
bm.Clear(afterAddr)
|
||||
fmt.Println("debuggee exited")
|
||||
continue
|
||||
}
|
||||
if sig := s.LastSignal(); sig != 0 {
|
||||
bm.Clear(afterAddr)
|
||||
regs, _ := s.GetRegs()
|
||||
fmt.Printf("stopped on signal %v at %#x\n", sig, regs.GetPC())
|
||||
continue
|
||||
}
|
||||
// Fetch the registers after the stop: the trap must be
|
||||
// evaluated against the real PC, not the pre-Continue
|
||||
// snapshot, and a stale SetRegs would clobber live state.
|
||||
regs, _ = s.GetRegs()
|
||||
bm.HandleTrap(®s)
|
||||
bm.Clear(afterAddr)
|
||||
} else {
|
||||
@@ -143,9 +159,21 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
bm.Clear(retAddr)
|
||||
continue
|
||||
}
|
||||
if !s.Exited() {
|
||||
bm.HandleTrap(®s)
|
||||
if s.Exited() {
|
||||
bm.Clear(retAddr)
|
||||
fmt.Println("debuggee exited")
|
||||
continue
|
||||
}
|
||||
if sig := s.LastSignal(); sig != 0 {
|
||||
bm.Clear(retAddr)
|
||||
regs, _ := s.GetRegs()
|
||||
fmt.Printf("stopped on signal %v at %#x\n", sig, regs.GetPC())
|
||||
continue
|
||||
}
|
||||
// Fetch the registers after the stop, as the continue case
|
||||
// does: HandleTrap must see the PC the trap left behind.
|
||||
regs, _ = s.GetRegs()
|
||||
bm.HandleTrap(®s)
|
||||
bm.Clear(retAddr)
|
||||
if s.Exited() {
|
||||
fmt.Println("debuggee exited")
|
||||
@@ -171,6 +199,15 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
fmt.Println("debuggee exited")
|
||||
break
|
||||
}
|
||||
if sig := s.LastSignal(); sig != 0 {
|
||||
// A genuine signal-delivery-stop (a fault): report it
|
||||
// and return to the prompt. Continuing would restart
|
||||
// the faulting instruction and fault forever.
|
||||
regs, _ := s.GetRegs()
|
||||
fmt.Printf("stopped on signal %v at %#x (func+%#x)\n",
|
||||
sig, regs.GetPC(), regs.GetPC()-codeBase-uint64(funcOffset))
|
||||
break
|
||||
}
|
||||
reason, wpAddr := s.StopInfo()
|
||||
if reason == StopWatchpoint {
|
||||
fmt.Printf("watchpoint hit at %#x\n", wpAddr)
|
||||
@@ -221,13 +258,27 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
reg := strings.ToLower(parts[3])
|
||||
op := parts[4]
|
||||
operand := parts[5]
|
||||
if val, err := strconv.ParseUint(operand, 0, 64); err == nil {
|
||||
cond = &Condition{Reg: reg, Op: op, Value: val}
|
||||
} else {
|
||||
cond = &Condition{Reg: reg, Op: op, Reg2: strings.ToLower(operand)}
|
||||
switch {
|
||||
case strings.HasPrefix(operand, "*"):
|
||||
// Memory operand: compare against the 8-byte word at
|
||||
// the address, resolved in the debuggee when the
|
||||
// breakpoint is evaluated.
|
||||
addr, err := strconv.ParseUint(strings.TrimPrefix(operand, "*"), 0, 64)
|
||||
if err != nil {
|
||||
fmt.Printf("invalid memory operand: %s\n", operand)
|
||||
continue
|
||||
}
|
||||
cond = &Condition{Reg: reg, Op: op, MemAddr: addr}
|
||||
default:
|
||||
val, err := strconv.ParseUint(operand, 0, 64)
|
||||
if err == nil {
|
||||
cond = &Condition{Reg: reg, Op: op, Value: val}
|
||||
} else {
|
||||
cond = &Condition{Reg: reg, Op: op, Reg2: strings.ToLower(operand)}
|
||||
}
|
||||
}
|
||||
} else if len(parts) >= 4 && parts[2] == "if" {
|
||||
fmt.Println("usage: break <label|addr> if <reg> <op> <value|reg>")
|
||||
fmt.Println("usage: break <label|addr> if <reg> <op> <value|reg|*addr>")
|
||||
continue
|
||||
}
|
||||
bp, err := bm.SetWithCond(addr, label, cond)
|
||||
@@ -237,7 +288,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
}
|
||||
condStr := ""
|
||||
if cond != nil {
|
||||
condStr = fmt.Sprintf(" if %s %s %#x", cond.Reg, cond.Op, cond.Value)
|
||||
condStr = " if " + cond.String()
|
||||
}
|
||||
fmt.Printf("breakpoint set: %s at %#x (func+%#x)%s\n", bp.Label, bp.Addr, bp.Addr-codeBase-uint64(funcOffset), condStr)
|
||||
|
||||
@@ -277,7 +328,11 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
addr, _ = resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
||||
}
|
||||
if len(parts) > 2 {
|
||||
length, _ = strconv.Atoi(parts[2])
|
||||
// A malformed or non-positive length would panic
|
||||
// ReadMemory's make; fall back to the default instead.
|
||||
if n, err := strconv.Atoi(parts[2]); err == nil && n > 0 {
|
||||
length = n
|
||||
}
|
||||
}
|
||||
mem, err := s.ReadMemory(addr, length)
|
||||
if err != nil {
|
||||
@@ -336,10 +391,8 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
}
|
||||
|
||||
case "labels", "l":
|
||||
sorted := make([]Label, len(labels))
|
||||
copy(sorted, labels)
|
||||
sort.Slice(sorted, func(i, j int) bool { return sorted[i].Offset < sorted[j].Offset })
|
||||
for _, l := range sorted {
|
||||
slices.SortFunc(labels, func(a, b Label) int { return cmp.Compare(a.Offset, b.Offset) })
|
||||
for _, l := range labels {
|
||||
fmt.Printf(" func+%#04x %s\n", l.Offset, l.Name)
|
||||
}
|
||||
|
||||
@@ -369,7 +422,10 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
fmt.Println()
|
||||
|
||||
case "help", "h", "?":
|
||||
fmt.Printf(` break <label|addr> [if <reg> <op> <val>] set a breakpoint
|
||||
fmt.Printf(` break <label|addr> [if <reg> <op> <val|reg|*addr>]
|
||||
set a breakpoint, optionally conditional on a
|
||||
register compared to a constant, a register, or the
|
||||
8-byte word at *addr
|
||||
delete <label|addr> remove a breakpoint
|
||||
info break list all breakpoints
|
||||
watch <addr> [r|w] [size] set a hardware watchpoint (write by default)
|
||||
@@ -463,8 +519,8 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
case "unwatch":
|
||||
if len(parts) >= 2 {
|
||||
slot, err := strconv.Atoi(parts[1])
|
||||
if err != nil || slot < 0 || slot > 3 {
|
||||
fmt.Println("usage: unwatch [<slot>]")
|
||||
if err != nil || slot < 0 || slot >= maxWatchpoints() {
|
||||
fmt.Printf("usage: unwatch [<slot 0-%d>]\n", maxWatchpoints()-1)
|
||||
continue
|
||||
}
|
||||
if err := s.ClearWatchpoint(slot); err != nil {
|
||||
|
||||
+10
-2
@@ -6,6 +6,7 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
@@ -61,8 +62,15 @@ func (s *Session) StopInfo() (StopReason, uint64) {
|
||||
case trapBRKPT:
|
||||
return StopBreakpoint, 0
|
||||
case trapHWBRKPT:
|
||||
addr := *(*uint64)(unsafe.Add(unsafe.Pointer(&info), 16))
|
||||
return StopWatchpoint, addr
|
||||
// si_addr sits at struct offset 16 (12 bytes of signo/errno/code
|
||||
// plus 4 bytes of union alignment). The siginfo buffer is only
|
||||
// 4-byte aligned, so the address is read byte-wise to keep the
|
||||
// load aligned on riscv64 and loong64. What si_addr names is
|
||||
// architecture-specific (the data address on arm64, the
|
||||
// instruction pointer on x86), so the per-architecture
|
||||
// archWatchpointAddr resolves it to the watched address.
|
||||
addr := binary.LittleEndian.Uint64(info._pad[4:12])
|
||||
return StopWatchpoint, archWatchpointAddr(s, addr)
|
||||
default:
|
||||
return StopSingleStep, 0
|
||||
}
|
||||
|
||||
@@ -28,15 +28,7 @@ func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
||||
return fmt.Errorf("debug target: parse: %v", errs[0])
|
||||
}
|
||||
|
||||
var img *asm.Image
|
||||
switch "arm64" {
|
||||
case "arm64":
|
||||
img, err = asm.AssembleFileARM64(file)
|
||||
case "riscv64":
|
||||
img, err = asm.AssembleFileRISCV(file)
|
||||
case "loong64":
|
||||
img, err = asm.AssembleFileLOONG64(file)
|
||||
}
|
||||
img, err := asm.AssembleFileARM64(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug target: assemble: %w", err)
|
||||
}
|
||||
|
||||
@@ -28,15 +28,7 @@ func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
||||
return fmt.Errorf("debug target: parse: %v", errs[0])
|
||||
}
|
||||
|
||||
var img *asm.Image
|
||||
switch "loong64" {
|
||||
case "arm64":
|
||||
img, err = asm.AssembleFileARM64(file)
|
||||
case "riscv64":
|
||||
img, err = asm.AssembleFileRISCV(file)
|
||||
case "loong64":
|
||||
img, err = asm.AssembleFileLOONG64(file)
|
||||
}
|
||||
img, err := asm.AssembleFileLOONG64(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug target: assemble: %w", err)
|
||||
}
|
||||
|
||||
+8
-2
@@ -12,10 +12,11 @@ type tracer interface {
|
||||
Peek(addr uint64) (uint64, error)
|
||||
Poke(addr uint64, val uint64) error
|
||||
SetRegs(regs *Regs) error
|
||||
Step() error
|
||||
Pid() int
|
||||
}
|
||||
|
||||
// mockTracer records Peek/Poke calls and provides fake register state.
|
||||
// mockTracer records Peek/Poke/Step calls and provides fake register state.
|
||||
type mockTracer struct {
|
||||
mem map[uint64]byte
|
||||
peeks []uint64
|
||||
@@ -23,7 +24,8 @@ type mockTracer struct {
|
||||
addr uint64
|
||||
val uint64
|
||||
}
|
||||
regs *Regs
|
||||
steps int
|
||||
regs *Regs
|
||||
}
|
||||
|
||||
func newMockTracer() *mockTracer {
|
||||
@@ -57,4 +59,8 @@ func (m *mockTracer) SetRegs(regs *Regs) error {
|
||||
m.regs = regs
|
||||
return nil
|
||||
}
|
||||
func (m *mockTracer) Step() error {
|
||||
m.steps++
|
||||
return nil
|
||||
}
|
||||
func (m *mockTracer) Pid() int { return 42 }
|
||||
|
||||
@@ -8,10 +8,48 @@ package debug
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Hardware watchpoint support via x86-64 debug registers (DR0-DR3, DR7).
|
||||
|
||||
// The kernel translates PTRACE_POKEUSER/PEEKUSER offsets inside
|
||||
// [offsetof(struct user, u_debugreg[0]), u_debugreg[7]] to DR0-DR7
|
||||
// (arch/x86/kernel/ptrace.c, arch_ptrace). sys/user.h places u_debugreg at
|
||||
// 0x350: DR0-DR3 are 0x350/0x358/0x360/0x368, DR6 (status) is 0x380 and
|
||||
// DR7 (control) is 0x388. Offsets below 0x350 write user_regs_struct
|
||||
// fields (r15 at 0x0, r10 at 0x38), not debug registers.
|
||||
const (
|
||||
drOffset = 0x350 // offsetof(struct user, u_debugreg[0]), DR0
|
||||
dr6Off = 0x380 // offsetof(struct user, u_debugreg[6]), DR6
|
||||
dr7Off = 0x388 // offsetof(struct user, u_debugreg[7]), DR7
|
||||
)
|
||||
|
||||
// archWatchpointAddr resolves the address of the watchpoint that fired.
|
||||
// x86 delivers si_addr = the instruction pointer of the trapping access
|
||||
// (arch/x86/kernel/ptrace.c send_sigtrap passes regs->ip), so the watched
|
||||
// data address is recovered from DR6's slot bits (B0-B3, positive polarity
|
||||
// through PEEKUSER) and the matching DR0-DR3.
|
||||
func archWatchpointAddr(s *Session, siAddr uint64) uint64 {
|
||||
dr6, err := ptracePeekUser(s.pid, dr6Off)
|
||||
if err != nil {
|
||||
return siAddr
|
||||
}
|
||||
for slot := range 4 {
|
||||
if dr6&(1<<slot) != 0 {
|
||||
addr, err := ptracePeekUser(s.pid, drOffset+uintptr(slot*8))
|
||||
if err == nil && addr != 0 {
|
||||
return addr
|
||||
}
|
||||
}
|
||||
}
|
||||
return siAddr
|
||||
}
|
||||
|
||||
// maxWatchpoints reports the number of hardware watchpoint slots the
|
||||
// architecture provides: four address registers, DR0-DR3.
|
||||
func maxWatchpoints() int { return 4 }
|
||||
|
||||
// WatchpointType selects what triggers the watchpoint.
|
||||
type WatchpointType int
|
||||
|
||||
@@ -62,23 +100,11 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
||||
return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8")
|
||||
}
|
||||
|
||||
var drAddr uintptr
|
||||
switch slot {
|
||||
case 0:
|
||||
drAddr = 0x0
|
||||
case 1:
|
||||
drAddr = 0x8
|
||||
case 2:
|
||||
drAddr = 0x10
|
||||
case 3:
|
||||
drAddr = 0x18
|
||||
}
|
||||
|
||||
if err := ptracePokeUser(s.pid, drAddr, addr); err != nil {
|
||||
if err := ptracePokeUser(s.pid, drOffset+uintptr(slot*8), addr); err != nil {
|
||||
return fmt.Errorf("debug: set DR%d: %w", slot, err)
|
||||
}
|
||||
|
||||
dr7, err := ptracePeekUser(s.pid, 0x38)
|
||||
dr7, err := ptracePeekUser(s.pid, dr7Off)
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug: read DR7: %w", err)
|
||||
}
|
||||
@@ -90,7 +116,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
||||
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 {
|
||||
if err := ptracePokeUser(s.pid, dr7Off, dr7); err != nil {
|
||||
return fmt.Errorf("debug: set DR7: %w", err)
|
||||
}
|
||||
s.wpSlots[slot] = true
|
||||
@@ -105,12 +131,12 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
||||
if !s.wpSlots[slot] {
|
||||
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
||||
}
|
||||
dr7, err := ptracePeekUser(s.pid, 0x38)
|
||||
dr7, err := ptracePeekUser(s.pid, dr7Off)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dr7 &^= uint64(1) << (2 * slot)
|
||||
if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil {
|
||||
if err := ptracePokeUser(s.pid, dr7Off, dr7); err != nil {
|
||||
return err
|
||||
}
|
||||
s.wpSlots[slot] = false
|
||||
@@ -119,7 +145,7 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
||||
|
||||
// ClearAllWatchpoints removes all hardware watchpoints.
|
||||
func (s *Session) ClearAllWatchpoints() error {
|
||||
for slot := range 4 {
|
||||
for slot := range maxWatchpoints() {
|
||||
if s.wpSlots[slot] {
|
||||
if err := s.ClearWatchpoint(slot); err != nil {
|
||||
return err
|
||||
@@ -146,16 +172,21 @@ func ptracePokeUser(pid int, offset uintptr, val uint64) error {
|
||||
}
|
||||
|
||||
func ptracePeekUser(pid int, offset uintptr) (uint64, error) {
|
||||
// x86 PEEKUSR writes the word to the user-space pointer in data
|
||||
// (arch/x86/kernel/ptrace.c uses put_user); passing 0 there fails with
|
||||
// EFAULT, so the word is read through a real address.
|
||||
const ptracePeekuser = 3
|
||||
val, _, errno := syscall.Syscall6(
|
||||
var word uint64
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(ptracePeekuser),
|
||||
uintptr(pid),
|
||||
offset,
|
||||
0, 0, 0,
|
||||
uintptr(unsafe.Pointer(&word)),
|
||||
0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return 0, errno
|
||||
}
|
||||
return uint64(val), nil
|
||||
return word, nil
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
// Hardware watchpoint support via arm64 debug registers (DBGWVR/DBGWCR).
|
||||
// Accessed via PTRACE_SETREGSET with NT_ARM_HW_BREAK.
|
||||
// Accessed via PTRACE_GETREGSET/SETREGSET with NT_ARM_HW_WATCH.
|
||||
|
||||
// WatchpointType selects what triggers the watchpoint.
|
||||
type WatchpointType int
|
||||
@@ -22,26 +22,35 @@ const (
|
||||
WatchRead WatchpointType = 3
|
||||
)
|
||||
|
||||
const maxWatchpoints = 16
|
||||
// maxWatchpoints reports the number of hardware watchpoint slots the
|
||||
// architecture provides: DBGWVR0-DBGWCR15.
|
||||
func maxWatchpoints() int { return 16 }
|
||||
|
||||
// hwBreakState mirrors the kernel's struct user_hwdebug_state.
|
||||
type hwBreakState struct {
|
||||
// hwWatchState mirrors the kernel's struct user_hwdebug_state.
|
||||
type hwWatchState struct {
|
||||
DbgInfo uint32
|
||||
_pad [4]byte
|
||||
DbgRegs [16]hwBreakReg
|
||||
DbgRegs [16]hwWatchReg
|
||||
}
|
||||
|
||||
type hwBreakReg struct {
|
||||
type hwWatchReg struct {
|
||||
Addr uint64
|
||||
Ctrl uint64
|
||||
}
|
||||
|
||||
const (
|
||||
ntArmHWBreak = 0x403 // NT_ARM_HW_BREAK
|
||||
)
|
||||
// ntArmHWWatch is NT_ARM_HW_WATCH (0x403), the watchpoint regset
|
||||
// (include/uapi/linux/elf.h; 0x402 is NT_ARM_HW_BREAK). Watchpoints and
|
||||
// breakpoints live in different regsets with the same struct shape, so the
|
||||
// constant is named for what it arms to keep a future edit from arming
|
||||
// breakpoints instead.
|
||||
const ntArmHWWatch = 0x403
|
||||
|
||||
// archWatchpointAddr resolves the address of the watchpoint that fired:
|
||||
// the arm64 kernel already reports the watched data address as si_addr.
|
||||
func archWatchpointAddr(s *Session, siAddr uint64) uint64 { return siAddr }
|
||||
|
||||
func (s *Session) FindFreeWatchpointSlot() int {
|
||||
for i := range maxWatchpoints {
|
||||
for i := range maxWatchpoints() {
|
||||
if !s.wpSlots[i] {
|
||||
return i
|
||||
}
|
||||
@@ -50,7 +59,7 @@ func (s *Session) FindFreeWatchpointSlot() int {
|
||||
}
|
||||
|
||||
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
||||
if slot < 0 || slot >= maxWatchpoints {
|
||||
if slot < 0 || slot >= maxWatchpoints() {
|
||||
return false
|
||||
}
|
||||
return s.wpSlots[slot]
|
||||
@@ -58,27 +67,31 @@ func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
||||
|
||||
// SetWatchpoint installs a hardware watchpoint on the given address.
|
||||
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 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)
|
||||
}
|
||||
|
||||
state, err := s.getHWBreakState()
|
||||
state, err := s.getHWWatchState()
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug: read watchpoint state: %w", err)
|
||||
}
|
||||
|
||||
if uint32(slot) >= state.DbgInfo {
|
||||
return fmt.Errorf("debug: slot %d exceeds available watchpoints (%d)", slot, state.DbgInfo)
|
||||
// MDSCR_EL1 packs (debug_arch << 8) | num_slots into dbg_info, so only
|
||||
// the low byte counts slots.
|
||||
if uint32(slot) >= state.DbgInfo&0xff {
|
||||
return fmt.Errorf("debug: slot %d exceeds available watchpoints (%d)", slot, state.DbgInfo&0xff)
|
||||
}
|
||||
|
||||
state.DbgRegs[slot].Addr = addr
|
||||
// DBGWCR bits 3-4 select the access type: 01 load, 10 store, 11 either
|
||||
// (ARM DDI 0487, DBGWCR<n>_EL1 watchpoint type field).
|
||||
ctrl := uint64(1) // enable
|
||||
switch typ {
|
||||
case WatchWrite:
|
||||
ctrl |= 1 << 3 // store only
|
||||
ctrl |= 2 << 3 // store only
|
||||
case WatchRead:
|
||||
ctrl |= 3 << 3 // load+store
|
||||
}
|
||||
@@ -98,7 +111,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
||||
ctrl |= bas << 5
|
||||
state.DbgRegs[slot].Ctrl = ctrl
|
||||
|
||||
if err := s.setHWBreakState(state); err != nil {
|
||||
if err := s.setHWWatchState(state); err != nil {
|
||||
return fmt.Errorf("debug: set watchpoint: %w", err)
|
||||
}
|
||||
|
||||
@@ -107,20 +120,20 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
||||
}
|
||||
|
||||
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 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)
|
||||
}
|
||||
|
||||
state, err := s.getHWBreakState()
|
||||
state, err := s.getHWWatchState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state.DbgRegs[slot].Addr = 0
|
||||
state.DbgRegs[slot].Ctrl = 0
|
||||
if err := s.setHWBreakState(state); err != nil {
|
||||
if err := s.setHWWatchState(state); err != nil {
|
||||
return err
|
||||
}
|
||||
s.wpSlots[slot] = false
|
||||
@@ -128,7 +141,7 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
||||
}
|
||||
|
||||
func (s *Session) ClearAllWatchpoints() error {
|
||||
for slot := 0; slot < maxWatchpoints; slot++ {
|
||||
for slot := range maxWatchpoints() {
|
||||
if s.wpSlots[slot] {
|
||||
if err := s.ClearWatchpoint(slot); err != nil {
|
||||
return err
|
||||
@@ -138,8 +151,8 @@ func (s *Session) ClearAllWatchpoints() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) getHWBreakState() (*hwBreakState, error) {
|
||||
var state hwBreakState
|
||||
func (s *Session) getHWWatchState() (*hwWatchState, error) {
|
||||
var state hwWatchState
|
||||
iovec := syscall.Iovec{
|
||||
Base: (*byte)(unsafe.Pointer(&state)),
|
||||
Len: uint64(unsafe.Sizeof(state)),
|
||||
@@ -148,7 +161,7 @@ func (s *Session) getHWBreakState() (*hwBreakState, error) {
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_GETREGSET),
|
||||
uintptr(s.pid),
|
||||
uintptr(ntArmHWBreak),
|
||||
uintptr(ntArmHWWatch),
|
||||
uintptr(unsafe.Pointer(&iovec)),
|
||||
0, 0,
|
||||
)
|
||||
@@ -158,7 +171,7 @@ func (s *Session) getHWBreakState() (*hwBreakState, error) {
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
func (s *Session) setHWBreakState(state *hwBreakState) error {
|
||||
func (s *Session) setHWWatchState(state *hwWatchState) error {
|
||||
iovec := syscall.Iovec{
|
||||
Base: (*byte)(unsafe.Pointer(state)),
|
||||
Len: uint64(unsafe.Sizeof(*state)),
|
||||
@@ -167,7 +180,7 @@ func (s *Session) setHWBreakState(state *hwBreakState) error {
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_SETREGSET),
|
||||
uintptr(s.pid),
|
||||
uintptr(ntArmHWBreak),
|
||||
uintptr(ntArmHWWatch),
|
||||
uintptr(unsafe.Pointer(&iovec)),
|
||||
0, 0,
|
||||
)
|
||||
|
||||
@@ -8,10 +8,47 @@ package debug
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Hardware watchpoint support for LoongArch via debug registers.
|
||||
// Uses PTRACE_POKEUSER/PEEKUSER to access HW watchpoint registers.
|
||||
// Hardware watchpoint support via the NT_LOONGARCH_HW_WATCH regset.
|
||||
//
|
||||
// The kernel's PTRACE_POKEUSER on loong64 accepts only the user_pt_regs
|
||||
// indices 0-34 (GPRs, orig_a0, era, badv, per
|
||||
// arch/loongarch/include/uapi/asm/ptrace.h), so there is no debug-register
|
||||
// window to poke. The real interface is PTRACE_GETREGSET/SETREGSET on
|
||||
// NT_LOONGARCH_HW_WATCH (0xa06, include/uapi/linux/elf.h) with struct
|
||||
// user_watch_state_v2 (arch/loongarch/include/uapi/asm/ptrace.h): a dbg_info
|
||||
// word followed by 14 slots of {addr u64, mask u64, ctrl u32, pad u32}.
|
||||
// hw_break_get puts the slot count in the low byte of dbg_info
|
||||
// (arch/loongarch/kernel/ptrace.c, ptrace_hbp_get_resource_info) and
|
||||
// hw_break_set ignores dbg_info, reading addr, mask and ctrl per slot.
|
||||
|
||||
const ntLoongHWWatch = 0xa06
|
||||
|
||||
// loongWatchState mirrors the kernel's struct user_watch_state_v2.
|
||||
type loongWatchState struct {
|
||||
DbgInfo uint64
|
||||
DbgRegs [14]loongWatchReg
|
||||
}
|
||||
|
||||
type loongWatchReg struct {
|
||||
Addr uint64
|
||||
Mask uint64
|
||||
Ctrl uint32
|
||||
Pad uint32
|
||||
}
|
||||
|
||||
// Control word bit layout (arch/loongarch/include/asm/hw_breakpoint.h):
|
||||
// bits 1-4 privilege enables (CTRL_PLV3_ENABLE, 0x10, covers user mode),
|
||||
// bits 8-9 access type (LOAD 1<<0, STORE 1<<1), bits 10-11 length
|
||||
// (0=8 bytes, 1=4, 2=2, 3=1, inverted like the hardware FWP cfg).
|
||||
const (
|
||||
loongCtrlPLV3Enable = 0x10
|
||||
loongTypeLoad = 1 << 8
|
||||
loongTypeStore = 2 << 8
|
||||
loongLenShift = 10
|
||||
)
|
||||
|
||||
// WatchpointType selects what triggers the watchpoint.
|
||||
type WatchpointType int
|
||||
@@ -21,10 +58,16 @@ const (
|
||||
WatchRead WatchpointType = 3
|
||||
)
|
||||
|
||||
const maxWatchpoints = 4
|
||||
// maxWatchpoints reports the slot capacity of the regset struct; the number
|
||||
// the hardware actually provides is read from dbg_info at arm time.
|
||||
func maxWatchpoints() int { return len(loongWatchState{}.DbgRegs) }
|
||||
|
||||
// archWatchpointAddr resolves the address of the watchpoint that fired:
|
||||
// the loongarch kernel already reports the accessed address as si_addr.
|
||||
func archWatchpointAddr(s *Session, siAddr uint64) uint64 { return siAddr }
|
||||
|
||||
func (s *Session) FindFreeWatchpointSlot() int {
|
||||
for i := range maxWatchpoints {
|
||||
for i := range maxWatchpoints() {
|
||||
if !s.wpSlots[i] {
|
||||
return i
|
||||
}
|
||||
@@ -33,53 +76,56 @@ func (s *Session) FindFreeWatchpointSlot() int {
|
||||
}
|
||||
|
||||
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
||||
if slot < 0 || slot >= maxWatchpoints {
|
||||
if slot < 0 || slot >= maxWatchpoints() {
|
||||
return false
|
||||
}
|
||||
return s.wpSlots[slot]
|
||||
}
|
||||
|
||||
// SetWatchpoint installs a hardware watchpoint.
|
||||
// SetWatchpoint installs a hardware watchpoint on the given address.
|
||||
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 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)
|
||||
}
|
||||
if size != 1 && size != 2 && size != 4 && size != 8 {
|
||||
|
||||
var ctrlType uint32
|
||||
switch typ {
|
||||
case WatchWrite:
|
||||
ctrlType = loongTypeStore
|
||||
case WatchRead:
|
||||
ctrlType = loongTypeLoad | loongTypeStore
|
||||
}
|
||||
var lenBits uint32
|
||||
switch size {
|
||||
case 1:
|
||||
lenBits = 3
|
||||
case 2:
|
||||
lenBits = 2
|
||||
case 4:
|
||||
lenBits = 1
|
||||
case 8:
|
||||
lenBits = 0
|
||||
default:
|
||||
return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8")
|
||||
}
|
||||
|
||||
// LoongArch debug registers: DBGWVR (watchpoint value) and DBGWCR (watchpoint control).
|
||||
// Accessed via PTRACE_POKEUSER at architecture-specific offsets.
|
||||
if err := ptracePokeUser(s.pid, uintptr(0x1000+slot*8), addr); err != nil {
|
||||
return fmt.Errorf("debug: set watchpoint address: %w", err)
|
||||
state, err := s.getLoongWatchState()
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug: read watchpoint state: %w", err)
|
||||
}
|
||||
if uint64(slot) >= state.DbgInfo&0xff {
|
||||
return fmt.Errorf("debug: slot %d exceeds available watchpoints (%d)", slot, state.DbgInfo&0xff)
|
||||
}
|
||||
|
||||
// DBGWCR: enable + type + size.
|
||||
var wcr uint64 = 1 // enable
|
||||
switch typ {
|
||||
case WatchWrite:
|
||||
wcr |= 1 << 3 // store
|
||||
case WatchRead:
|
||||
wcr |= 3 << 3 // load+store
|
||||
}
|
||||
var sizeBits uint64
|
||||
switch size {
|
||||
case 1:
|
||||
sizeBits = 0
|
||||
case 2:
|
||||
sizeBits = 1
|
||||
case 4:
|
||||
sizeBits = 2
|
||||
case 8:
|
||||
sizeBits = 3
|
||||
}
|
||||
wcr |= sizeBits << 5
|
||||
state.DbgRegs[slot].Addr = addr
|
||||
state.DbgRegs[slot].Mask = 0
|
||||
state.DbgRegs[slot].Ctrl = loongCtrlPLV3Enable | ctrlType | lenBits<<loongLenShift
|
||||
|
||||
if err := ptracePokeUser(s.pid, uintptr(0x1001+slot*8), wcr); err != nil {
|
||||
return fmt.Errorf("debug: set watchpoint control: %w", err)
|
||||
if err := s.setLoongWatchState(state); err != nil {
|
||||
return fmt.Errorf("debug: set watchpoint: %w", err)
|
||||
}
|
||||
|
||||
s.wpSlots[slot] = true
|
||||
@@ -87,14 +133,21 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
||||
}
|
||||
|
||||
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 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)
|
||||
}
|
||||
|
||||
if err := ptracePokeUser(s.pid, uintptr(0x1001+slot*8), 0); err != nil {
|
||||
state, err := s.getLoongWatchState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state.DbgRegs[slot].Addr = 0
|
||||
state.DbgRegs[slot].Mask = 0
|
||||
state.DbgRegs[slot].Ctrl = 0
|
||||
if err := s.setLoongWatchState(state); err != nil {
|
||||
return err
|
||||
}
|
||||
s.wpSlots[slot] = false
|
||||
@@ -102,7 +155,7 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
||||
}
|
||||
|
||||
func (s *Session) ClearAllWatchpoints() error {
|
||||
for slot := 0; slot < maxWatchpoints; slot++ {
|
||||
for slot := range maxWatchpoints() {
|
||||
if s.wpSlots[slot] {
|
||||
if err := s.ClearWatchpoint(slot); err != nil {
|
||||
return err
|
||||
@@ -112,14 +165,37 @@ func (s *Session) ClearAllWatchpoints() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ptracePokeUser(pid int, offset uintptr, val uint64) error {
|
||||
const ptracePokeuser = 6
|
||||
func (s *Session) getLoongWatchState() (*loongWatchState, error) {
|
||||
var state loongWatchState
|
||||
iovec := syscall.Iovec{
|
||||
Base: (*byte)(unsafe.Pointer(&state)),
|
||||
Len: uint64(unsafe.Sizeof(state)),
|
||||
}
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(ptracePokeuser),
|
||||
uintptr(pid),
|
||||
offset,
|
||||
uintptr(val),
|
||||
uintptr(syscall.PTRACE_GETREGSET),
|
||||
uintptr(s.pid),
|
||||
uintptr(ntLoongHWWatch),
|
||||
uintptr(unsafe.Pointer(&iovec)),
|
||||
0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return nil, errno
|
||||
}
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
func (s *Session) setLoongWatchState(state *loongWatchState) error {
|
||||
iovec := syscall.Iovec{
|
||||
Base: (*byte)(unsafe.Pointer(state)),
|
||||
Len: uint64(unsafe.Sizeof(*state)),
|
||||
}
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_SETREGSET),
|
||||
uintptr(s.pid),
|
||||
uintptr(ntLoongHWWatch),
|
||||
uintptr(unsafe.Pointer(&iovec)),
|
||||
0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
@@ -127,18 +203,3 @@ func ptracePokeUser(pid int, offset uintptr, val uint64) error {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -7,11 +7,16 @@ package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Hardware watchpoint support for RISC-V via Sdtrig trigger registers.
|
||||
// Uses PTRACE_POKEUSER/PEEKUSER to access debug registers.
|
||||
// Hardware watchpoints are not reachable through the riscv64 kernel ptrace
|
||||
// interface. arch/riscv/kernel/ptrace.c forwards every POKEUSER/PEEKUSER to
|
||||
// the generic ptrace_request, and the riscv user_regset view contains only
|
||||
// the GPR, FP and vector regsets: there is no debug-register or trigger
|
||||
// regset, and offsets outside the view fail with EIO. The Sdtrig CSRs
|
||||
// (tselect/tdata1/tdata2) are not exposed to ptrace either. Until the
|
||||
// kernel grows a trigger regset, SetWatchpoint reports the fact instead of
|
||||
// poking a window that does not exist.
|
||||
|
||||
// WatchpointType selects what triggers the watchpoint.
|
||||
type WatchpointType int
|
||||
@@ -21,10 +26,18 @@ const (
|
||||
WatchRead WatchpointType = 3
|
||||
)
|
||||
|
||||
const maxWatchpoints = 4
|
||||
// maxWatchpoints reports the number of hardware watchpoint slots the
|
||||
// architecture provides. riscv64 exposes none via ptrace; the bound exists
|
||||
// so the slot bookkeeping stays consistent.
|
||||
func maxWatchpoints() int { return 4 }
|
||||
|
||||
// archWatchpointAddr resolves the address of the watchpoint that fired.
|
||||
// Unreachable in practice (watchpoints cannot be armed), but si_addr names
|
||||
// the accessed address where the kernel does report one.
|
||||
func archWatchpointAddr(s *Session, siAddr uint64) uint64 { return siAddr }
|
||||
|
||||
func (s *Session) FindFreeWatchpointSlot() int {
|
||||
for i := range maxWatchpoints {
|
||||
for i := range maxWatchpoints() {
|
||||
if !s.wpSlots[i] {
|
||||
return i
|
||||
}
|
||||
@@ -33,115 +46,26 @@ func (s *Session) FindFreeWatchpointSlot() int {
|
||||
}
|
||||
|
||||
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
||||
if slot < 0 || slot >= maxWatchpoints {
|
||||
if slot < 0 || slot >= maxWatchpoints() {
|
||||
return false
|
||||
}
|
||||
return s.wpSlots[slot]
|
||||
}
|
||||
|
||||
// SetWatchpoint installs a hardware watchpoint.
|
||||
// SetWatchpoint always fails: the riscv64 kernel ptrace interface has no
|
||||
// hardware-watchpoint access.
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
s.wpSlots[slot] = true
|
||||
return nil
|
||||
return fmt.Errorf("debug: hardware watchpoints are not supported by the riscv64 kernel ptrace interface")
|
||||
}
|
||||
|
||||
// ClearWatchpoint always fails: no watchpoint can ever be armed.
|
||||
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 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)
|
||||
}
|
||||
|
||||
// Disable by clearing tdata1.
|
||||
if err := ptracePokeUser(s.pid, uintptr(0x1001+slot*8), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
s.wpSlots[slot] = false
|
||||
return nil
|
||||
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
||||
}
|
||||
|
||||
func (s *Session) ClearAllWatchpoints() error {
|
||||
for slot := 0; slot < maxWatchpoints; slot++ {
|
||||
if s.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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user