// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause //go:build linux && amd64 package debug 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 simple register-comparison condition evaluated when a // breakpoint is hit. Format: . 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 bps map[uint64]*Breakpoint } // NewBreakpoints creates a breakpoint manager for the session. func NewBreakpoints(s *Session) *Breakpoints { return &Breakpoints{s: s, 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 byte. word, err := bm.s.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 if err := bm.s.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 } // 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] if !ok { return fmt.Errorf("debug: no breakpoint at %#x", addr) } word, err := bm.s.Peek(addr) if err != nil { return err } restored := (word &^ 0xFF) | uint64(bp.Orig) if err := bm.s.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 (RIP-1 matches // a breakpoint address), restores the original byte, rewinds RIP, 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 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.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) if err == nil { restored := (word &^ 0xFF) | uint64(bp.Orig) bm.s.Poke(trapAddr, restored) } // Rewind RIP to re-execute the original instruction. regs.RIP = trapAddr bm.s.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.s.Peek(addr) if err != nil { return err } patched := (word &^ 0xFF) | 0xCC return bm.s.Poke(addr, patched) }