629 lines
16 KiB
Go
629 lines
16 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build linux
|
|
|
|
package debug
|
|
|
|
import (
|
|
"bufio"
|
|
"cmp"
|
|
"fmt"
|
|
"io"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Label is a named address within the debugged function.
|
|
type Label struct {
|
|
Name string
|
|
Offset int // function-relative offset
|
|
}
|
|
|
|
// SourceLine maps a byte offset to a source line number.
|
|
type SourceLine struct {
|
|
Offset int
|
|
Line int
|
|
}
|
|
|
|
// REPL runs the interactive debugger loop, reading commands from in (pass
|
|
// os.Stdin interactively, or a bytes.Reader/script file for headless runs).
|
|
func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, argsSize int, labels []Label, lines []SourceLine, in io.Reader) {
|
|
entryAddr := codeBase + uint64(funcOffset)
|
|
|
|
fmt.Printf("stopped at function entry: %#x (%d bytes)\n", entryAddr, funcSize)
|
|
fmt.Println("commands: break <label|addr> | step [n] | continue | disas [n] | regs | where | x <addr> [len] | w <addr> <val...> | labels | quit")
|
|
|
|
scanner := bufio.NewScanner(in)
|
|
|
|
for {
|
|
fmt.Print("(gasm) ")
|
|
if !scanner.Scan() {
|
|
break
|
|
}
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
parts := strings.Fields(line)
|
|
cmd := parts[0]
|
|
|
|
switch cmd {
|
|
case "q", "quit":
|
|
s.Kill()
|
|
return
|
|
|
|
case "regs":
|
|
regs, err := s.GetRegs()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
continue
|
|
}
|
|
printRegs(®s, codeBase, uint64(funcOffset))
|
|
vregs, err := s.GetVectorRegs()
|
|
if err != nil {
|
|
fmt.Printf(" (vector regs unavailable: %v)\n", err)
|
|
} else {
|
|
printVectorRegs(&vregs)
|
|
}
|
|
|
|
case "step", "s":
|
|
n := 1
|
|
if len(parts) > 1 {
|
|
n, _ = strconv.Atoi(parts[1])
|
|
}
|
|
for range n {
|
|
if s.Exited() {
|
|
fmt.Println("debuggee exited")
|
|
break
|
|
}
|
|
if err := s.Step(); err != nil {
|
|
fmt.Println(err)
|
|
break
|
|
}
|
|
}
|
|
if !s.Exited() {
|
|
regs, _ := s.GetRegs()
|
|
pc := regs.GetPC()
|
|
text, _, _ := s.Disassemble(pc)
|
|
fmt.Printf("=> %#x (func+%#x): %s\n", pc, pc-codeBase-uint64(funcOffset), text)
|
|
}
|
|
|
|
case "next", "n":
|
|
regs, _ := s.GetRegs()
|
|
pc := regs.GetPC()
|
|
text, instLen, _ := s.Disassemble(pc)
|
|
if isCallInsn(text) {
|
|
afterAddr := pc + uint64(instLen)
|
|
_, err := bm.Set(afterAddr, "(next)")
|
|
if err != nil {
|
|
fmt.Printf("cannot set next breakpoint: %v\n", err)
|
|
continue
|
|
}
|
|
for _, b := range bm.All() {
|
|
bm.Reinsert(b.Addr)
|
|
}
|
|
if err := s.Continue(); err != nil {
|
|
fmt.Println(err)
|
|
bm.Clear(afterAddr)
|
|
continue
|
|
}
|
|
if s.Exited() {
|
|
bm.Clear(afterAddr)
|
|
fmt.Println("debuggee exited")
|
|
continue
|
|
}
|
|
if sig := s.LastSignal(); sig != 0 {
|
|
bm.Clear(afterAddr)
|
|
regs, _ := s.GetRegs()
|
|
fmt.Printf("stopped on signal %v at %#x\n", sig, regs.GetPC())
|
|
continue
|
|
}
|
|
// Fetch the registers after the stop: the trap must be
|
|
// evaluated against the real PC, not the pre-Continue
|
|
// snapshot, and a stale SetRegs would clobber live state.
|
|
regs, _ = s.GetRegs()
|
|
bm.HandleTrap(®s)
|
|
bm.Clear(afterAddr)
|
|
} else {
|
|
if err := s.Step(); err != nil {
|
|
fmt.Println(err)
|
|
continue
|
|
}
|
|
}
|
|
if !s.Exited() {
|
|
regs, _ := s.GetRegs()
|
|
pc := regs.GetPC()
|
|
text, _, _ := s.Disassemble(pc)
|
|
fmt.Printf("=> %#x (func+%#x): %s\n", pc, pc-codeBase-uint64(funcOffset), text)
|
|
}
|
|
|
|
case "finish", "fin":
|
|
regs, _ := s.GetRegs()
|
|
retAddr, err := archReturnAddr(s, ®s)
|
|
if err != nil {
|
|
fmt.Printf("cannot read return address: %v\n", err)
|
|
continue
|
|
}
|
|
_, err = bm.Set(retAddr, "(finish)")
|
|
if err != nil {
|
|
fmt.Printf("cannot set finish breakpoint: %v\n", err)
|
|
continue
|
|
}
|
|
for _, b := range bm.All() {
|
|
bm.Reinsert(b.Addr)
|
|
}
|
|
if err := s.Continue(); err != nil {
|
|
fmt.Println(err)
|
|
bm.Clear(retAddr)
|
|
continue
|
|
}
|
|
if s.Exited() {
|
|
bm.Clear(retAddr)
|
|
fmt.Println("debuggee exited")
|
|
continue
|
|
}
|
|
if sig := s.LastSignal(); sig != 0 {
|
|
bm.Clear(retAddr)
|
|
regs, _ := s.GetRegs()
|
|
fmt.Printf("stopped on signal %v at %#x\n", sig, regs.GetPC())
|
|
continue
|
|
}
|
|
// Fetch the registers after the stop, as the continue case
|
|
// does: HandleTrap must see the PC the trap left behind.
|
|
regs, _ = s.GetRegs()
|
|
bm.HandleTrap(®s)
|
|
bm.Clear(retAddr)
|
|
if s.Exited() {
|
|
fmt.Println("debuggee exited")
|
|
} else {
|
|
regs, _ := s.GetRegs()
|
|
fmt.Printf("finished, now at %#x\n", regs.GetPC())
|
|
}
|
|
|
|
case "continue", "c":
|
|
if s.Exited() {
|
|
fmt.Println("debuggee exited")
|
|
continue
|
|
}
|
|
for {
|
|
for _, bp := range bm.All() {
|
|
bm.Reinsert(bp.Addr)
|
|
}
|
|
if err := s.Continue(); err != nil {
|
|
fmt.Println(err)
|
|
break
|
|
}
|
|
if s.Exited() {
|
|
fmt.Println("debuggee exited")
|
|
break
|
|
}
|
|
if sig := s.LastSignal(); sig != 0 {
|
|
// A genuine signal-delivery-stop (a fault): report it
|
|
// and return to the prompt. Continuing would restart
|
|
// the faulting instruction and fault forever.
|
|
regs, _ := s.GetRegs()
|
|
fmt.Printf("stopped on signal %v at %#x (func+%#x)\n",
|
|
sig, regs.GetPC(), regs.GetPC()-codeBase-uint64(funcOffset))
|
|
break
|
|
}
|
|
reason, wpAddr := s.StopInfo()
|
|
if reason == StopWatchpoint {
|
|
fmt.Printf("watchpoint hit at %#x\n", wpAddr)
|
|
break
|
|
}
|
|
regs, _ := s.GetRegs()
|
|
if bp := bm.HandleTrap(®s); bp != nil {
|
|
// Execute the instruction under the restored breakpoint
|
|
// so the next continue cannot re-trap on the same
|
|
// breakpoint; the process parks right after it.
|
|
if err := s.Step(); err != nil {
|
|
fmt.Println(err)
|
|
break
|
|
}
|
|
name := bp.Label
|
|
if name == "" {
|
|
name = fmt.Sprintf("%#x", bp.Addr)
|
|
}
|
|
fmt.Printf("breakpoint hit: %s (func+%#x)\n", name, bp.Addr-codeBase-uint64(funcOffset))
|
|
break
|
|
}
|
|
}
|
|
|
|
case "break", "b":
|
|
if len(parts) < 2 {
|
|
fmt.Println("usage: break <label|addr|line> [if <reg> <op> <val>]")
|
|
continue
|
|
}
|
|
var addr uint64
|
|
var label string
|
|
if lineNum, err := strconv.Atoi(parts[1]); err == nil && lineNum > 0 {
|
|
off := offsetForLine(lines, lineNum)
|
|
if off < 0 {
|
|
fmt.Printf("no instruction at line %d\n", lineNum)
|
|
continue
|
|
}
|
|
addr = codeBase + uint64(funcOffset) + uint64(off)
|
|
label = fmt.Sprintf("line %d", lineNum)
|
|
} else {
|
|
addr, label = resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
|
}
|
|
if addr == 0 {
|
|
fmt.Printf("unknown label, address, or line: %s\n", parts[1])
|
|
continue
|
|
}
|
|
var cond *Condition
|
|
if len(parts) >= 6 && parts[2] == "if" {
|
|
reg := strings.ToLower(parts[3])
|
|
op := parts[4]
|
|
operand := parts[5]
|
|
switch {
|
|
case strings.HasPrefix(operand, "*"):
|
|
// Memory operand: compare against the 8-byte word at
|
|
// the address, resolved in the debuggee when the
|
|
// breakpoint is evaluated.
|
|
addr, err := strconv.ParseUint(strings.TrimPrefix(operand, "*"), 0, 64)
|
|
if err != nil {
|
|
fmt.Printf("invalid memory operand: %s\n", operand)
|
|
continue
|
|
}
|
|
cond = &Condition{Reg: reg, Op: op, MemAddr: addr}
|
|
default:
|
|
val, err := strconv.ParseUint(operand, 0, 64)
|
|
if err == nil {
|
|
cond = &Condition{Reg: reg, Op: op, Value: val}
|
|
} else {
|
|
cond = &Condition{Reg: reg, Op: op, Reg2: strings.ToLower(operand)}
|
|
}
|
|
}
|
|
} else if len(parts) >= 4 && parts[2] == "if" {
|
|
fmt.Println("usage: break <label|addr> if <reg> <op> <value|reg|*addr>")
|
|
continue
|
|
}
|
|
bp, err := bm.SetWithCond(addr, label, cond)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
continue
|
|
}
|
|
condStr := ""
|
|
if cond != nil {
|
|
condStr = " if " + cond.String()
|
|
}
|
|
fmt.Printf("breakpoint set: %s at %#x (func+%#x)%s\n", bp.Label, bp.Addr, bp.Addr-codeBase-uint64(funcOffset), condStr)
|
|
|
|
case "info":
|
|
if len(parts) < 2 {
|
|
fmt.Println("usage: info break")
|
|
continue
|
|
}
|
|
switch parts[1] {
|
|
case "break", "breakpoints", "b":
|
|
fmt.Print(bm.Info())
|
|
default:
|
|
fmt.Printf("unknown info target: %s\n", parts[1])
|
|
}
|
|
|
|
case "delete", "d":
|
|
if len(parts) < 2 {
|
|
fmt.Println("usage: delete <label|addr>")
|
|
continue
|
|
}
|
|
addr, _ := resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
|
if addr == 0 {
|
|
fmt.Printf("unknown: %s\n", parts[1])
|
|
continue
|
|
}
|
|
if err := bm.Clear(addr); err != nil {
|
|
fmt.Println(err)
|
|
} else {
|
|
fmt.Println("breakpoint removed")
|
|
}
|
|
|
|
case "x":
|
|
regs, _ := s.GetRegs()
|
|
addr := regs.GetPC()
|
|
length := 64
|
|
if len(parts) > 1 {
|
|
addr, _ = resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
|
}
|
|
if len(parts) > 2 {
|
|
// A malformed or non-positive length would panic
|
|
// ReadMemory's make; fall back to the default instead.
|
|
if n, err := strconv.Atoi(parts[2]); err == nil && n > 0 {
|
|
length = n
|
|
}
|
|
}
|
|
mem, err := s.ReadMemory(addr, length)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
continue
|
|
}
|
|
hexDump(addr, mem)
|
|
|
|
case "w":
|
|
if len(parts) < 3 {
|
|
fmt.Println("usage: w <addr> <byte|0x...> [byte...]")
|
|
continue
|
|
}
|
|
addr, _ := resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
|
if addr == 0 {
|
|
fmt.Printf("unknown address: %s\n", parts[1])
|
|
continue
|
|
}
|
|
var bytes []byte
|
|
for _, arg := range parts[2:] {
|
|
v, err := strconv.ParseUint(arg, 0, 64)
|
|
if err != nil {
|
|
fmt.Printf("invalid value: %s\n", arg)
|
|
continue
|
|
}
|
|
if v > 255 {
|
|
for j := range 8 {
|
|
bytes = append(bytes, byte(v>>(8*j)))
|
|
}
|
|
} else {
|
|
bytes = append(bytes, byte(v))
|
|
}
|
|
}
|
|
if len(bytes) > 0 {
|
|
if err := s.WriteMemory(addr, bytes); err != nil {
|
|
fmt.Println(err)
|
|
} else {
|
|
fmt.Printf("wrote %d bytes at %#x\n", len(bytes), addr)
|
|
}
|
|
}
|
|
|
|
case "set":
|
|
if len(parts) < 3 {
|
|
fmt.Println("usage: set <reg> <value>")
|
|
continue
|
|
}
|
|
val, err := strconv.ParseUint(parts[2], 0, 64)
|
|
if err != nil {
|
|
fmt.Printf("invalid value: %s\n", parts[2])
|
|
continue
|
|
}
|
|
if err := s.SetReg(strings.ToLower(parts[1]), val); err != nil {
|
|
fmt.Printf("set: %v\n", err)
|
|
} else {
|
|
fmt.Printf("%s = %#x\n", parts[1], val)
|
|
}
|
|
|
|
case "labels", "l":
|
|
slices.SortFunc(labels, func(a, b Label) int { return cmp.Compare(a.Offset, b.Offset) })
|
|
for _, l := range labels {
|
|
fmt.Printf(" func+%#04x %s\n", l.Offset, l.Name)
|
|
}
|
|
|
|
case "disas", "u":
|
|
n := 5
|
|
if len(parts) > 1 {
|
|
n, _ = strconv.Atoi(parts[1])
|
|
if n <= 0 {
|
|
n = 5
|
|
}
|
|
}
|
|
regs, _ := s.GetRegs()
|
|
fmt.Print(s.DisassembleN(regs.GetPC(), n))
|
|
|
|
case "where":
|
|
regs, _ := s.GetRegs()
|
|
funcOff := int(regs.GetPC() - codeBase - uint64(funcOffset))
|
|
line := lineAt(lines, funcOff)
|
|
label := nearestLabel(labels, funcOff)
|
|
fmt.Printf(" func+%#x", funcOff)
|
|
if label != "" {
|
|
fmt.Printf(" (near %s)", label)
|
|
}
|
|
if line > 0 {
|
|
fmt.Printf(" line %d", line)
|
|
}
|
|
fmt.Println()
|
|
|
|
case "help", "h", "?":
|
|
fmt.Printf(` break <label|addr> [if <reg> <op> <val|reg|*addr>]
|
|
set a breakpoint, optionally conditional on a
|
|
register compared to a constant, a register, or the
|
|
8-byte word at *addr
|
|
delete <label|addr> remove a breakpoint
|
|
info break list all breakpoints
|
|
watch <addr> [r|w] [size] set a hardware watchpoint (write by default)
|
|
unwatch [<slot>] clear one or all watchpoints
|
|
step [n], s single-step n instructions
|
|
next, n step over CALL/BL
|
|
continue, c run until breakpoint or exit
|
|
disas [n], u disassemble n instructions at PC
|
|
regs print registers
|
|
where show source line and nearest label
|
|
stack show stack near %s (args + return address)
|
|
x [addr] [len] hex-dump memory
|
|
w <addr> <val...> write bytes to memory
|
|
labels, l list function labels
|
|
help, h, ? this help
|
|
quit, q kill debuggee and exit`, archSPLabel())
|
|
|
|
case "stack":
|
|
regs, _ := s.GetRegs()
|
|
sp := regs.GetSP()
|
|
retAddr, _ := archReturnAddr(s, ®s)
|
|
fmt.Printf(" [%s] return addr = %#x\n", archSPLabel(), retAddr)
|
|
if argsSize > 0 {
|
|
fmt.Printf(" args (%d bytes at %s+8):\n", argsSize, archSPLabel())
|
|
argBytes, err := s.ReadMemory(sp+8, argsSize)
|
|
if err == nil {
|
|
for i := 0; i < argsSize; i += 8 {
|
|
var v uint64
|
|
for j := 0; j < 8 && i+j < len(argBytes); j++ {
|
|
v |= uint64(argBytes[i+j]) << (8 * j)
|
|
}
|
|
fmt.Printf(" [%+3d] %#016x\n", i+8, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
case "bt", "backtrace":
|
|
regs, _ := s.GetRegs()
|
|
funcOff := int(regs.GetPC() - codeBase - uint64(funcOffset))
|
|
line := lineAt(lines, funcOff)
|
|
label := nearestLabel(labels, funcOff)
|
|
fmt.Printf(" #0 func+%#x", funcOff)
|
|
if label != "" {
|
|
fmt.Printf(" (%s)", label)
|
|
}
|
|
if line > 0 {
|
|
fmt.Printf(" [line %d]", line)
|
|
}
|
|
fmt.Println()
|
|
retAddr, _ := archReturnAddr(s, ®s)
|
|
fmt.Printf(" #1 return to %#x\n", retAddr)
|
|
|
|
case "watch":
|
|
if len(parts) < 2 {
|
|
fmt.Println("usage: watch <addr> [r|w] [size]")
|
|
continue
|
|
}
|
|
addr, _ := resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
|
if addr == 0 {
|
|
fmt.Printf("unknown address: %s\n", parts[1])
|
|
continue
|
|
}
|
|
typ := WatchWrite
|
|
size := 8
|
|
if len(parts) > 2 {
|
|
switch parts[2] {
|
|
case "r":
|
|
typ = WatchRead
|
|
case "w":
|
|
typ = WatchWrite
|
|
}
|
|
}
|
|
if len(parts) > 3 {
|
|
size, _ = strconv.Atoi(parts[3])
|
|
}
|
|
slot := s.FindFreeWatchpointSlot()
|
|
if slot < 0 {
|
|
fmt.Println("no free watchpoint slots (use 'unwatch <slot>' to clear one)")
|
|
continue
|
|
}
|
|
if err := s.SetWatchpoint(slot, addr, typ, size); err != nil {
|
|
fmt.Printf("watch: %v\n", err)
|
|
} else {
|
|
typStr := "w"
|
|
if typ == WatchRead {
|
|
typStr = "r"
|
|
}
|
|
fmt.Printf("watchpoint %d set: %#x (%s, %d bytes)\n", slot, addr, typStr, size)
|
|
}
|
|
|
|
case "unwatch":
|
|
if len(parts) >= 2 {
|
|
slot, err := strconv.Atoi(parts[1])
|
|
if err != nil || slot < 0 || slot >= maxWatchpoints() {
|
|
fmt.Printf("usage: unwatch [<slot 0-%d>]\n", maxWatchpoints()-1)
|
|
continue
|
|
}
|
|
if err := s.ClearWatchpoint(slot); err != nil {
|
|
fmt.Printf("unwatch: %v\n", err)
|
|
} else {
|
|
fmt.Printf("watchpoint %d cleared\n", slot)
|
|
}
|
|
} else {
|
|
if err := s.ClearAllWatchpoints(); err != nil {
|
|
fmt.Printf("unwatch: %v\n", err)
|
|
} else {
|
|
fmt.Println("all watchpoints cleared")
|
|
}
|
|
}
|
|
|
|
default:
|
|
fmt.Printf("unknown command: %s\n", cmd)
|
|
}
|
|
}
|
|
s.Kill()
|
|
}
|
|
|
|
func hexDump(addr uint64, data []byte) {
|
|
for i := 0; i < len(data); i += 16 {
|
|
end := min(i+16, len(data))
|
|
fmt.Printf(" %#08x:", addr+uint64(i))
|
|
for j := i; j < i+16; j++ {
|
|
if j < end {
|
|
fmt.Printf(" %02x", data[j])
|
|
} else {
|
|
fmt.Print(" ")
|
|
}
|
|
}
|
|
fmt.Print(" ")
|
|
for j := i; j < end; j++ {
|
|
if data[j] >= 0x20 && data[j] < 0x7f {
|
|
fmt.Printf("%c", data[j])
|
|
} else {
|
|
fmt.Print(".")
|
|
}
|
|
}
|
|
fmt.Println()
|
|
}
|
|
}
|
|
|
|
func resolveAddr(s string, codeBase, funcOff uint64, labels []Label) (uint64, string) {
|
|
if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
|
|
v, err := strconv.ParseUint(s, 0, 64)
|
|
if err == nil {
|
|
return v, ""
|
|
}
|
|
}
|
|
if strings.HasPrefix(s, "+") {
|
|
off, err := strconv.ParseUint(s[1:], 0, 64)
|
|
if err == nil {
|
|
return codeBase + funcOff + off, fmt.Sprintf("func+%#x", off)
|
|
}
|
|
}
|
|
for _, l := range labels {
|
|
if l.Name == s {
|
|
return codeBase + funcOff + uint64(l.Offset), l.Name
|
|
}
|
|
}
|
|
return 0, ""
|
|
}
|
|
|
|
func lineAt(lines []SourceLine, offset int) int {
|
|
if len(lines) == 0 {
|
|
return 0
|
|
}
|
|
lo, hi := 0, len(lines)-1
|
|
for lo < hi {
|
|
mid := (lo + hi + 1) / 2
|
|
if lines[mid].Offset <= offset {
|
|
lo = mid
|
|
} else {
|
|
hi = mid - 1
|
|
}
|
|
}
|
|
if lines[lo].Offset <= offset {
|
|
return lines[lo].Line
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func offsetForLine(lines []SourceLine, line int) int {
|
|
for _, le := range lines {
|
|
if le.Line == line {
|
|
return le.Offset
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func nearestLabel(labels []Label, offset int) string {
|
|
best := ""
|
|
bestOff := -1
|
|
for _, l := range labels {
|
|
if l.Offset <= offset && l.Offset > bestOff {
|
|
best = l.Name
|
|
bestOff = l.Offset
|
|
}
|
|
}
|
|
return best
|
|
}
|