// 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 hits int } // 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) { if bp, ok := bm.bps[addr]; ok { bp.Enabled = true 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} bm.bps[addr] = bp return bp, nil } // 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 } 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) }