refactor(debug): make Regs and breakpoint arch-neutral for arm64

Assisted-by: MiMo V2.5 Pro
This commit is contained in:
2026-08-21 00:35:21 +02:00
parent 874e054ecb
commit ba502c9b79
4 changed files with 258 additions and 79 deletions
+29 -47
View File
@@ -25,43 +25,8 @@ type Condition struct {
// 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:
actual, ok := regs.RegValue(c.Reg)
if !ok {
return true // unknown register — don't block
}
switch c.Op {
@@ -106,14 +71,18 @@ func (bm *Breakpoints) SetWithCond(addr uint64, label string, cond *Condition) (
bp.Cond = cond
return bp, nil
}
// Read the original byte.
// Read the original bytes.
word, err := bm.t.Peek(addr)
if err != nil {
return nil, err
}
orig := byte(word)
// Patch with INT3 (0xCC), preserving the rest of the word.
patched := (word &^ 0xFF) | 0xCC
// Patch with the breakpoint instruction, preserving the rest of the word.
mask := uint64(0)
for range breakpointInsn {
mask = (mask << 8) | 0xFF
}
patched := (word &^ mask) | breakpointWord(breakpointInsn)
if err := bm.t.Poke(addr, patched); err != nil {
return nil, err
}
@@ -196,12 +165,12 @@ 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 (RIP-1 matches
// a breakpoint address), restores the original byte, rewinds RIP, and
// whether the trap was caused by one of our breakpoints (PC-adjust matches
// a breakpoint address), restores the original byte, rewinds PC, and
// returns the breakpoint that was hit (or nil if it was a single-step).
func (bm *Breakpoints) HandleTrap(regs *Regs) *Breakpoint {
// After INT3, RIP points to the byte AFTER the 0xCC.
trapAddr := regs.RIP - 1
// After a breakpoint trap, PC points past the breakpoint instruction.
trapAddr := regs.GetPC() - uint64(breakpointPCAdjust)
bp, ok := bm.bps[trapAddr]
if !ok || !bp.Enabled {
return nil // single-step trap or unknown
@@ -225,8 +194,8 @@ func (bm *Breakpoints) HandleTrap(regs *Regs) *Breakpoint {
restored := (word &^ 0xFF) | uint64(bp.Orig)
bm.t.Poke(trapAddr, restored)
}
// Rewind RIP to re-execute the original instruction.
regs.RIP = trapAddr
// Rewind PC to re-execute the original instruction.
regs.SetPC(trapAddr)
bm.t.SetRegs(regs)
return bp
}
@@ -243,6 +212,19 @@ func (bm *Breakpoints) Reinsert(addr uint64) error {
if err != nil {
return err
}
patched := (word &^ 0xFF) | 0xCC
mask := uint64(0)
for range breakpointInsn {
mask = (mask << 8) | 0xFF
}
patched := (word &^ mask) | breakpointWord(breakpointInsn)
return bm.t.Poke(addr, patched)
}
// breakpointWord converts the breakpoint instruction bytes to a uint64.
func breakpointWord(insn []byte) uint64 {
var w uint64
for i, b := range insn {
w |= uint64(b) << (i * 8)
}
return w
}
+95
View File
@@ -0,0 +1,95 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && amd64
package debug
// Regs holds the full general-purpose register set of a traced process
// (the Linux amd64 user_regs_struct layout).
type Regs struct {
R15 uint64
R14 uint64
R13 uint64
R12 uint64
RBP uint64
RBX uint64
R11 uint64
R10 uint64
R9 uint64
R8 uint64
RAX uint64
RCX uint64
RDX uint64
RSI uint64
RDI uint64
OrigRAX uint64
RIP uint64
CS uint64
RFLAGS uint64
RSP uint64
SS uint64
FSBase uint64
GSBase uint64
DS uint64
ES uint64
FS uint64
GS uint64
}
// GetPC returns the program counter.
func (r *Regs) GetPC() uint64 { return r.RIP }
// SetPC sets the program counter.
func (r *Regs) SetPC(pc uint64) { r.RIP = pc }
// GetSP returns the stack pointer.
func (r *Regs) GetSP() uint64 { return r.RSP }
// RegValue returns the value of the named register, or false if unknown.
func (r *Regs) RegValue(name string) (uint64, bool) {
switch name {
case "rax", "eax", "ax", "al":
return r.RAX, true
case "rbx", "ebx", "bx", "bl":
return r.RBX, true
case "rcx", "ecx", "cx", "cl":
return r.RCX, true
case "rdx", "edx", "dx", "dl":
return r.RDX, true
case "rsi", "esi", "si":
return r.RSI, true
case "rdi", "edi", "di":
return r.RDI, true
case "rbp", "ebp", "bp":
return r.RBP, true
case "rsp", "esp", "sp":
return r.RSP, true
case "r8":
return r.R8, true
case "r9":
return r.R9, true
case "r10":
return r.R10, true
case "r11":
return r.R11, true
case "r12":
return r.R12, true
case "r13":
return r.R13, true
case "r14":
return r.R14, true
case "r15":
return r.R15, true
case "rip", "eip":
return r.RIP, true
default:
return 0, false
}
}
// breakpointInsn is the software breakpoint instruction.
var breakpointInsn = []byte{0xCC} // INT3
// breakpointPCAdjust is how far PC is past the breakpoint instruction after a trap.
const breakpointPCAdjust = 1
+134
View File
@@ -0,0 +1,134 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && arm64
package debug
// Regs holds the full general-purpose register set of a traced process
// (the Linux arm64 user_pt_regs layout).
type Regs struct {
X0 uint64
X1 uint64
X2 uint64
X3 uint64
X4 uint64
X5 uint64
X6 uint64
X7 uint64
X8 uint64
X9 uint64
X10 uint64
X11 uint64
X12 uint64
X13 uint64
X14 uint64
X15 uint64
X16 uint64
X17 uint64
X18 uint64
X19 uint64
X20 uint64
X21 uint64
X22 uint64
X23 uint64
X24 uint64
X25 uint64
X26 uint64
X27 uint64
X28 uint64
X29 uint64 // FP (frame pointer)
X30 uint64 // LR (link register)
SP uint64
PC uint64
PSTATE uint64
}
// PC returns the program counter.
func (r *Regs) GetPC() uint64 { return r.PC }
// SetPC sets the program counter.
func (r *Regs) SetPC(pc uint64) { r.PC = pc }
// GetSP returns the stack pointer.
func (r *Regs) GetSP() uint64 { return r.SP }
// RegValue returns the value of the named register, or false if unknown.
func (r *Regs) RegValue(name string) (uint64, bool) {
switch name {
case "x0":
return r.X0, true
case "x1":
return r.X1, true
case "x2":
return r.X2, true
case "x3":
return r.X3, true
case "x4":
return r.X4, true
case "x5":
return r.X5, true
case "x6":
return r.X6, true
case "x7":
return r.X7, true
case "x8":
return r.X8, true
case "x9":
return r.X9, true
case "x10":
return r.X10, true
case "x11":
return r.X11, true
case "x12":
return r.X12, true
case "x13":
return r.X13, true
case "x14":
return r.X14, true
case "x15":
return r.X15, true
case "x16":
return r.X16, true
case "x17":
return r.X17, true
case "x18":
return r.X18, true
case "x19":
return r.X19, true
case "x20":
return r.X20, true
case "x21":
return r.X21, true
case "x22":
return r.X22, true
case "x23":
return r.X23, true
case "x24":
return r.X24, true
case "x25":
return r.X25, true
case "x26":
return r.X26, true
case "x27":
return r.X27, true
case "x28":
return r.X28, true
case "x29", "fp":
return r.X29, true
case "x30", "lr":
return r.X30, true
case "sp":
return r.SP, true
case "pc":
return r.PC, true
default:
return 0, false
}
}
// breakpointInsn is the software breakpoint instruction (BRK #0).
var breakpointInsn = []byte{0x00, 0x00, 0x20, 0xD4} // BRK #0
// breakpointPCAdjust is how far PC is past the breakpoint instruction after a trap.
const breakpointPCAdjust = 4
-32
View File
@@ -3,38 +3,6 @@
package debug
// Regs holds the full general-purpose register set of a traced process
// (the Linux amd64 user_regs_struct layout).
type Regs struct {
R15 uint64
R14 uint64
R13 uint64
R12 uint64
RBP uint64
RBX uint64
R11 uint64
R10 uint64
R9 uint64
R8 uint64
RAX uint64
RCX uint64
RDX uint64
RSI uint64
RDI uint64
OrigRAX uint64
RIP uint64
CS uint64
RFLAGS uint64
RSP uint64
SS uint64
FSBase uint64
GSBase uint64
DS uint64
ES uint64
FS uint64
GS uint64
}
// tracer abstracts the minimal ptrace operations needed by the breakpoint
// manager and the stop-information helpers. The live implementation is
// *Session (ptrace_linux_amd64.go); tests supply a mock.