feat(debug): add interactive ptrace debugger MVP — single-step, regs, breakpoints, labels
Assisted-by: Qwen 3.8 Max Preview
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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)
|
||||
}
|
||||
Reference in New Issue
Block a user