feat(debug): complete the interactive debugger with disassembly, breakpoints, watchpoints and execution control

Assisted-by: DeepSeek V4 Pro
This commit is contained in:
2026-08-01 09:47:00 +02:00
parent c77d68018c
commit d114b3412c
9 changed files with 922 additions and 34 deletions
+118 -1
View File
@@ -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)