// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause //go:build linux package debug import ( "bufio" "fmt" "io" "sort" "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 | step [n] | continue | disas [n] | regs | where | x [len] | w | 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 strings.HasPrefix(strings.ToLower(text), "call") || strings.HasPrefix(strings.ToLower(text), "bl") { 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 } 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.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 } 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 [if ]") 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] if val, err := strconv.ParseUint(operand, 0, 64); 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 if ") continue } bp, err := bm.SetWithCond(addr, label, cond) if err != nil { fmt.Println(err) continue } condStr := "" if cond != nil { condStr = fmt.Sprintf(" if %s %s %#x", cond.Reg, cond.Op, cond.Value) } 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 ") 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 { length, _ = strconv.Atoi(parts[2]) } 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 [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 ") 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": 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) } 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 [if ] set a breakpoint delete remove a breakpoint info break list all breakpoints watch [r|w] [size] set a hardware watchpoint (write by default) unwatch [] 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 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 [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 ' 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 > 3 { fmt.Println("usage: unwatch []") 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 }