From ba502c9b797d913c7a82b969be91173ccc7ba0e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Thu, 20 Aug 2026 23:34:52 +0200 Subject: [PATCH] refactor(debug): make Regs and breakpoint arch-neutral for arm64 Assisted-by: MiMo V2.5 Pro --- debug/breakpoint.go | 76 +++++++++------------ debug/regs_linux_amd64.go | 95 +++++++++++++++++++++++++++ debug/regs_linux_arm64.go | 134 ++++++++++++++++++++++++++++++++++++++ debug/tracer.go | 32 --------- 4 files changed, 258 insertions(+), 79 deletions(-) create mode 100644 debug/regs_linux_amd64.go create mode 100644 debug/regs_linux_arm64.go diff --git a/debug/breakpoint.go b/debug/breakpoint.go index 7ec579c..12beadb 100644 --- a/debug/breakpoint.go +++ b/debug/breakpoint.go @@ -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 +} diff --git a/debug/regs_linux_amd64.go b/debug/regs_linux_amd64.go new file mode 100644 index 0000000..f18c632 --- /dev/null +++ b/debug/regs_linux_amd64.go @@ -0,0 +1,95 @@ +// Copyright (c) 2026 Petr Balvín (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 diff --git a/debug/regs_linux_arm64.go b/debug/regs_linux_arm64.go new file mode 100644 index 0000000..da7b436 --- /dev/null +++ b/debug/regs_linux_arm64.go @@ -0,0 +1,134 @@ +// Copyright (c) 2026 Petr Balvín (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 diff --git a/debug/tracer.go b/debug/tracer.go index e3c0c2a..0ffaf54 100644 --- a/debug/tracer.go +++ b/debug/tracer.go @@ -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.