// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause //go:build linux package debug import "strings" import "fmt" // Breakpoint is one INT3 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) Enabled bool Cond *Condition // optional condition (nil = unconditional) hits int } // Condition is a register-comparison condition evaluated when a breakpoint // is hit. Supports three forms: // - register vs constant: // - register vs register: // - register vs memory: * type Condition struct { Reg string // register name (rax, rbx, rip, rsp, ...) Op string // comparison operator: ==, !=, <, >, <=, >= Value uint64 // constant value (when Reg2 == "" and MemAddr == 0) Reg2 string // second register name (for register-register comparison) 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 { actual, ok := regs.RegValue(c.Reg) if !ok { return true // unknown register — don't block } var expected uint64 switch { case c.Reg2 != "": // Register-register comparison. v, ok := regs.RegValue(c.Reg2) if !ok { return true } 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 default: expected = c.Value } switch c.Op { case "==", "=": return actual == expected case "!=": return actual != expected case "<": return actual < expected case ">": return actual > expected case "<=": return actual <= expected case ">=": return actual >= expected default: return true } } // Breakpoints manages the set of breakpoints for a Session. // Breakpoints manages software breakpoints for a debuggee. type Breakpoints struct { t tracer bps map[uint64]*Breakpoint } // NewBreakpoints creates a new breakpoint manager. func NewBreakpoints(t tracer) *Breakpoints { return &Breakpoints{t: t, bps: make(map[uint64]*Breakpoint)} } // 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 bytes. word, err := bm.t.Peek(addr) 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 } patched := (word &^ mask) | breakpointWord(breakpointInsn) if err := bm.t.Poke(addr, patched); err != nil { return nil, err } bp := &Breakpoint{Addr: addr, Label: label, Orig: orig, Enabled: true, Cond: cond} bm.bps[addr] = bp return bp, nil } // Info returns a formatted list of all breakpoints. func (bm *Breakpoints) Info() string { if len(bm.bps) == 0 { return "no breakpoints set\n" } var result strings.Builder 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.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. 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 } delete(bm.bps, addr) return nil } // ClearAll removes all breakpoints. func (bm *Breakpoints) ClearAll() error { for addr := range bm.bps { if err := bm.Clear(addr); err != nil { return err } } return nil } // At returns the breakpoint at addr, if any. func (bm *Breakpoints) At(addr uint64) *Breakpoint { return bm.bps[addr] } // All returns all breakpoints. func (bm *Breakpoints) All() []*Breakpoint { out := make([]*Breakpoint, 0, len(bm.bps)) for _, bp := range bm.bps { out = append(out, bp) } return out } // 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 // returns the breakpoint that was hit (or nil if it was a single-step). func (bm *Breakpoints) HandleTrap(regs *Regs) *Breakpoint { // 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 } // 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) } // RIP is already past the INT3 (trapAddr + 1). Don't rewind. 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. regs.SetPC(trapAddr) bm.t.SetRegs(regs) return bp } // 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(). func (bm *Breakpoints) Reinsert(addr uint64) error { bp, ok := bm.bps[addr] if !ok || !bp.Enabled { return nil } word, err := bm.t.Peek(addr) if err != nil { return err } 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 }