// Copyright (c) 2026 Petr Balvín (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 | step [n] | continue | regs | x [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 ") 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 ") 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, "" }