feat(debug): complete the interactive debugger with disassembly, breakpoints, watchpoints and execution control
Assisted-by: DeepSeek V4 Pro
This commit is contained in:
+118
-1
@@ -13,9 +13,77 @@ type Breakpoint struct {
|
||||
Label string // source label ("" for raw addresses)
|
||||
Orig byte // original byte at Addr (restored on removal)
|
||||
Enabled bool
|
||||
Cond *Condition // optional condition (nil = unconditional)
|
||||
hits int
|
||||
}
|
||||
|
||||
// Condition is a simple register-comparison condition evaluated when a
|
||||
// breakpoint is hit. Format: <reg> <op> <value>.
|
||||
type Condition struct {
|
||||
Reg string // register name (rax, rbx, rip, rsp, ...)
|
||||
Op string // comparison operator: ==, !=, <, >, <=, >=
|
||||
Value uint64
|
||||
}
|
||||
|
||||
// Eval checks the condition against the current registers.
|
||||
func (c *Condition) Eval(regs *Regs) bool {
|
||||
var actual uint64
|
||||
switch c.Reg {
|
||||
case "rax", "eax", "ax", "al":
|
||||
actual = regs.RAX
|
||||
case "rbx", "ebx", "bx", "bl":
|
||||
actual = regs.RBX
|
||||
case "rcx", "ecx", "cx", "cl":
|
||||
actual = regs.RCX
|
||||
case "rdx", "edx", "dx", "dl":
|
||||
actual = regs.RDX
|
||||
case "rsi", "esi", "si":
|
||||
actual = regs.RSI
|
||||
case "rdi", "edi", "di":
|
||||
actual = regs.RDI
|
||||
case "rbp", "ebp", "bp":
|
||||
actual = regs.RBP
|
||||
case "rsp", "esp", "sp":
|
||||
actual = regs.RSP
|
||||
case "r8":
|
||||
actual = regs.R8
|
||||
case "r9":
|
||||
actual = regs.R9
|
||||
case "r10":
|
||||
actual = regs.R10
|
||||
case "r11":
|
||||
actual = regs.R11
|
||||
case "r12":
|
||||
actual = regs.R12
|
||||
case "r13":
|
||||
actual = regs.R13
|
||||
case "r14":
|
||||
actual = regs.R14
|
||||
case "r15":
|
||||
actual = regs.R15
|
||||
case "rip", "eip":
|
||||
actual = regs.RIP
|
||||
default:
|
||||
return true // unknown register — don't block
|
||||
}
|
||||
switch c.Op {
|
||||
case "==", "=":
|
||||
return actual == c.Value
|
||||
case "!=":
|
||||
return actual != c.Value
|
||||
case "<":
|
||||
return actual < c.Value
|
||||
case ">":
|
||||
return actual > c.Value
|
||||
case "<=":
|
||||
return actual <= c.Value
|
||||
case ">=":
|
||||
return actual >= c.Value
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Breakpoints manages the set of breakpoints for a Session.
|
||||
type Breakpoints struct {
|
||||
s *Session
|
||||
@@ -29,8 +97,14 @@ func NewBreakpoints(s *Session) *Breakpoints {
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// SetWithCond installs a breakpoint with an optional condition.
|
||||
func (bm *Breakpoints) SetWithCond(addr uint64, label string, cond *Condition) (*Breakpoint, error) {
|
||||
if bp, ok := bm.bps[addr]; ok {
|
||||
bp.Enabled = true
|
||||
bp.Cond = cond
|
||||
return bp, nil
|
||||
}
|
||||
// Read the original byte.
|
||||
@@ -44,11 +118,42 @@ func (bm *Breakpoints) Set(addr uint64, label string) (*Breakpoint, error) {
|
||||
if err := bm.s.Poke(addr, patched); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bp := &Breakpoint{Addr: addr, Label: label, Orig: orig, Enabled: true}
|
||||
bp := &Breakpoint{Addr: addr, Label: label, Orig: orig, Enabled: true, Cond: cond}
|
||||
bm.bps[addr] = bp
|
||||
return bp, nil
|
||||
}
|
||||
|
||||
// Hits returns the number of times the breakpoint has been hit.
|
||||
func (bp *Breakpoint) Hits() int {
|
||||
return bp.hits
|
||||
}
|
||||
|
||||
// Info returns a formatted list of all breakpoints.
|
||||
func (bm *Breakpoints) Info() string {
|
||||
if len(bm.bps) == 0 {
|
||||
return "no breakpoints set\n"
|
||||
}
|
||||
result := ""
|
||||
i := 0
|
||||
for _, bp := range bm.bps {
|
||||
i++
|
||||
status := "enabled"
|
||||
if !bp.Enabled {
|
||||
status = "disabled"
|
||||
}
|
||||
label := bp.Label
|
||||
if label == "" {
|
||||
label = fmt.Sprintf("%#x", bp.Addr)
|
||||
}
|
||||
cond := ""
|
||||
if bp.Cond != nil {
|
||||
cond = fmt.Sprintf(" if %s %s %#x", bp.Cond.Reg, bp.Cond.Op, bp.Cond.Value)
|
||||
}
|
||||
result += fmt.Sprintf(" %d: %s at %#x [%s, %d hits]%s\n", i, label, bp.Addr, status, bp.hits, cond)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Clear removes the breakpoint at addr, restoring the original byte.
|
||||
func (bm *Breakpoints) Clear(addr uint64) error {
|
||||
bp, ok := bm.bps[addr]
|
||||
@@ -102,6 +207,18 @@ func (bm *Breakpoints) HandleTrap(regs *Regs) *Breakpoint {
|
||||
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.s.Peek(trapAddr)
|
||||
if err == nil {
|
||||
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
||||
bm.s.Poke(trapAddr, restored)
|
||||
}
|
||||
// RIP is already past the INT3 (trapAddr + 1). Don't rewind.
|
||||
return nil
|
||||
}
|
||||
bp.hits++
|
||||
// Restore the original byte.
|
||||
word, err := bm.s.Peek(trapAddr)
|
||||
|
||||
Reference in New Issue
Block a user