2026-08-01 02:34:00 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
2026-08-21 00:50:31 +02:00
|
|
|
//go:build linux
|
|
|
|
|
|
2026-08-01 02:34:00 +02:00
|
|
|
package debug
|
|
|
|
|
|
2026-08-29 15:40:31 +02:00
|
|
|
import "strings"
|
|
|
|
|
|
2026-08-01 02:34:00 +02:00
|
|
|
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
|
2026-08-01 09:47:00 +02:00
|
|
|
Cond *Condition // optional condition (nil = unconditional)
|
2026-08-01 02:34:00 +02:00
|
|
|
hits int
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-21 01:07:21 +02:00
|
|
|
// Condition is a register-comparison condition evaluated when a breakpoint
|
|
|
|
|
// is hit. Supports three forms:
|
|
|
|
|
// - register vs constant: <reg> <op> <value>
|
|
|
|
|
// - register vs register: <reg> <op> <reg2>
|
|
|
|
|
// - register vs memory: <reg> <op> *<addr>
|
2026-08-01 09:47:00 +02:00
|
|
|
type Condition struct {
|
2026-08-21 01:07:21 +02:00
|
|
|
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 *)
|
2026-08-01 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Eval checks the condition against the current registers.
|
|
|
|
|
func (c *Condition) Eval(regs *Regs) bool {
|
2026-08-20 23:34:52 +02:00
|
|
|
actual, ok := regs.RegValue(c.Reg)
|
|
|
|
|
if !ok {
|
2026-08-01 09:47:00 +02:00
|
|
|
return true // unknown register — don't block
|
|
|
|
|
}
|
2026-08-21 01:07:21 +02:00
|
|
|
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
|
|
|
|
|
}
|
2026-08-01 09:47:00 +02:00
|
|
|
switch c.Op {
|
|
|
|
|
case "==", "=":
|
2026-08-21 01:07:21 +02:00
|
|
|
return actual == expected
|
2026-08-01 09:47:00 +02:00
|
|
|
case "!=":
|
2026-08-21 01:07:21 +02:00
|
|
|
return actual != expected
|
2026-08-01 09:47:00 +02:00
|
|
|
case "<":
|
2026-08-21 01:07:21 +02:00
|
|
|
return actual < expected
|
2026-08-01 09:47:00 +02:00
|
|
|
case ">":
|
2026-08-21 01:07:21 +02:00
|
|
|
return actual > expected
|
2026-08-01 09:47:00 +02:00
|
|
|
case "<=":
|
2026-08-21 01:07:21 +02:00
|
|
|
return actual <= expected
|
2026-08-01 09:47:00 +02:00
|
|
|
case ">=":
|
2026-08-21 01:07:21 +02:00
|
|
|
return actual >= expected
|
2026-08-01 09:47:00 +02:00
|
|
|
default:
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 02:34:00 +02:00
|
|
|
// Breakpoints manages the set of breakpoints for a Session.
|
2026-08-03 19:42:05 +02:00
|
|
|
// Breakpoints manages software breakpoints for a debuggee.
|
2026-08-01 02:34:00 +02:00
|
|
|
type Breakpoints struct {
|
2026-08-03 19:42:05 +02:00
|
|
|
t tracer
|
2026-08-01 02:34:00 +02:00
|
|
|
bps map[uint64]*Breakpoint
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 19:42:05 +02:00
|
|
|
// NewBreakpoints creates a new breakpoint manager.
|
|
|
|
|
func NewBreakpoints(t tracer) *Breakpoints {
|
|
|
|
|
return &Breakpoints{t: t, bps: make(map[uint64]*Breakpoint)}
|
2026-08-01 02:34:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set installs a breakpoint at addr (replaces any existing one).
|
|
|
|
|
func (bm *Breakpoints) Set(addr uint64, label string) (*Breakpoint, error) {
|
2026-08-01 09:47:00 +02:00
|
|
|
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) {
|
2026-08-01 02:34:00 +02:00
|
|
|
if bp, ok := bm.bps[addr]; ok {
|
|
|
|
|
bp.Enabled = true
|
2026-08-01 09:47:00 +02:00
|
|
|
bp.Cond = cond
|
2026-08-01 02:34:00 +02:00
|
|
|
return bp, nil
|
|
|
|
|
}
|
2026-08-20 23:34:52 +02:00
|
|
|
// Read the original bytes.
|
2026-08-03 19:42:05 +02:00
|
|
|
word, err := bm.t.Peek(addr)
|
2026-08-01 02:34:00 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
orig := byte(word)
|
2026-08-20 23:34:52 +02:00
|
|
|
// 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)
|
2026-08-03 19:42:05 +02:00
|
|
|
if err := bm.t.Poke(addr, patched); err != nil {
|
2026-08-01 02:34:00 +02:00
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-08-01 09:47:00 +02:00
|
|
|
bp := &Breakpoint{Addr: addr, Label: label, Orig: orig, Enabled: true, Cond: cond}
|
2026-08-01 02:34:00 +02:00
|
|
|
bm.bps[addr] = bp
|
|
|
|
|
return bp, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 09:47:00 +02:00
|
|
|
// Info returns a formatted list of all breakpoints.
|
|
|
|
|
func (bm *Breakpoints) Info() string {
|
|
|
|
|
if len(bm.bps) == 0 {
|
|
|
|
|
return "no breakpoints set\n"
|
|
|
|
|
}
|
2026-08-29 15:40:31 +02:00
|
|
|
var result strings.Builder
|
2026-08-01 09:47:00 +02:00
|
|
|
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)
|
|
|
|
|
}
|
2026-08-29 15:40:31 +02:00
|
|
|
result.WriteString(fmt.Sprintf(" %d: %s at %#x [%s, %d hits]%s\n", i, label, bp.Addr, status, bp.hits, cond))
|
2026-08-01 09:47:00 +02:00
|
|
|
}
|
2026-08-29 15:40:31 +02:00
|
|
|
return result.String()
|
2026-08-01 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-01 02:34:00 +02:00
|
|
|
// 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)
|
|
|
|
|
}
|
2026-08-03 19:42:05 +02:00
|
|
|
word, err := bm.t.Peek(addr)
|
2026-08-01 02:34:00 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
2026-08-03 19:42:05 +02:00
|
|
|
if err := bm.t.Poke(addr, restored); err != nil {
|
2026-08-01 02:34:00 +02:00
|
|
|
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
|
2026-08-20 23:34:52 +02:00
|
|
|
// whether the trap was caused by one of our breakpoints (PC-adjust matches
|
|
|
|
|
// a breakpoint address), restores the original byte, rewinds PC, and
|
2026-08-01 02:34:00 +02:00
|
|
|
// returns the breakpoint that was hit (or nil if it was a single-step).
|
2026-08-30 22:05:05 +02:00
|
|
|
// Hits returns how many times the breakpoint has been hit.
|
|
|
|
|
func (bp *Breakpoint) Hits() int { return bp.hits }
|
|
|
|
|
|
2026-08-01 02:34:00 +02:00
|
|
|
func (bm *Breakpoints) HandleTrap(regs *Regs) *Breakpoint {
|
2026-08-20 23:34:52 +02:00
|
|
|
// After a breakpoint trap, PC points past the breakpoint instruction.
|
|
|
|
|
trapAddr := regs.GetPC() - uint64(breakpointPCAdjust)
|
2026-08-01 02:34:00 +02:00
|
|
|
bp, ok := bm.bps[trapAddr]
|
|
|
|
|
if !ok || !bp.Enabled {
|
|
|
|
|
return nil // single-step trap or unknown
|
|
|
|
|
}
|
2026-08-01 09:47:00 +02:00
|
|
|
// 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).
|
2026-08-03 19:42:05 +02:00
|
|
|
word, err := bm.t.Peek(trapAddr)
|
2026-08-01 09:47:00 +02:00
|
|
|
if err == nil {
|
|
|
|
|
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
2026-08-03 19:42:05 +02:00
|
|
|
bm.t.Poke(trapAddr, restored)
|
2026-08-01 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
// RIP is already past the INT3 (trapAddr + 1). Don't rewind.
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-08-01 02:34:00 +02:00
|
|
|
bp.hits++
|
|
|
|
|
// Restore the original byte.
|
2026-08-03 19:42:05 +02:00
|
|
|
word, err := bm.t.Peek(trapAddr)
|
2026-08-01 02:34:00 +02:00
|
|
|
if err == nil {
|
|
|
|
|
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
2026-08-03 19:42:05 +02:00
|
|
|
bm.t.Poke(trapAddr, restored)
|
2026-08-01 02:34:00 +02:00
|
|
|
}
|
2026-08-20 23:34:52 +02:00
|
|
|
// Rewind PC to re-execute the original instruction.
|
|
|
|
|
regs.SetPC(trapAddr)
|
2026-08-03 19:42:05 +02:00
|
|
|
bm.t.SetRegs(regs)
|
2026-08-01 02:34:00 +02:00
|
|
|
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
|
|
|
|
|
}
|
2026-08-03 19:42:05 +02:00
|
|
|
word, err := bm.t.Peek(addr)
|
2026-08-01 02:34:00 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-08-20 23:34:52 +02:00
|
|
|
mask := uint64(0)
|
|
|
|
|
for range breakpointInsn {
|
|
|
|
|
mask = (mask << 8) | 0xFF
|
|
|
|
|
}
|
|
|
|
|
patched := (word &^ mask) | breakpointWord(breakpointInsn)
|
2026-08-03 19:42:05 +02:00
|
|
|
return bm.t.Poke(addr, patched)
|
2026-08-01 02:34:00 +02:00
|
|
|
}
|
2026-08-20 23:34:52 +02:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|