feat(debug): add interactive ptrace debugger MVP — single-step, regs, breakpoints, labels
Assisted-by: Qwen 3.8 Max Preview
This commit is contained in:
+246
@@ -0,0 +1,246 @@
|
||||
// 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 (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Label is a named address within the debugged function.
|
||||
type Label struct {
|
||||
Name string
|
||||
Offset int // function-relative offset
|
||||
}
|
||||
|
||||
// REPL runs the interactive debugger loop. On entry, the debuggee is
|
||||
// stopped in the Go runtime (after PTRACE_TRACEME + SIGSTOP). The REPL
|
||||
// sets a temporary breakpoint at the function entry, continues to it, and
|
||||
// then presents the prompt — so the user starts debugging at the first
|
||||
// instruction of the assembled function.
|
||||
func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, argsSize int, labels []Label) {
|
||||
entryAddr := codeBase + uint64(funcOffset)
|
||||
|
||||
// Run to the function entry.
|
||||
bp, err := bm.Set(entryAddr, "(entry)")
|
||||
if err != nil {
|
||||
fmt.Printf("warning: cannot set entry breakpoint: %v\n", err)
|
||||
} else {
|
||||
if err := s.Continue(); err != nil {
|
||||
fmt.Printf("warning: continue to entry: %v\n", err)
|
||||
}
|
||||
regs, _ := s.GetRegs()
|
||||
bm.HandleTrap(®s)
|
||||
// Remove the temporary entry breakpoint.
|
||||
bm.Clear(entryAddr)
|
||||
_ = bp
|
||||
}
|
||||
|
||||
fmt.Printf("stopped at function entry: %#x (%d bytes)\n", entryAddr, funcSize)
|
||||
fmt.Println("commands: break <label|addr> | step [n] | continue | regs | x <addr> [len] | labels | quit")
|
||||
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
|
||||
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))
|
||||
|
||||
case "step", "s":
|
||||
n := 1
|
||||
if len(parts) > 1 {
|
||||
n, _ = strconv.Atoi(parts[1])
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
if s.Exited() {
|
||||
fmt.Println("debuggee exited")
|
||||
break
|
||||
}
|
||||
if err := s.Step(); err != nil {
|
||||
fmt.Println(err)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !s.Exited() {
|
||||
regs, _ := s.GetRegs()
|
||||
fmt.Printf("=> %#x (func+%#x)\n", regs.RIP, regs.RIP-codeBase-uint64(funcOffset))
|
||||
}
|
||||
|
||||
case "continue", "c":
|
||||
if s.Exited() {
|
||||
fmt.Println("debuggee exited")
|
||||
continue
|
||||
}
|
||||
// Re-insert all breakpoints before continuing.
|
||||
for _, bp := range bm.All() {
|
||||
bm.Reinsert(bp.Addr)
|
||||
}
|
||||
if err := s.Continue(); err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
regs, _ := s.GetRegs()
|
||||
if bp := bm.HandleTrap(®s); bp != nil {
|
||||
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))
|
||||
} else if !s.Exited() {
|
||||
fmt.Printf("stopped at %#x\n", regs.RIP)
|
||||
}
|
||||
|
||||
case "break", "b":
|
||||
if len(parts) < 2 {
|
||||
fmt.Println("usage: break <label|addr>")
|
||||
continue
|
||||
}
|
||||
addr, label := resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
||||
if addr == 0 {
|
||||
fmt.Printf("unknown label or address: %s\n", parts[1])
|
||||
continue
|
||||
}
|
||||
bp, err := bm.Set(addr, label)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("breakpoint set: %s at %#x (func+%#x)\n", bp.Label, bp.Addr, bp.Addr-codeBase-uint64(funcOffset))
|
||||
|
||||
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.RIP // default: current PC
|
||||
length := 64
|
||||
if len(parts) > 1 {
|
||||
addr, _ = resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
||||
}
|
||||
if len(parts) > 2 {
|
||||
length, _ = strconv.Atoi(parts[2])
|
||||
}
|
||||
mem, err := s.ReadMemory(addr, length)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
hexDump(addr, mem)
|
||||
|
||||
case "labels", "l":
|
||||
sorted := make([]Label, len(labels))
|
||||
copy(sorted, labels)
|
||||
sort.Slice(sorted, func(i, j int) bool { return sorted[i].Offset < sorted[j].Offset })
|
||||
for _, l := range sorted {
|
||||
fmt.Printf(" func+%#04x %s\n", l.Offset, l.Name)
|
||||
}
|
||||
|
||||
default:
|
||||
fmt.Printf("unknown command: %s\n", cmd)
|
||||
}
|
||||
}
|
||||
s.Kill()
|
||||
}
|
||||
|
||||
func printRegs(regs *Regs, codeBase, funcOff uint64) {
|
||||
fmt.Printf(" RIP = %#016x (func+%#x)\n", regs.RIP, regs.RIP-codeBase-funcOff)
|
||||
fmt.Printf(" RSP = %#016x RBP = %#016x\n", regs.RSP, regs.RBP)
|
||||
fmt.Printf(" RAX = %#016x RBX = %#016x\n", regs.RAX, regs.RBX)
|
||||
fmt.Printf(" RCX = %#016x RDX = %#016x\n", regs.RCX, regs.RDX)
|
||||
fmt.Printf(" RSI = %#016x RDI = %#016x\n", regs.RSI, regs.RDI)
|
||||
fmt.Printf(" R8 = %#016x R9 = %#016x\n", regs.R8, regs.R9)
|
||||
fmt.Printf(" R10 = %#016x R11 = %#016x\n", regs.R10, regs.R11)
|
||||
fmt.Printf(" R12 = %#016x R13 = %#016x\n", regs.R12, regs.R13)
|
||||
fmt.Printf(" R14 = %#016x R15 = %#016x\n", regs.R14, regs.R15)
|
||||
fmt.Printf(" RFLAGS = %#x\n", regs.RFLAGS)
|
||||
}
|
||||
|
||||
func hexDump(addr uint64, data []byte) {
|
||||
for i := 0; i < len(data); i += 16 {
|
||||
end := i + 16
|
||||
if end > len(data) {
|
||||
end = 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) {
|
||||
// Try as a hex address.
|
||||
if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
|
||||
v, err := strconv.ParseUint(s, 0, 64)
|
||||
if err == nil {
|
||||
return v, ""
|
||||
}
|
||||
}
|
||||
// Try as func+offset.
|
||||
if strings.HasPrefix(s, "+") {
|
||||
off, err := strconv.ParseUint(s[1:], 0, 64)
|
||||
if err == nil {
|
||||
return codeBase + funcOff + off, fmt.Sprintf("func+%#x", off)
|
||||
}
|
||||
}
|
||||
// Try as a label name.
|
||||
for _, l := range labels {
|
||||
if l.Name == s {
|
||||
return codeBase + funcOff + uint64(l.Offset), l.Name
|
||||
}
|
||||
}
|
||||
return 0, ""
|
||||
}
|
||||
Reference in New Issue
Block a user