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")
|
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")
|
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")
|
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)
|
fs.Parse(args)
|
||||||
|
|
||||||
// --- Debuggee mode (internal, spawned by the debugger) ---
|
// --- Debuggee mode (internal, spawned by the debugger) ---
|
||||||
@@ -231,6 +231,13 @@ REPL commands:
|
|||||||
if sess.Exited() {
|
if sess.Exited() {
|
||||||
break
|
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()
|
regs, rerr := sess.GetRegs()
|
||||||
if rerr != nil {
|
if rerr != nil {
|
||||||
break
|
break
|
||||||
|
|||||||
+99
-44
@@ -9,11 +9,11 @@ import "strings"
|
|||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
// Breakpoint is one INT3 breakpoint in the debuggee.
|
// Breakpoint is one software breakpoint in the debuggee.
|
||||||
type Breakpoint struct {
|
type Breakpoint struct {
|
||||||
Addr uint64 // absolute address in the debuggee
|
Addr uint64 // absolute address in the debuggee
|
||||||
Label string // source label ("" for raw addresses)
|
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
|
Enabled bool
|
||||||
Cond *Condition // optional condition (nil = unconditional)
|
Cond *Condition // optional condition (nil = unconditional)
|
||||||
hits int
|
hits int
|
||||||
@@ -32,8 +32,12 @@ type Condition struct {
|
|||||||
MemAddr uint64 // memory address (for register-memory comparison, prefixed with *)
|
MemAddr uint64 // memory address (for register-memory comparison, prefixed with *)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eval checks the condition against the current registers.
|
// Eval checks the condition against the current registers. For the
|
||||||
func (c *Condition) Eval(regs *Regs) bool {
|
// 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)
|
actual, ok := regs.RegValue(c.Reg)
|
||||||
if !ok {
|
if !ok {
|
||||||
return true // unknown register, don't block
|
return true // unknown register, don't block
|
||||||
@@ -48,9 +52,16 @@ func (c *Condition) Eval(regs *Regs) bool {
|
|||||||
}
|
}
|
||||||
expected = v
|
expected = v
|
||||||
case c.MemAddr != 0:
|
case c.MemAddr != 0:
|
||||||
// Register-memory comparison, requires a Session, not available here.
|
// Register-memory comparison, resolved in the debuggee at
|
||||||
// Fall back to treating as constant (the caller should resolve).
|
// evaluation time.
|
||||||
expected = c.Value
|
if mem == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
v, ok := mem(c.MemAddr)
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
expected = v
|
||||||
default:
|
default:
|
||||||
expected = c.Value
|
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.
|
// Breakpoints manages the software breakpoints of one Session.
|
||||||
type Breakpoints struct {
|
type Breakpoints struct {
|
||||||
t tracer
|
t tracer
|
||||||
@@ -83,6 +106,18 @@ func NewBreakpoints(t tracer) *Breakpoints {
|
|||||||
return &Breakpoints{t: t, bps: make(map[uint64]*Breakpoint)}
|
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).
|
// Set installs a breakpoint at addr (replaces any existing one).
|
||||||
func (bm *Breakpoints) Set(addr uint64, label string) (*Breakpoint, error) {
|
func (bm *Breakpoints) Set(addr uint64, label string) (*Breakpoint, error) {
|
||||||
return bm.SetWithCond(addr, label, nil)
|
return bm.SetWithCond(addr, label, nil)
|
||||||
@@ -100,13 +135,12 @@ func (bm *Breakpoints) SetWithCond(addr uint64, label string, cond *Condition) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
orig := byte(word)
|
orig := make([]byte, len(breakpointInsn))
|
||||||
// Patch with the breakpoint instruction, preserving the rest of the word.
|
for i := range orig {
|
||||||
mask := uint64(0)
|
orig[i] = byte(word >> (8 * i))
|
||||||
for range breakpointInsn {
|
|
||||||
mask = (mask << 8) | 0xFF
|
|
||||||
}
|
}
|
||||||
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 {
|
if err := bm.t.Poke(addr, patched); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -134,26 +168,40 @@ func (bm *Breakpoints) Info() string {
|
|||||||
}
|
}
|
||||||
cond := ""
|
cond := ""
|
||||||
if bp.Cond != nil {
|
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))
|
result.WriteString(fmt.Sprintf(" %d: %s at %#x [%s, %d hits]%s\n", i, label, bp.Addr, status, bp.hits, cond))
|
||||||
}
|
}
|
||||||
return result.String()
|
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 {
|
func (bm *Breakpoints) Clear(addr uint64) error {
|
||||||
bp, ok := bm.bps[addr]
|
bp, ok := bm.bps[addr]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("debug: no breakpoint at %#x", addr)
|
return fmt.Errorf("debug: no breakpoint at %#x", addr)
|
||||||
}
|
}
|
||||||
word, err := bm.t.Peek(addr)
|
if !bm.restore(addr, bp) {
|
||||||
if err != nil {
|
word, err := bm.t.Peek(addr)
|
||||||
return err
|
if err != nil {
|
||||||
}
|
return err
|
||||||
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
}
|
||||||
if err := bm.t.Poke(addr, restored); err != nil {
|
return fmt.Errorf("debug: restore breakpoint at %#x failed, word is %#x", addr, word)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
delete(bm.bps, addr)
|
delete(bm.bps, addr)
|
||||||
return nil
|
return nil
|
||||||
@@ -185,43 +233,54 @@ func (bm *Breakpoints) All() []*Breakpoint {
|
|||||||
|
|
||||||
// HandleTrap is called after the debuggee stops on SIGTRAP. It checks
|
// HandleTrap is called after the debuggee stops on SIGTRAP. It checks
|
||||||
// whether the trap was caused by one of our breakpoints (PC-adjust matches
|
// 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).
|
// returns the breakpoint that was hit (or nil if it was a single-step).
|
||||||
// Hits returns how many times the breakpoint has been hit.
|
// Hits returns how many times the breakpoint has been hit.
|
||||||
func (bp *Breakpoint) Hits() int { return bp.hits }
|
func (bp *Breakpoint) Hits() int { return bp.hits }
|
||||||
|
|
||||||
func (bm *Breakpoints) HandleTrap(regs *Regs) *Breakpoint {
|
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)
|
trapAddr := regs.GetPC() - uint64(breakpointPCAdjust)
|
||||||
bp, ok := bm.bps[trapAddr]
|
bp, ok := bm.bps[trapAddr]
|
||||||
if !ok || !bp.Enabled {
|
if !ok || !bp.Enabled {
|
||||||
return nil // single-step trap or unknown
|
return nil // single-step trap or unknown
|
||||||
}
|
}
|
||||||
// Check the condition (if any).
|
// Check the condition (if any).
|
||||||
if bp.Cond != nil && !bp.Cond.Eval(regs) {
|
if bp.Cond != nil && !bp.Cond.Eval(regs, bm.peekValue) {
|
||||||
// Condition not met, restore the byte but do NOT rewind RIP.
|
// Condition not met: step the original instruction and re-arm the
|
||||||
// The process continues from the next instruction (past the INT3).
|
// breakpoint, leaving the debuggee stopped just past it, ready to
|
||||||
word, err := bm.t.Peek(trapAddr)
|
// resume silently. The PC must be rewound first: on architectures
|
||||||
if err == nil {
|
// that report the trap past the instruction (amd64) it would
|
||||||
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
// otherwise sit on the second byte of the replaced instruction.
|
||||||
bm.t.Poke(trapAddr, restored)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
bp.hits++
|
bp.hits++
|
||||||
// Restore the original byte.
|
// Restore the original bytes and rewind PC to re-execute them.
|
||||||
word, err := bm.t.Peek(trapAddr)
|
bm.restore(trapAddr, bp)
|
||||||
if err == nil {
|
|
||||||
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
|
||||||
bm.t.Poke(trapAddr, restored)
|
|
||||||
}
|
|
||||||
// Rewind PC to re-execute the original instruction.
|
|
||||||
regs.SetPC(trapAddr)
|
regs.SetPC(trapAddr)
|
||||||
bm.t.SetRegs(regs)
|
bm.t.SetRegs(regs)
|
||||||
return bp
|
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.
|
// 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
|
// Called after Step() when we want the breakpoint to fire again on the
|
||||||
// next Continue().
|
// next Continue().
|
||||||
@@ -234,11 +293,7 @@ func (bm *Breakpoints) Reinsert(addr uint64) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
mask := uint64(0)
|
patched := (word &^ breakpointMask()) | breakpointWord(breakpointInsn)
|
||||||
for range breakpointInsn {
|
|
||||||
mask = (mask << 8) | 0xFF
|
|
||||||
}
|
|
||||||
patched := (word &^ mask) | breakpointWord(breakpointInsn)
|
|
||||||
return bm.t.Poke(addr, patched)
|
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
|
package debug
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -48,7 +47,7 @@ func TestConditionEval(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
got := tt.cond.Eval(regs)
|
got := tt.cond.Eval(regs, nil)
|
||||||
if got != tt.want {
|
if got != tt.want {
|
||||||
t.Errorf("Condition{%q %q %d}.Eval() = %v, want %v",
|
t.Errorf("Condition{%q %q %d}.Eval() = %v, want %v",
|
||||||
tt.cond.Reg, tt.cond.Op, tt.cond.Value, got, tt.want)
|
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) {
|
// TestConditionEvalMem covers the register-memory form: the value is read
|
||||||
lines := []SourceLine{
|
// through the supplied reader, and a missing or failing reader must not
|
||||||
{Offset: 0, Line: 5},
|
// block the breakpoint.
|
||||||
{Offset: 5, Line: 6},
|
func TestConditionEvalMem(t *testing.T) {
|
||||||
{Offset: 10, Line: 7},
|
regs := &Regs{RAX: 7}
|
||||||
{Offset: 15, Line: 8},
|
mem := func(addr uint64) (uint64, bool) {
|
||||||
}
|
if addr == 0x5000 {
|
||||||
|
return 7, true
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty table.
|
eq := Condition{Reg: "rax", Op: "==", MemAddr: 0x5000}
|
||||||
if lineAt(nil, 5) != 0 {
|
if !eq.Eval(regs, mem) {
|
||||||
t.Error("lineAt(nil, 5) should return 0")
|
t.Error("register-memory comparison with matching word should hold")
|
||||||
}
|
}
|
||||||
}
|
ne := Condition{Reg: "rax", Op: "!=", MemAddr: 0x5000}
|
||||||
|
if ne.Eval(regs, mem) {
|
||||||
func TestOffsetForLine(t *testing.T) {
|
t.Error("register-memory comparison with mismatching word should not hold")
|
||||||
lines := []SourceLine{
|
|
||||||
{Offset: 0, Line: 5},
|
|
||||||
{Offset: 5, Line: 6},
|
|
||||||
{Offset: 10, Line: 7},
|
|
||||||
}
|
}
|
||||||
|
bad := Condition{Reg: "rax", Op: "==", MemAddr: 0x6000}
|
||||||
tests := []struct {
|
if !bad.Eval(regs, mem) {
|
||||||
line int
|
t.Error("unreadable memory must not block the breakpoint")
|
||||||
want int
|
|
||||||
}{
|
|
||||||
{5, 0},
|
|
||||||
{6, 5},
|
|
||||||
{7, 10},
|
|
||||||
{99, -1}, // not found
|
|
||||||
{0, -1}, // not found
|
|
||||||
}
|
}
|
||||||
|
noReader := Condition{Reg: "rax", Op: "==", MemAddr: 0x5000}
|
||||||
for _, tt := range tests {
|
if !noReader.Eval(regs, nil) {
|
||||||
got := offsetForLine(lines, tt.line)
|
t.Error("missing memory reader must not block the breakpoint")
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("offsetForLine(lines, %d) = %d, want %d", tt.line, got, tt.want)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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) {
|
func TestWatchpointSlotTracking(t *testing.T) {
|
||||||
s := &Session{} // per-session slots start free
|
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)
|
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()
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
||||||
@@ -44,3 +45,15 @@ func (s *Session) DisassembleN(addr uint64, n int) string {
|
|||||||
}
|
}
|
||||||
return result
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
||||||
@@ -44,3 +45,16 @@ func (s *Session) DisassembleN(addr uint64, n int) string {
|
|||||||
}
|
}
|
||||||
return result
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
||||||
@@ -44,3 +45,17 @@ func (s *Session) DisassembleN(addr uint64, n int) string {
|
|||||||
}
|
}
|
||||||
return result
|
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]
|
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) {
|
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.
|
// archSPLabel returns the SP register name for display.
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
|
|
||||||
package debug
|
package debug
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
func printRegs(regs *Regs, codeBase, funcOff uint64) {
|
func printRegs(regs *Regs, codeBase, funcOff uint64) {
|
||||||
fmt.Printf(" PC = %#016x (func+%#x)\n", regs.PC, regs.PC-codeBase-funcOff)
|
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) {
|
func printVectorRegs(v *VectorRegs) {
|
||||||
fmt.Println("\n Vector registers (V0-V31):")
|
fmt.Println("\n Vector registers (V0-V31):")
|
||||||
for i := 0; i < 32; i += 2 {
|
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, 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, v.V[i+1][8], v.V[i+1][0])
|
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
|
stopped bool
|
||||||
exited bool
|
exited bool
|
||||||
codeBase uint64 // base address of the JIT code in the debuggee
|
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
|
// 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)
|
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")
|
readyFile := filepath.Join(tmpDir, "ready")
|
||||||
for range 500 {
|
for range 500 {
|
||||||
@@ -125,29 +131,17 @@ func LaunchWithBuffers(gasmBin, asmPath, funcName string, args []byte, bufSpec s
|
|||||||
return s, bufAddrs, nil
|
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
|
// waitStopped consumes ptrace-stop events until one the debugger cares
|
||||||
// about arrives: SIGTRAP (a breakpoint or a completed single-step) or the
|
// about arrives: SIGTRAP (a breakpoint or a completed single-step), the
|
||||||
// debuggee's own SIGSTOP. A Go tracee's runtime raises SIGURG for
|
// debuggee's own SIGSTOP, or a genuine signal-delivery-stop. A Go tracee's
|
||||||
// asynchronous preemption, and every signal on a traced thread surfaces as
|
// runtime raises SIGURG for asynchronous preemption, and every signal on a
|
||||||
// a signal-delivery-stop, so those are suppressed and the tracee resumed
|
// traced thread surfaces as a signal-delivery-stop, so SIGURG is suppressed
|
||||||
// without them. Runtime noise is why a single wait can return in the
|
// and the tracee resumed without it. Every other signal (SIGSEGV, SIGBUS,
|
||||||
// middle of runtime code and a resume can then fail: the event stream must
|
// SIGFPE, SIGILL, ...) is returned to the caller: resuming with signal 0
|
||||||
// be drained by the tracer.
|
// 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) {
|
func (s *Session) waitStopped() (syscall.Signal, error) {
|
||||||
for {
|
for {
|
||||||
var ws syscall.WaitStatus
|
var ws syscall.WaitStatus
|
||||||
@@ -165,10 +159,12 @@ func (s *Session) waitStopped() (syscall.Signal, error) {
|
|||||||
switch sig := ws.StopSignal(); sig {
|
switch sig := ws.StopSignal(); sig {
|
||||||
case syscall.SIGTRAP, syscall.SIGSTOP:
|
case syscall.SIGTRAP, syscall.SIGSTOP:
|
||||||
s.stopped = true
|
s.stopped = true
|
||||||
|
s.lastSignal = 0
|
||||||
return sig, nil
|
return sig, nil
|
||||||
default:
|
case syscall.SIGURG:
|
||||||
// Runtime noise (SIGURG preemption and friends): resume the
|
// Go runtime asynchronous preemption: resume the tracee
|
||||||
// tracee without delivering the signal.
|
// without delivering the signal.
|
||||||
|
s.lastSignal = 0
|
||||||
if _, _, errno := syscall.Syscall6(
|
if _, _, errno := syscall.Syscall6(
|
||||||
syscall.SYS_PTRACE,
|
syscall.SYS_PTRACE,
|
||||||
uintptr(syscall.PTRACE_CONT),
|
uintptr(syscall.PTRACE_CONT),
|
||||||
@@ -177,10 +173,22 @@ func (s *Session) waitStopped() (syscall.Signal, error) {
|
|||||||
); errno != 0 {
|
); errno != 0 {
|
||||||
return 0, fmt.Errorf("debug: PTRACE_CONT: %w", errno)
|
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.
|
// Peek reads a word (8 bytes) from the debuggee's memory at addr.
|
||||||
func (s *Session) Peek(addr uint64) (uint64, error) {
|
func (s *Session) Peek(addr uint64) (uint64, error) {
|
||||||
mem, err := os.OpenFile(fmt.Sprintf("/proc/%d/mem", s.pid), os.O_RDONLY, 0)
|
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.
|
// CodeBase returns the base address of the JIT code in the debuggee.
|
||||||
func (s *Session) CodeBase() uint64 { return s.codeBase }
|
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() {
|
func (s *Session) Kill() {
|
||||||
if !s.exited {
|
if !s.exited {
|
||||||
syscall.Kill(s.pid, syscall.SIGKILL)
|
syscall.Kill(s.pid, syscall.SIGKILL)
|
||||||
@@ -304,6 +313,35 @@ func (s *Session) Kill() {
|
|||||||
if s.cmd != nil && s.cmd.Process != nil {
|
if s.cmd != nil && s.cmd.Process != nil {
|
||||||
s.cmd.Wait()
|
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
|
// findRWXMapping reads /proc/pid/maps and returns the base address of the
|
||||||
|
|||||||
+68
-11
@@ -6,6 +6,7 @@
|
|||||||
package debug
|
package debug
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
@@ -44,20 +45,24 @@ func (s *Session) SetRegs(regs *Regs) error {
|
|||||||
return nil
|
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 {
|
type FPRegs struct {
|
||||||
FCW uint16
|
FCW uint16
|
||||||
FSW uint16
|
FSW uint16
|
||||||
FTW byte
|
FTW uint16
|
||||||
FOP uint16
|
FOP uint16
|
||||||
FIP uint64
|
FIP uint64
|
||||||
FCS uint16
|
|
||||||
FDP uint64
|
FDP uint64
|
||||||
FDS uint16
|
|
||||||
MXCSR uint32
|
MXCSR uint32
|
||||||
MXCSRMask uint32
|
MXCSRMask uint32
|
||||||
ST [8][16]byte // x87 stack (10 bytes per reg, padded to 16)
|
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.
|
// 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)
|
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) {
|
func (s *Session) GetVectorRegs() (VectorRegs, error) {
|
||||||
var v VectorRegs
|
var v VectorRegs
|
||||||
fp, err := s.GetFPRegs()
|
buf := make([]byte, xstateMaxBuffer)
|
||||||
if err != nil {
|
iovec := syscall.Iovec{
|
||||||
return v, err
|
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 i := range 16 {
|
||||||
for j := range 16 {
|
copy(v.YMM[i][:16], buf[xsaveXMMOffset+16*i:xsaveXMMOffset+16*i+16])
|
||||||
v.YMM[i][j] = fp.XMM[i][j]
|
}
|
||||||
|
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
|
return v, nil
|
||||||
|
|||||||
@@ -91,5 +91,9 @@ func (r *Regs) RegValue(name string) (uint64, bool) {
|
|||||||
// breakpointInsn is the software breakpoint instruction.
|
// breakpointInsn is the software breakpoint instruction.
|
||||||
var breakpointInsn = []byte{0xCC} // INT3
|
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
|
const breakpointPCAdjust = 1
|
||||||
|
|||||||
@@ -130,5 +130,11 @@ func (r *Regs) RegValue(name string) (uint64, bool) {
|
|||||||
// breakpointInsn is the software breakpoint instruction (BRK #0).
|
// breakpointInsn is the software breakpoint instruction (BRK #0).
|
||||||
var breakpointInsn = []byte{0x00, 0x00, 0x20, 0xD4} // BRK #0
|
var breakpointInsn = []byte{0x00, 0x00, 0x20, 0xD4} // BRK #0
|
||||||
|
|
||||||
// breakpointPCAdjust is how far PC is past the breakpoint instruction after a trap.
|
// breakpointPCAdjust is how far PC is past the breakpoint instruction after
|
||||||
const breakpointPCAdjust = 4
|
// 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).
|
// breakpointInsn is the software breakpoint instruction (BRK $0).
|
||||||
var breakpointInsn = []byte{0x05, 0x00, 0x2a, 0x00} // break 0
|
var breakpointInsn = []byte{0x05, 0x00, 0x2a, 0x00} // break 0
|
||||||
|
|
||||||
// breakpointPCAdjust is how far PC is past the breakpoint instruction after a trap.
|
// breakpointPCAdjust is how far PC is past the breakpoint instruction after
|
||||||
const breakpointPCAdjust = 4
|
// 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).
|
// breakpointInsn is the software breakpoint instruction (EBREAK).
|
||||||
var breakpointInsn = []byte{0x73, 0x00, 0x10, 0x00} // ebreak
|
var breakpointInsn = []byte{0x73, 0x00, 0x10, 0x00} // ebreak
|
||||||
|
|
||||||
// breakpointPCAdjust is how far PC is past the breakpoint instruction after a trap.
|
// breakpointPCAdjust is how far PC is past the breakpoint instruction after
|
||||||
const breakpointPCAdjust = 4
|
// 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 (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"cmp"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"sort"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -93,7 +94,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
|||||||
regs, _ := s.GetRegs()
|
regs, _ := s.GetRegs()
|
||||||
pc := regs.GetPC()
|
pc := regs.GetPC()
|
||||||
text, instLen, _ := s.Disassemble(pc)
|
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)
|
afterAddr := pc + uint64(instLen)
|
||||||
_, err := bm.Set(afterAddr, "(next)")
|
_, err := bm.Set(afterAddr, "(next)")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -108,6 +109,21 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
|||||||
bm.Clear(afterAddr)
|
bm.Clear(afterAddr)
|
||||||
continue
|
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.HandleTrap(®s)
|
||||||
bm.Clear(afterAddr)
|
bm.Clear(afterAddr)
|
||||||
} else {
|
} else {
|
||||||
@@ -143,9 +159,21 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
|||||||
bm.Clear(retAddr)
|
bm.Clear(retAddr)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !s.Exited() {
|
if s.Exited() {
|
||||||
bm.HandleTrap(®s)
|
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)
|
bm.Clear(retAddr)
|
||||||
if s.Exited() {
|
if s.Exited() {
|
||||||
fmt.Println("debuggee exited")
|
fmt.Println("debuggee exited")
|
||||||
@@ -171,6 +199,15 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
|||||||
fmt.Println("debuggee exited")
|
fmt.Println("debuggee exited")
|
||||||
break
|
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()
|
reason, wpAddr := s.StopInfo()
|
||||||
if reason == StopWatchpoint {
|
if reason == StopWatchpoint {
|
||||||
fmt.Printf("watchpoint hit at %#x\n", wpAddr)
|
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])
|
reg := strings.ToLower(parts[3])
|
||||||
op := parts[4]
|
op := parts[4]
|
||||||
operand := parts[5]
|
operand := parts[5]
|
||||||
if val, err := strconv.ParseUint(operand, 0, 64); err == nil {
|
switch {
|
||||||
cond = &Condition{Reg: reg, Op: op, Value: val}
|
case strings.HasPrefix(operand, "*"):
|
||||||
} else {
|
// Memory operand: compare against the 8-byte word at
|
||||||
cond = &Condition{Reg: reg, Op: op, Reg2: strings.ToLower(operand)}
|
// 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" {
|
} 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
|
continue
|
||||||
}
|
}
|
||||||
bp, err := bm.SetWithCond(addr, label, cond)
|
bp, err := bm.SetWithCond(addr, label, cond)
|
||||||
@@ -237,7 +288,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
|||||||
}
|
}
|
||||||
condStr := ""
|
condStr := ""
|
||||||
if cond != nil {
|
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)
|
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)
|
addr, _ = resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
||||||
}
|
}
|
||||||
if len(parts) > 2 {
|
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)
|
mem, err := s.ReadMemory(addr, length)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -336,10 +391,8 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "labels", "l":
|
case "labels", "l":
|
||||||
sorted := make([]Label, len(labels))
|
slices.SortFunc(labels, func(a, b Label) int { return cmp.Compare(a.Offset, b.Offset) })
|
||||||
copy(sorted, labels)
|
for _, l := range labels {
|
||||||
sort.Slice(sorted, func(i, j int) bool { return sorted[i].Offset < sorted[j].Offset })
|
|
||||||
for _, l := range sorted {
|
|
||||||
fmt.Printf(" func+%#04x %s\n", l.Offset, l.Name)
|
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()
|
fmt.Println()
|
||||||
|
|
||||||
case "help", "h", "?":
|
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
|
delete <label|addr> remove a breakpoint
|
||||||
info break list all breakpoints
|
info break list all breakpoints
|
||||||
watch <addr> [r|w] [size] set a hardware watchpoint (write by default)
|
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":
|
case "unwatch":
|
||||||
if len(parts) >= 2 {
|
if len(parts) >= 2 {
|
||||||
slot, err := strconv.Atoi(parts[1])
|
slot, err := strconv.Atoi(parts[1])
|
||||||
if err != nil || slot < 0 || slot > 3 {
|
if err != nil || slot < 0 || slot >= maxWatchpoints() {
|
||||||
fmt.Println("usage: unwatch [<slot>]")
|
fmt.Printf("usage: unwatch [<slot 0-%d>]\n", maxWatchpoints()-1)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := s.ClearWatchpoint(slot); err != nil {
|
if err := s.ClearWatchpoint(slot); err != nil {
|
||||||
|
|||||||
+10
-2
@@ -6,6 +6,7 @@
|
|||||||
package debug
|
package debug
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
@@ -61,8 +62,15 @@ func (s *Session) StopInfo() (StopReason, uint64) {
|
|||||||
case trapBRKPT:
|
case trapBRKPT:
|
||||||
return StopBreakpoint, 0
|
return StopBreakpoint, 0
|
||||||
case trapHWBRKPT:
|
case trapHWBRKPT:
|
||||||
addr := *(*uint64)(unsafe.Add(unsafe.Pointer(&info), 16))
|
// si_addr sits at struct offset 16 (12 bytes of signo/errno/code
|
||||||
return StopWatchpoint, addr
|
// 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:
|
default:
|
||||||
return StopSingleStep, 0
|
return StopSingleStep, 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,15 +28,7 @@ func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
|||||||
return fmt.Errorf("debug target: parse: %v", errs[0])
|
return fmt.Errorf("debug target: parse: %v", errs[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
var img *asm.Image
|
img, err := asm.AssembleFileARM64(file)
|
||||||
switch "arm64" {
|
|
||||||
case "arm64":
|
|
||||||
img, err = asm.AssembleFileARM64(file)
|
|
||||||
case "riscv64":
|
|
||||||
img, err = asm.AssembleFileRISCV(file)
|
|
||||||
case "loong64":
|
|
||||||
img, err = asm.AssembleFileLOONG64(file)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("debug target: assemble: %w", err)
|
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])
|
return fmt.Errorf("debug target: parse: %v", errs[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
var img *asm.Image
|
img, err := asm.AssembleFileLOONG64(file)
|
||||||
switch "loong64" {
|
|
||||||
case "arm64":
|
|
||||||
img, err = asm.AssembleFileARM64(file)
|
|
||||||
case "riscv64":
|
|
||||||
img, err = asm.AssembleFileRISCV(file)
|
|
||||||
case "loong64":
|
|
||||||
img, err = asm.AssembleFileLOONG64(file)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("debug target: assemble: %w", err)
|
return fmt.Errorf("debug target: assemble: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -12,10 +12,11 @@ type tracer interface {
|
|||||||
Peek(addr uint64) (uint64, error)
|
Peek(addr uint64) (uint64, error)
|
||||||
Poke(addr uint64, val uint64) error
|
Poke(addr uint64, val uint64) error
|
||||||
SetRegs(regs *Regs) error
|
SetRegs(regs *Regs) error
|
||||||
|
Step() error
|
||||||
Pid() int
|
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 {
|
type mockTracer struct {
|
||||||
mem map[uint64]byte
|
mem map[uint64]byte
|
||||||
peeks []uint64
|
peeks []uint64
|
||||||
@@ -23,7 +24,8 @@ type mockTracer struct {
|
|||||||
addr uint64
|
addr uint64
|
||||||
val uint64
|
val uint64
|
||||||
}
|
}
|
||||||
regs *Regs
|
steps int
|
||||||
|
regs *Regs
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMockTracer() *mockTracer {
|
func newMockTracer() *mockTracer {
|
||||||
@@ -57,4 +59,8 @@ func (m *mockTracer) SetRegs(regs *Regs) error {
|
|||||||
m.regs = regs
|
m.regs = regs
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
func (m *mockTracer) Step() error {
|
||||||
|
m.steps++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
func (m *mockTracer) Pid() int { return 42 }
|
func (m *mockTracer) Pid() int { return 42 }
|
||||||
|
|||||||
@@ -8,10 +8,48 @@ package debug
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Hardware watchpoint support via x86-64 debug registers (DR0-DR3, DR7).
|
// 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.
|
// WatchpointType selects what triggers the watchpoint.
|
||||||
type WatchpointType int
|
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")
|
return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8")
|
||||||
}
|
}
|
||||||
|
|
||||||
var drAddr uintptr
|
if err := ptracePokeUser(s.pid, drOffset+uintptr(slot*8), addr); err != nil {
|
||||||
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 {
|
|
||||||
return fmt.Errorf("debug: set DR%d: %w", slot, err)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("debug: read DR7: %w", err)
|
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)))
|
mask := ^((uint64(1) << (2 * slot)) | (uint64(3) << (16 + 4*slot)) | (uint64(3) << (18 + 4*slot)))
|
||||||
dr7 = (dr7 & mask) | enableBit | rwBits | lenField
|
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)
|
return fmt.Errorf("debug: set DR7: %w", err)
|
||||||
}
|
}
|
||||||
s.wpSlots[slot] = true
|
s.wpSlots[slot] = true
|
||||||
@@ -105,12 +131,12 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
if !s.wpSlots[slot] {
|
if !s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d is not in use", 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dr7 &^= uint64(1) << (2 * slot)
|
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
|
return err
|
||||||
}
|
}
|
||||||
s.wpSlots[slot] = false
|
s.wpSlots[slot] = false
|
||||||
@@ -119,7 +145,7 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
|
|
||||||
// ClearAllWatchpoints removes all hardware watchpoints.
|
// ClearAllWatchpoints removes all hardware watchpoints.
|
||||||
func (s *Session) ClearAllWatchpoints() error {
|
func (s *Session) ClearAllWatchpoints() error {
|
||||||
for slot := range 4 {
|
for slot := range maxWatchpoints() {
|
||||||
if s.wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
if err := s.ClearWatchpoint(slot); err != nil {
|
if err := s.ClearWatchpoint(slot); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -146,16 +172,21 @@ func ptracePokeUser(pid int, offset uintptr, val uint64) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ptracePeekUser(pid int, offset uintptr) (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
|
const ptracePeekuser = 3
|
||||||
val, _, errno := syscall.Syscall6(
|
var word uint64
|
||||||
|
_, _, errno := syscall.Syscall6(
|
||||||
syscall.SYS_PTRACE,
|
syscall.SYS_PTRACE,
|
||||||
uintptr(ptracePeekuser),
|
uintptr(ptracePeekuser),
|
||||||
uintptr(pid),
|
uintptr(pid),
|
||||||
offset,
|
offset,
|
||||||
0, 0, 0,
|
uintptr(unsafe.Pointer(&word)),
|
||||||
|
0, 0,
|
||||||
)
|
)
|
||||||
if errno != 0 {
|
if errno != 0 {
|
||||||
return 0, errno
|
return 0, errno
|
||||||
}
|
}
|
||||||
return uint64(val), nil
|
return word, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Hardware watchpoint support via arm64 debug registers (DBGWVR/DBGWCR).
|
// 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.
|
// WatchpointType selects what triggers the watchpoint.
|
||||||
type WatchpointType int
|
type WatchpointType int
|
||||||
@@ -22,26 +22,35 @@ const (
|
|||||||
WatchRead WatchpointType = 3
|
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.
|
// hwWatchState mirrors the kernel's struct user_hwdebug_state.
|
||||||
type hwBreakState struct {
|
type hwWatchState struct {
|
||||||
DbgInfo uint32
|
DbgInfo uint32
|
||||||
_pad [4]byte
|
_pad [4]byte
|
||||||
DbgRegs [16]hwBreakReg
|
DbgRegs [16]hwWatchReg
|
||||||
}
|
}
|
||||||
|
|
||||||
type hwBreakReg struct {
|
type hwWatchReg struct {
|
||||||
Addr uint64
|
Addr uint64
|
||||||
Ctrl uint64
|
Ctrl uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
// ntArmHWWatch is NT_ARM_HW_WATCH (0x403), the watchpoint regset
|
||||||
ntArmHWBreak = 0x403 // NT_ARM_HW_BREAK
|
// (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 {
|
func (s *Session) FindFreeWatchpointSlot() int {
|
||||||
for i := range maxWatchpoints {
|
for i := range maxWatchpoints() {
|
||||||
if !s.wpSlots[i] {
|
if !s.wpSlots[i] {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
@@ -50,7 +59,7 @@ func (s *Session) FindFreeWatchpointSlot() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return s.wpSlots[slot]
|
return s.wpSlots[slot]
|
||||||
@@ -58,27 +67,31 @@ func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
|||||||
|
|
||||||
// SetWatchpoint installs a hardware watchpoint on the given address.
|
// SetWatchpoint installs a hardware watchpoint on the given address.
|
||||||
func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error {
|
func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error {
|
||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints() {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints()-1)
|
||||||
}
|
}
|
||||||
if s.wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
state, err := s.getHWBreakState()
|
state, err := s.getHWWatchState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("debug: read watchpoint state: %w", err)
|
return fmt.Errorf("debug: read watchpoint state: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if uint32(slot) >= state.DbgInfo {
|
// MDSCR_EL1 packs (debug_arch << 8) | num_slots into dbg_info, so only
|
||||||
return fmt.Errorf("debug: slot %d exceeds available watchpoints (%d)", slot, state.DbgInfo)
|
// 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
|
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
|
ctrl := uint64(1) // enable
|
||||||
switch typ {
|
switch typ {
|
||||||
case WatchWrite:
|
case WatchWrite:
|
||||||
ctrl |= 1 << 3 // store only
|
ctrl |= 2 << 3 // store only
|
||||||
case WatchRead:
|
case WatchRead:
|
||||||
ctrl |= 3 << 3 // load+store
|
ctrl |= 3 << 3 // load+store
|
||||||
}
|
}
|
||||||
@@ -98,7 +111,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
|||||||
ctrl |= bas << 5
|
ctrl |= bas << 5
|
||||||
state.DbgRegs[slot].Ctrl = ctrl
|
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)
|
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 {
|
func (s *Session) ClearWatchpoint(slot int) error {
|
||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints() {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints()-1)
|
||||||
}
|
}
|
||||||
if !s.wpSlots[slot] {
|
if !s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
state, err := s.getHWBreakState()
|
state, err := s.getHWWatchState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
state.DbgRegs[slot].Addr = 0
|
state.DbgRegs[slot].Addr = 0
|
||||||
state.DbgRegs[slot].Ctrl = 0
|
state.DbgRegs[slot].Ctrl = 0
|
||||||
if err := s.setHWBreakState(state); err != nil {
|
if err := s.setHWWatchState(state); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.wpSlots[slot] = false
|
s.wpSlots[slot] = false
|
||||||
@@ -128,7 +141,7 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) ClearAllWatchpoints() error {
|
func (s *Session) ClearAllWatchpoints() error {
|
||||||
for slot := 0; slot < maxWatchpoints; slot++ {
|
for slot := range maxWatchpoints() {
|
||||||
if s.wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
if err := s.ClearWatchpoint(slot); err != nil {
|
if err := s.ClearWatchpoint(slot); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -138,8 +151,8 @@ func (s *Session) ClearAllWatchpoints() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) getHWBreakState() (*hwBreakState, error) {
|
func (s *Session) getHWWatchState() (*hwWatchState, error) {
|
||||||
var state hwBreakState
|
var state hwWatchState
|
||||||
iovec := syscall.Iovec{
|
iovec := syscall.Iovec{
|
||||||
Base: (*byte)(unsafe.Pointer(&state)),
|
Base: (*byte)(unsafe.Pointer(&state)),
|
||||||
Len: uint64(unsafe.Sizeof(state)),
|
Len: uint64(unsafe.Sizeof(state)),
|
||||||
@@ -148,7 +161,7 @@ func (s *Session) getHWBreakState() (*hwBreakState, error) {
|
|||||||
syscall.SYS_PTRACE,
|
syscall.SYS_PTRACE,
|
||||||
uintptr(syscall.PTRACE_GETREGSET),
|
uintptr(syscall.PTRACE_GETREGSET),
|
||||||
uintptr(s.pid),
|
uintptr(s.pid),
|
||||||
uintptr(ntArmHWBreak),
|
uintptr(ntArmHWWatch),
|
||||||
uintptr(unsafe.Pointer(&iovec)),
|
uintptr(unsafe.Pointer(&iovec)),
|
||||||
0, 0,
|
0, 0,
|
||||||
)
|
)
|
||||||
@@ -158,7 +171,7 @@ func (s *Session) getHWBreakState() (*hwBreakState, error) {
|
|||||||
return &state, nil
|
return &state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) setHWBreakState(state *hwBreakState) error {
|
func (s *Session) setHWWatchState(state *hwWatchState) error {
|
||||||
iovec := syscall.Iovec{
|
iovec := syscall.Iovec{
|
||||||
Base: (*byte)(unsafe.Pointer(state)),
|
Base: (*byte)(unsafe.Pointer(state)),
|
||||||
Len: uint64(unsafe.Sizeof(*state)),
|
Len: uint64(unsafe.Sizeof(*state)),
|
||||||
@@ -167,7 +180,7 @@ func (s *Session) setHWBreakState(state *hwBreakState) error {
|
|||||||
syscall.SYS_PTRACE,
|
syscall.SYS_PTRACE,
|
||||||
uintptr(syscall.PTRACE_SETREGSET),
|
uintptr(syscall.PTRACE_SETREGSET),
|
||||||
uintptr(s.pid),
|
uintptr(s.pid),
|
||||||
uintptr(ntArmHWBreak),
|
uintptr(ntArmHWWatch),
|
||||||
uintptr(unsafe.Pointer(&iovec)),
|
uintptr(unsafe.Pointer(&iovec)),
|
||||||
0, 0,
|
0, 0,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,10 +8,47 @@ package debug
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Hardware watchpoint support for LoongArch via debug registers.
|
// Hardware watchpoint support via the NT_LOONGARCH_HW_WATCH regset.
|
||||||
// Uses PTRACE_POKEUSER/PEEKUSER to access HW watchpoint registers.
|
//
|
||||||
|
// 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.
|
// WatchpointType selects what triggers the watchpoint.
|
||||||
type WatchpointType int
|
type WatchpointType int
|
||||||
@@ -21,10 +58,16 @@ const (
|
|||||||
WatchRead WatchpointType = 3
|
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 {
|
func (s *Session) FindFreeWatchpointSlot() int {
|
||||||
for i := range maxWatchpoints {
|
for i := range maxWatchpoints() {
|
||||||
if !s.wpSlots[i] {
|
if !s.wpSlots[i] {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
@@ -33,53 +76,56 @@ func (s *Session) FindFreeWatchpointSlot() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return s.wpSlots[slot]
|
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 {
|
func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error {
|
||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints() {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints()-1)
|
||||||
}
|
}
|
||||||
if s.wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d already in use", 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")
|
return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8")
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoongArch debug registers: DBGWVR (watchpoint value) and DBGWCR (watchpoint control).
|
state, err := s.getLoongWatchState()
|
||||||
// Accessed via PTRACE_POKEUSER at architecture-specific offsets.
|
if err != nil {
|
||||||
if err := ptracePokeUser(s.pid, uintptr(0x1000+slot*8), addr); err != nil {
|
return fmt.Errorf("debug: read watchpoint state: %w", err)
|
||||||
return fmt.Errorf("debug: set watchpoint address: %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.
|
state.DbgRegs[slot].Addr = addr
|
||||||
var wcr uint64 = 1 // enable
|
state.DbgRegs[slot].Mask = 0
|
||||||
switch typ {
|
state.DbgRegs[slot].Ctrl = loongCtrlPLV3Enable | ctrlType | lenBits<<loongLenShift
|
||||||
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
|
|
||||||
|
|
||||||
if err := ptracePokeUser(s.pid, uintptr(0x1001+slot*8), wcr); err != nil {
|
if err := s.setLoongWatchState(state); err != nil {
|
||||||
return fmt.Errorf("debug: set watchpoint control: %w", err)
|
return fmt.Errorf("debug: set watchpoint: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.wpSlots[slot] = true
|
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 {
|
func (s *Session) ClearWatchpoint(slot int) error {
|
||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints() {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints()-1)
|
||||||
}
|
}
|
||||||
if !s.wpSlots[slot] {
|
if !s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d is not in use", 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
|
return err
|
||||||
}
|
}
|
||||||
s.wpSlots[slot] = false
|
s.wpSlots[slot] = false
|
||||||
@@ -102,7 +155,7 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) ClearAllWatchpoints() error {
|
func (s *Session) ClearAllWatchpoints() error {
|
||||||
for slot := 0; slot < maxWatchpoints; slot++ {
|
for slot := range maxWatchpoints() {
|
||||||
if s.wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
if err := s.ClearWatchpoint(slot); err != nil {
|
if err := s.ClearWatchpoint(slot); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -112,14 +165,37 @@ func (s *Session) ClearAllWatchpoints() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ptracePokeUser(pid int, offset uintptr, val uint64) error {
|
func (s *Session) getLoongWatchState() (*loongWatchState, error) {
|
||||||
const ptracePokeuser = 6
|
var state loongWatchState
|
||||||
|
iovec := syscall.Iovec{
|
||||||
|
Base: (*byte)(unsafe.Pointer(&state)),
|
||||||
|
Len: uint64(unsafe.Sizeof(state)),
|
||||||
|
}
|
||||||
_, _, errno := syscall.Syscall6(
|
_, _, errno := syscall.Syscall6(
|
||||||
syscall.SYS_PTRACE,
|
syscall.SYS_PTRACE,
|
||||||
uintptr(ptracePokeuser),
|
uintptr(syscall.PTRACE_GETREGSET),
|
||||||
uintptr(pid),
|
uintptr(s.pid),
|
||||||
offset,
|
uintptr(ntLoongHWWatch),
|
||||||
uintptr(val),
|
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,
|
0, 0,
|
||||||
)
|
)
|
||||||
if errno != 0 {
|
if errno != 0 {
|
||||||
@@ -127,18 +203,3 @@ func ptracePokeUser(pid int, offset uintptr, val uint64) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"syscall"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Hardware watchpoint support for RISC-V via Sdtrig trigger registers.
|
// Hardware watchpoints are not reachable through the riscv64 kernel ptrace
|
||||||
// Uses PTRACE_POKEUSER/PEEKUSER to access debug registers.
|
// 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.
|
// WatchpointType selects what triggers the watchpoint.
|
||||||
type WatchpointType int
|
type WatchpointType int
|
||||||
@@ -21,10 +26,18 @@ const (
|
|||||||
WatchRead WatchpointType = 3
|
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 {
|
func (s *Session) FindFreeWatchpointSlot() int {
|
||||||
for i := range maxWatchpoints {
|
for i := range maxWatchpoints() {
|
||||||
if !s.wpSlots[i] {
|
if !s.wpSlots[i] {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
@@ -33,115 +46,26 @@ func (s *Session) FindFreeWatchpointSlot() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return s.wpSlots[slot]
|
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 {
|
func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error {
|
||||||
if slot < 0 || slot >= maxWatchpoints {
|
return fmt.Errorf("debug: hardware watchpoints are not supported by the riscv64 kernel ptrace interface")
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearWatchpoint always fails: no watchpoint can ever be armed.
|
||||||
func (s *Session) ClearWatchpoint(slot int) error {
|
func (s *Session) ClearWatchpoint(slot int) error {
|
||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints() {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
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)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) ClearAllWatchpoints() error {
|
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
|
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