feat(debug): multi-architecture debugger support for arm64, riscv64, loong64

Assisted-by: MiMo V2.5 Pro
This commit is contained in:
2026-08-21 00:35:21 +02:00
parent 3acbdd6533
commit de9e211ff1
32 changed files with 2276 additions and 659 deletions
+29 -124
View File
@@ -1,7 +1,7 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && amd64
//go:build linux
package debug
@@ -26,15 +26,10 @@ type SourceLine struct {
Line int
}
// 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.
// REPL runs the interactive debugger loop.
func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, argsSize int, labels []Label, lines []SourceLine) {
entryAddr := codeBase + uint64(funcOffset)
// The debuggee is already stopped at the function entry point.
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")
@@ -64,7 +59,6 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
continue
}
printRegs(&regs, codeBase, uint64(funcOffset))
// Also show vector registers.
vregs, err := s.GetVectorRegs()
if err != nil {
fmt.Printf(" (vector regs unavailable: %v)\n", err)
@@ -89,24 +83,22 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
}
if !s.Exited() {
regs, _ := s.GetRegs()
text, _, _ := s.Disassemble(regs.RIP)
fmt.Printf("=> %#x (func+%#x): %s\n", regs.RIP, regs.RIP-codeBase-uint64(funcOffset), text)
pc := regs.GetPC()
text, _, _ := s.Disassemble(pc)
fmt.Printf("=> %#x (func+%#x): %s\n", pc, pc-codeBase-uint64(funcOffset), text)
}
case "next", "n":
// Step over: if the current instruction is a CALL, set a
// breakpoint after it and continue; otherwise single-step.
regs, _ := s.GetRegs()
text, instLen, _ := s.Disassemble(regs.RIP)
if strings.HasPrefix(strings.ToLower(text), "call") {
// Set a temporary breakpoint after the CALL.
afterAddr := regs.RIP + uint64(instLen)
pc := regs.GetPC()
text, instLen, _ := s.Disassemble(pc)
if strings.HasPrefix(strings.ToLower(text), "call") || strings.HasPrefix(strings.ToLower(text), "bl") {
afterAddr := pc + uint64(instLen)
bp, err := bm.Set(afterAddr, "(next)")
if err != nil {
fmt.Printf("cannot set next breakpoint: %v\n", err)
continue
}
// Continue until the breakpoint.
for _, b := range bm.All() {
bm.Reinsert(b.Addr)
}
@@ -119,7 +111,6 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
bm.Clear(afterAddr)
_ = bp
} else {
// Not a CALL — just single-step.
if err := s.Step(); err != nil {
fmt.Println(err)
continue
@@ -127,26 +118,23 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
}
if !s.Exited() {
regs, _ := s.GetRegs()
text, _, _ := s.Disassemble(regs.RIP)
fmt.Printf("=> %#x (func+%#x): %s\n", regs.RIP, regs.RIP-codeBase-uint64(funcOffset), text)
pc := regs.GetPC()
text, _, _ := s.Disassemble(pc)
fmt.Printf("=> %#x (func+%#x): %s\n", pc, pc-codeBase-uint64(funcOffset), text)
}
case "finish", "fin":
// Run until the current function returns.
// For NOSPLIT frame=0: return address is at [RSP].
regs, _ := s.GetRegs()
retAddr, err := s.Peek(regs.RSP)
retAddr, err := archReturnAddr(s, &regs)
if err != nil {
fmt.Printf("cannot read return address: %v\n", err)
continue
}
// Set a temporary breakpoint at the return address.
bp, err := bm.Set(retAddr, "(finish)")
if err != nil {
fmt.Printf("cannot set finish breakpoint: %v\n", err)
continue
}
// Continue until the breakpoint.
for _, b := range bm.All() {
bm.Reinsert(b.Addr)
}
@@ -164,7 +152,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
fmt.Println("debuggee exited")
} else {
regs, _ := s.GetRegs()
fmt.Printf("finished, now at %#x\n", regs.RIP)
fmt.Printf("finished, now at %#x\n", regs.GetPC())
}
case "continue", "c":
@@ -172,9 +160,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
fmt.Println("debuggee exited")
continue
}
// Loop: continue until a breakpoint fires (condition met) or exit.
for {
// Re-insert all breakpoints before continuing.
for _, bp := range bm.All() {
bm.Reinsert(bp.Addr)
}
@@ -186,7 +172,6 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
fmt.Println("debuggee exited")
break
}
// Check for watchpoint hits.
reason, wpAddr := s.StopInfo()
if reason == StopWatchpoint {
fmt.Printf("watchpoint hit at %#x\n", wpAddr)
@@ -201,7 +186,6 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
fmt.Printf("breakpoint hit: %s (func+%#x)\n", name, bp.Addr-codeBase-uint64(funcOffset))
break
}
// Condition not met (or single-step trap) — re-insert and continue.
}
case "break", "b":
@@ -209,11 +193,9 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
fmt.Println("usage: break <label|addr|line> [if <reg> <op> <val>]")
continue
}
// Try as a line number first.
var addr uint64
var label string
if lineNum, err := strconv.Atoi(parts[1]); err == nil && lineNum > 0 {
// Find the byte offset for this line.
off := offsetForLine(lines, lineNum)
if off < 0 {
fmt.Printf("no instruction at line %d\n", lineNum)
@@ -228,7 +210,6 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
fmt.Printf("unknown label, address, or line: %s\n", parts[1])
continue
}
// Parse optional condition: "if <reg> <op> <value>"
var cond *Condition
if len(parts) >= 6 && parts[2] == "if" {
val, err := strconv.ParseUint(parts[5], 0, 64)
@@ -282,7 +263,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
case "x":
regs, _ := s.GetRegs()
addr := regs.RIP // default: current PC
addr := regs.GetPC()
length := 64
if len(parts) > 1 {
addr, _ = resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
@@ -314,7 +295,6 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
fmt.Printf("invalid value: %s\n", arg)
continue
}
// Write as 8-byte word if it looks like a large value, else single byte.
if v > 255 {
for j := 0; j < 8; j++ {
bytes = append(bytes, byte(v>>(8*j)))
@@ -364,11 +344,11 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
}
}
regs, _ := s.GetRegs()
fmt.Print(s.DisassembleN(regs.RIP, n))
fmt.Print(s.DisassembleN(regs.GetPC(), n))
case "where":
regs, _ := s.GetRegs()
funcOff := int(regs.RIP - codeBase - uint64(funcOffset))
funcOff := int(regs.GetPC() - codeBase - uint64(funcOffset))
line := lineAt(lines, funcOff)
label := nearestLabel(labels, funcOff)
fmt.Printf(" func+%#x", funcOff)
@@ -381,32 +361,32 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
fmt.Println()
case "help", "h", "?":
fmt.Println(` break <label|addr> [if <reg> <op> <val>] set a breakpoint
fmt.Printf(` break <label|addr> [if <reg> <op> <val>] set a breakpoint
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
next, n step over CALL/BL
continue, c run until breakpoint or exit
disas [n], u disassemble n instructions at PC
regs print registers and RFLAGS
regs print registers
where show source line and nearest label
stack show stack near RSP (args + return address)
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`)
quit, q kill debuggee and exit`, archSPLabel())
case "stack":
regs, _ := s.GetRegs()
// For NOSPLIT frame=0: [RSP] = return address, [RSP+8..] = args.
retAddr, _ := s.Peek(regs.RSP)
fmt.Printf(" [RSP] return addr = %#x\n", retAddr)
sp := regs.GetSP()
retAddr, _ := archReturnAddr(s, &regs)
fmt.Printf(" [%s] return addr = %#x\n", archSPLabel(), retAddr)
if argsSize > 0 {
fmt.Printf(" args (%d bytes at RSP+8):\n", argsSize)
argBytes, err := s.ReadMemory(regs.RSP+8, argsSize)
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
@@ -420,7 +400,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
case "bt", "backtrace":
regs, _ := s.GetRegs()
funcOff := int(regs.RIP - codeBase - uint64(funcOffset))
funcOff := int(regs.GetPC() - codeBase - uint64(funcOffset))
line := lineAt(lines, funcOff)
label := nearestLabel(labels, funcOff)
fmt.Printf(" #0 func+%#x", funcOff)
@@ -431,7 +411,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
fmt.Printf(" [line %d]", line)
}
fmt.Println()
retAddr, _ := s.Peek(regs.RSP)
retAddr, _ := archReturnAddr(s, &regs)
fmt.Printf(" #1 return to %#x\n", retAddr)
case "watch":
@@ -499,74 +479,6 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
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 [%s]\n", regs.RFLAGS, decodeRflags(regs.RFLAGS))
}
// printVectorRegs displays the YMM registers.
func printVectorRegs(v *VectorRegs) {
fmt.Println("\n Vector registers (YMM):")
for i := 0; i < 16; i += 2 {
fmt.Printf(" YMM%-2d = ", i)
printYMM(v.YMM[i][:])
fmt.Printf(" YMM%-2d = ", i+1)
printYMM(v.YMM[i+1][:])
fmt.Println()
}
}
func printYMM(b []byte) {
// Show as 8 32-bit values.
for j := 0; j < 32; j += 4 {
v := uint32(b[j]) | uint32(b[j+1])<<8 | uint32(b[j+2])<<16 | uint32(b[j+3])<<24
fmt.Printf("%08x ", v)
}
}
func decodeRflags(f uint64) string {
var flags string
if f&1 != 0 {
flags += "CF "
}
if f&(1<<2) != 0 {
flags += "PF "
}
if f&(1<<4) != 0 {
flags += "AF "
}
if f&(1<<6) != 0 {
flags += "ZF "
}
if f&(1<<7) != 0 {
flags += "SF "
}
if f&(1<<8) != 0 {
flags += "TF "
}
if f&(1<<9) != 0 {
flags += "IF "
}
if f&(1<<10) != 0 {
flags += "DF "
}
if f&(1<<11) != 0 {
flags += "OF "
}
if flags == "" {
return "none"
}
return flags[:len(flags)-1] // trim trailing space
}
func hexDump(addr uint64, data []byte) {
for i := 0; i < len(data); i += 16 {
end := i + 16
@@ -594,21 +506,18 @@ func hexDump(addr uint64, data []byte) {
}
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
@@ -617,7 +526,6 @@ func resolveAddr(s string, codeBase, funcOff uint64, labels []Label) (uint64, st
return 0, ""
}
// lineAt returns the source line for a given function-relative offset.
func lineAt(lines []SourceLine, offset int) int {
if len(lines) == 0 {
return 0
@@ -637,8 +545,6 @@ func lineAt(lines []SourceLine, offset int) int {
return 0
}
// offsetForLine returns the byte offset for a given source line number.
// Returns -1 if no instruction is at that line.
func offsetForLine(lines []SourceLine, line int) int {
for _, le := range lines {
if le.Line == line {
@@ -648,7 +554,6 @@ func offsetForLine(lines []SourceLine, line int) int {
return -1
}
// nearestLabel returns the name of the label at or just before the offset.
func nearestLabel(labels []Label, offset int) string {
best := ""
bestOff := -1