fix(debug): hardware watchpoints, signal stops and breakpoint restore
Assisted-by: GLM 5.3
This commit is contained in:
+99
-44
@@ -9,11 +9,11 @@ import "strings"
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Breakpoint is one INT3 breakpoint in the debuggee.
|
||||
// Breakpoint is one software breakpoint in the debuggee.
|
||||
type Breakpoint struct {
|
||||
Addr uint64 // absolute address in the debuggee
|
||||
Label string // source label ("" for raw addresses)
|
||||
Orig byte // original byte at Addr (restored on removal)
|
||||
Orig []byte // original bytes at Addr (restored on removal)
|
||||
Enabled bool
|
||||
Cond *Condition // optional condition (nil = unconditional)
|
||||
hits int
|
||||
@@ -32,8 +32,12 @@ type Condition struct {
|
||||
MemAddr uint64 // memory address (for register-memory comparison, prefixed with *)
|
||||
}
|
||||
|
||||
// Eval checks the condition against the current registers.
|
||||
func (c *Condition) Eval(regs *Regs) bool {
|
||||
// Eval checks the condition against the current registers. For the
|
||||
// register-memory form, mem reads an 8-byte little-endian word from the
|
||||
// debuggee; it may be nil when no reader is available. Anything that cannot
|
||||
// be decided (unknown register or operator, unreadable memory) does not
|
||||
// block the breakpoint.
|
||||
func (c *Condition) Eval(regs *Regs, mem func(addr uint64) (uint64, bool)) bool {
|
||||
actual, ok := regs.RegValue(c.Reg)
|
||||
if !ok {
|
||||
return true // unknown register, don't block
|
||||
@@ -48,9 +52,16 @@ func (c *Condition) Eval(regs *Regs) bool {
|
||||
}
|
||||
expected = v
|
||||
case c.MemAddr != 0:
|
||||
// Register-memory comparison, requires a Session, not available here.
|
||||
// Fall back to treating as constant (the caller should resolve).
|
||||
expected = c.Value
|
||||
// Register-memory comparison, resolved in the debuggee at
|
||||
// evaluation time.
|
||||
if mem == nil {
|
||||
return true
|
||||
}
|
||||
v, ok := mem(c.MemAddr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
expected = v
|
||||
default:
|
||||
expected = c.Value
|
||||
}
|
||||
@@ -72,6 +83,18 @@ func (c *Condition) Eval(regs *Regs) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// String renders the condition for display.
|
||||
func (c *Condition) String() string {
|
||||
switch {
|
||||
case c.Reg2 != "":
|
||||
return fmt.Sprintf("%s %s %s", c.Reg, c.Op, c.Reg2)
|
||||
case c.MemAddr != 0:
|
||||
return fmt.Sprintf("%s %s *%#x", c.Reg, c.Op, c.MemAddr)
|
||||
default:
|
||||
return fmt.Sprintf("%s %s %#x", c.Reg, c.Op, c.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// Breakpoints manages the software breakpoints of one Session.
|
||||
type Breakpoints struct {
|
||||
t tracer
|
||||
@@ -83,6 +106,18 @@ func NewBreakpoints(t tracer) *Breakpoints {
|
||||
return &Breakpoints{t: t, bps: make(map[uint64]*Breakpoint)}
|
||||
}
|
||||
|
||||
// breakpointMask is the byte mask of the breakpoint instruction inside a
|
||||
// peeked word: the low len(breakpointInsn) bytes, because every supported
|
||||
// architecture is little-endian and patches the instruction at the lowest
|
||||
// address of the word.
|
||||
func breakpointMask() uint64 {
|
||||
var mask uint64
|
||||
for range breakpointInsn {
|
||||
mask = (mask << 8) | 0xFF
|
||||
}
|
||||
return mask
|
||||
}
|
||||
|
||||
// Set installs a breakpoint at addr (replaces any existing one).
|
||||
func (bm *Breakpoints) Set(addr uint64, label string) (*Breakpoint, error) {
|
||||
return bm.SetWithCond(addr, label, nil)
|
||||
@@ -100,13 +135,12 @@ func (bm *Breakpoints) SetWithCond(addr uint64, label string, cond *Condition) (
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orig := byte(word)
|
||||
// Patch with the breakpoint instruction, preserving the rest of the word.
|
||||
mask := uint64(0)
|
||||
for range breakpointInsn {
|
||||
mask = (mask << 8) | 0xFF
|
||||
orig := make([]byte, len(breakpointInsn))
|
||||
for i := range orig {
|
||||
orig[i] = byte(word >> (8 * i))
|
||||
}
|
||||
patched := (word &^ mask) | breakpointWord(breakpointInsn)
|
||||
// Patch with the breakpoint instruction, preserving the rest of the word.
|
||||
patched := (word &^ breakpointMask()) | breakpointWord(breakpointInsn)
|
||||
if err := bm.t.Poke(addr, patched); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -134,26 +168,40 @@ func (bm *Breakpoints) Info() string {
|
||||
}
|
||||
cond := ""
|
||||
if bp.Cond != nil {
|
||||
cond = fmt.Sprintf(" if %s %s %#x", bp.Cond.Reg, bp.Cond.Op, bp.Cond.Value)
|
||||
cond = " if " + bp.Cond.String()
|
||||
}
|
||||
result.WriteString(fmt.Sprintf(" %d: %s at %#x [%s, %d hits]%s\n", i, label, bp.Addr, status, bp.hits, cond))
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// Clear removes the breakpoint at addr, restoring the original byte.
|
||||
// restore writes the saved original bytes back over the breakpoint
|
||||
// instruction, preserving the rest of the peeked word. It reports whether
|
||||
// both the peek and the poke succeeded.
|
||||
func (bm *Breakpoints) restore(addr uint64, bp *Breakpoint) bool {
|
||||
word, err := bm.t.Peek(addr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
orig := uint64(0)
|
||||
for i, b := range bp.Orig {
|
||||
orig |= uint64(b) << (8 * i)
|
||||
}
|
||||
return bm.t.Poke(addr, (word&^breakpointMask())|orig) == nil
|
||||
}
|
||||
|
||||
// Clear removes the breakpoint at addr, restoring the original bytes.
|
||||
func (bm *Breakpoints) Clear(addr uint64) error {
|
||||
bp, ok := bm.bps[addr]
|
||||
if !ok {
|
||||
return fmt.Errorf("debug: no breakpoint at %#x", addr)
|
||||
}
|
||||
word, err := bm.t.Peek(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
||||
if err := bm.t.Poke(addr, restored); err != nil {
|
||||
return err
|
||||
if !bm.restore(addr, bp) {
|
||||
word, err := bm.t.Peek(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("debug: restore breakpoint at %#x failed, word is %#x", addr, word)
|
||||
}
|
||||
delete(bm.bps, addr)
|
||||
return nil
|
||||
@@ -185,43 +233,54 @@ func (bm *Breakpoints) All() []*Breakpoint {
|
||||
|
||||
// HandleTrap is called after the debuggee stops on SIGTRAP. It checks
|
||||
// whether the trap was caused by one of our breakpoints (PC-adjust matches
|
||||
// a breakpoint address), restores the original byte, rewinds PC, and
|
||||
// a breakpoint address), restores the original bytes, rewinds PC, and
|
||||
// returns the breakpoint that was hit (or nil if it was a single-step).
|
||||
// Hits returns how many times the breakpoint has been hit.
|
||||
func (bp *Breakpoint) Hits() int { return bp.hits }
|
||||
|
||||
func (bm *Breakpoints) HandleTrap(regs *Regs) *Breakpoint {
|
||||
// After a breakpoint trap, PC points past the breakpoint instruction.
|
||||
// On amd64 the kernel reports the trap with RIP past the INT3; on the
|
||||
// other supported architectures the PC still stands on the trap
|
||||
// instruction, which breakpointPCAdjust encodes per architecture.
|
||||
trapAddr := regs.GetPC() - uint64(breakpointPCAdjust)
|
||||
bp, ok := bm.bps[trapAddr]
|
||||
if !ok || !bp.Enabled {
|
||||
return nil // single-step trap or unknown
|
||||
}
|
||||
// Check the condition (if any).
|
||||
if bp.Cond != nil && !bp.Cond.Eval(regs) {
|
||||
// Condition not met, restore the byte but do NOT rewind RIP.
|
||||
// The process continues from the next instruction (past the INT3).
|
||||
word, err := bm.t.Peek(trapAddr)
|
||||
if err == nil {
|
||||
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
||||
bm.t.Poke(trapAddr, restored)
|
||||
if bp.Cond != nil && !bp.Cond.Eval(regs, bm.peekValue) {
|
||||
// Condition not met: step the original instruction and re-arm the
|
||||
// breakpoint, leaving the debuggee stopped just past it, ready to
|
||||
// resume silently. The PC must be rewound first: on architectures
|
||||
// that report the trap past the instruction (amd64) it would
|
||||
// otherwise sit on the second byte of the replaced instruction.
|
||||
if !bm.restore(trapAddr, bp) {
|
||||
return nil
|
||||
}
|
||||
// RIP is already past the INT3 (trapAddr + 1). Don't rewind.
|
||||
regs.SetPC(trapAddr)
|
||||
if err := bm.t.SetRegs(regs); err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := bm.t.Step(); err != nil {
|
||||
return nil
|
||||
}
|
||||
bm.Reinsert(trapAddr)
|
||||
return nil
|
||||
}
|
||||
bp.hits++
|
||||
// Restore the original byte.
|
||||
word, err := bm.t.Peek(trapAddr)
|
||||
if err == nil {
|
||||
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
||||
bm.t.Poke(trapAddr, restored)
|
||||
}
|
||||
// Rewind PC to re-execute the original instruction.
|
||||
// Restore the original bytes and rewind PC to re-execute them.
|
||||
bm.restore(trapAddr, bp)
|
||||
regs.SetPC(trapAddr)
|
||||
bm.t.SetRegs(regs)
|
||||
return bp
|
||||
}
|
||||
|
||||
// peekValue adapts tracer.Peek to the Condition value reader.
|
||||
func (bm *Breakpoints) peekValue(addr uint64) (uint64, bool) {
|
||||
v, err := bm.t.Peek(addr)
|
||||
return v, err == nil
|
||||
}
|
||||
|
||||
// Reinsert re-inserts the breakpoint at addr after a single-step past it.
|
||||
// Called after Step() when we want the breakpoint to fire again on the
|
||||
// next Continue().
|
||||
@@ -234,11 +293,7 @@ func (bm *Breakpoints) Reinsert(addr uint64) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mask := uint64(0)
|
||||
for range breakpointInsn {
|
||||
mask = (mask << 8) | 0xFF
|
||||
}
|
||||
patched := (word &^ mask) | breakpointWord(breakpointInsn)
|
||||
patched := (word &^ breakpointMask()) | breakpointWord(breakpointInsn)
|
||||
return bm.t.Poke(addr, patched)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user