From de9e211ff1747cf0acef1f4e5de27366a308aac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Thu, 20 Aug 2026 23:57:10 +0200 Subject: [PATCH] feat(debug): multi-architecture debugger support for arm64, riscv64, loong64 Assisted-by: MiMo V2.5 Pro --- cmd/gasm/debug_linux.go | 193 ++++++++++++++++++ cmd/gasm/debug_other.go | 4 +- debug/debug_test.go | 9 +- debug/disasm_linux_arm64.go | 46 +++++ debug/disasm_linux_loong64.go | 46 +++++ debug/disasm_linux_riscv64.go | 46 +++++ debug/display_linux_amd64.go | 82 ++++++++ debug/display_linux_arm64.go | 45 +++++ debug/display_linux_loong64.go | 40 ++++ debug/display_linux_riscv64.go | 40 ++++ debug/ptrace_linux.go | 291 +++++++++++++++++++++++++++ debug/ptrace_linux_amd64.go | 314 ------------------------------ debug/ptrace_linux_arm64.go | 85 ++++++++ debug/ptrace_linux_loong64.go | 76 ++++++++ debug/ptrace_linux_riscv64.go | 76 ++++++++ debug/regs_linux_loong64.go | 130 +++++++++++++ debug/regs_linux_riscv64.go | 130 +++++++++++++ debug/repl.go | 153 +++------------ debug/stopinfo_linux.go | 69 +++++++ debug/stopinfo_linux_amd64.go | 64 +----- debug/stopinfo_linux_arm64.go | 87 +++++++++ debug/stopinfo_linux_loong64.go | 85 ++++++++ debug/stopinfo_linux_riscv64.go | 85 ++++++++ debug/target_linux.go | 99 ++++++++++ debug/target_linux_amd64.go | 122 +----------- debug/target_linux_arm64.go | 108 ++++++++++ debug/target_linux_loong64.go | 108 ++++++++++ debug/target_linux_riscv64.go | 108 ++++++++++ debug/watchpoint_linux_amd64.go | 53 ++--- debug/watchpoint_linux_arm64.go | 47 +++++ debug/watchpoint_linux_loong64.go | 47 +++++ debug/watchpoint_linux_riscv64.go | 47 +++++ 32 files changed, 2276 insertions(+), 659 deletions(-) create mode 100644 cmd/gasm/debug_linux.go create mode 100644 debug/disasm_linux_arm64.go create mode 100644 debug/disasm_linux_loong64.go create mode 100644 debug/disasm_linux_riscv64.go create mode 100644 debug/display_linux_amd64.go create mode 100644 debug/display_linux_arm64.go create mode 100644 debug/display_linux_loong64.go create mode 100644 debug/display_linux_riscv64.go create mode 100644 debug/ptrace_linux.go create mode 100644 debug/ptrace_linux_arm64.go create mode 100644 debug/ptrace_linux_loong64.go create mode 100644 debug/ptrace_linux_riscv64.go create mode 100644 debug/regs_linux_loong64.go create mode 100644 debug/regs_linux_riscv64.go create mode 100644 debug/stopinfo_linux.go create mode 100644 debug/stopinfo_linux_arm64.go create mode 100644 debug/stopinfo_linux_loong64.go create mode 100644 debug/stopinfo_linux_riscv64.go create mode 100644 debug/target_linux.go create mode 100644 debug/target_linux_arm64.go create mode 100644 debug/target_linux_loong64.go create mode 100644 debug/target_linux_riscv64.go create mode 100644 debug/watchpoint_linux_arm64.go create mode 100644 debug/watchpoint_linux_loong64.go create mode 100644 debug/watchpoint_linux_riscv64.go diff --git a/cmd/gasm/debug_linux.go b/cmd/gasm/debug_linux.go new file mode 100644 index 0000000..a7062a1 --- /dev/null +++ b/cmd/gasm/debug_linux.go @@ -0,0 +1,193 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux + +package main + +import ( + "fmt" + "os" + "sort" + "strings" + + "sourcedock.dev/petrbalvin/gasm-devkit/debug" + "sourcedock.dev/petrbalvin/gasm-devkit/verify" +) + +func cmdDebug(args []string) int { + fs := newCommand("debug", "gasm debug --func ", ` +Interactive debugger for JIT-assembled amd64 functions. Launches the +function in a traced subprocess (ptrace), then provides a REPL for +single-stepping, breakpoints, register and memory inspection. + +REPL commands: + break set a breakpoint at a label or absolute address + step [n] single-step n instructions (default 1) + continue run until next breakpoint or exit + regs print general-purpose registers + x [addr] [len] hex-dump memory (default: current PC, 64 bytes) + labels list function labels and offsets + quit kill the debuggee and exit +`) + target := fs.Bool("target", false, "") // hidden: debuggee subprocess mode + funcName := fs.String("func", "", "function to debug") + argsFile := fs.String("args", "", "file containing the ABI0 argument block") + bufSpec := fs.String("buf", "", "buffer specification: name:size:pattern[,name:size:pattern...] where pattern is zero, ones, seq, or hex") + fs.Parse(args) + + // --- Debuggee mode (internal, spawned by the debugger) --- + if *target { + tmpDir := os.Getenv("GASM_DEBUG_TMP") + if tmpDir == "" || fs.NArg() < 1 || *funcName == "" || *argsFile == "" { + fmt.Fprintln(os.Stderr, "gasm debug --target: internal mode") + return 2 + } + if err := debug.RunTarget(fs.Arg(0), *funcName, *argsFile, tmpDir); err != nil { + fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err) + return 1 + } + return 0 + } + + // --- Debugger mode (interactive REPL) --- + if fs.NArg() < 1 || *funcName == "" { + fmt.Fprintln(os.Stderr, "usage: gasm debug --func ") + return 2 + } + path := fs.Arg(0) + + // Load the kernel to extract function metadata and labels. + k, err := verify.Load(path) + if err != nil { + fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err) + return 1 + } + defer k.Close() + + fl, err := k.Func(*funcName) + if err != nil { + fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err) + return 1 + } + + // Build the label list for the REPL. + var labels []debug.Label + for name, off := range fl.Labels { + labels = append(labels, debug.Label{Name: name, Offset: off}) + } + sort.Slice(labels, func(i, j int) bool { return labels[i].Offset < labels[j].Offset }) + + // Launch the debuggee with the argument block. + var argBlock []byte + var bufAddrs []uint64 + var sess *debug.Session + if *bufSpec != "" { + // Parse the function signature to determine argument layout. + src, err := readSource(path) + if err != nil { + fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err) + return 1 + } + sig, ok := verify.ExtractFuncSig(src, *funcName) + if !ok { + fmt.Fprintf(os.Stderr, "gasm debug: no // func signature found for %s\n", *funcName) + return 1 + } + layout := verify.ArgLayout(sig) + + // Parse the buffer spec to get buffer names. + bufNames := parseBufNames(*bufSpec) + + // Allocate buffers in the debuggee. + argBlock = make([]byte, fl.Args) + sess, bufAddrs, err = debug.LaunchWithBuffers("", path, *funcName, argBlock, *bufSpec) + if err != nil { + fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err) + return 1 + } + + // Construct the argument block with buffer pointers at the correct positions. + bufIdx := 0 + for _, arg := range layout { + if !arg.IsPtr { + continue + } + // Find the buffer that matches this argument. + for i, name := range bufNames { + if i < len(bufAddrs) && (name == arg.Name || strings.HasPrefix(arg.Name, name)) { + addr := bufAddrs[i] + off := arg.Offset + if off+8 <= len(argBlock) { + argBlock[off] = byte(addr) + argBlock[off+1] = byte(addr >> 8) + argBlock[off+2] = byte(addr >> 16) + argBlock[off+3] = byte(addr >> 24) + argBlock[off+4] = byte(addr >> 32) + argBlock[off+5] = byte(addr >> 40) + argBlock[off+6] = byte(addr >> 48) + argBlock[off+7] = byte(addr >> 56) + } + // For slices, also set the length and capacity. + if strings.HasPrefix(arg.Typ, "[]") && off+24 <= len(argBlock) { + // Find the buffer size from the spec. + size := parseBufSize(*bufSpec, name) + // Length at offset+8, capacity at offset+16. + for j := 0; j < 8; j++ { + argBlock[off+8+j] = byte(size >> (j * 8)) + argBlock[off+16+j] = byte(size >> (j * 8)) + } + } + bufIdx++ + break + } + } + } + _ = bufIdx + } else { + argBlock = make([]byte, fl.Args) + sess, err = debug.Launch("", path, *funcName, argBlock) + if err != nil { + fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err) + return 1 + } + } + defer sess.Kill() + + bm := debug.NewBreakpoints(sess) + fmt.Printf("gasm debug: %s in %s (pid %d)\n", *funcName, path, sess.Pid()) + + // Convert the line table for the REPL. + var srcLines []debug.SourceLine + for _, le := range fl.Lines { + srcLines = append(srcLines, debug.SourceLine{Offset: le.Offset, Line: le.Line}) + } + debug.REPL(sess, bm, sess.CodeBase(), fl.Offset, fl.Size, fl.Args, labels, srcLines) + return 0 +} + +// parseBufNames extracts buffer names from a buffer specification. +// Format: name:size:pattern[,name:size:pattern...] +func parseBufNames(spec string) []string { + var names []string + for _, part := range strings.Split(spec, ",") { + fields := strings.SplitN(part, ":", 3) + if len(fields) >= 1 && fields[0] != "" { + names = append(names, fields[0]) + } + } + return names +} + +// parseBufSize extracts the size of a named buffer from a buffer specification. +func parseBufSize(spec, name string) int { + for _, part := range strings.Split(spec, ",") { + fields := strings.SplitN(part, ":", 3) + if len(fields) >= 2 && fields[0] == name { + var size int + fmt.Sscanf(fields[1], "%d", &size) + return size + } + } + return 0 +} diff --git a/cmd/gasm/debug_other.go b/cmd/gasm/debug_other.go index 833a697..825f9f9 100644 --- a/cmd/gasm/debug_other.go +++ b/cmd/gasm/debug_other.go @@ -1,7 +1,7 @@ // Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause -//go:build !(linux && amd64) +//go:build !linux package main @@ -11,6 +11,6 @@ import ( ) func cmdDebug(args []string) int { - fmt.Fprintln(os.Stderr, "gasm debug: the interactive debugger requires linux/amd64 (ptrace)") + fmt.Fprintln(os.Stderr, "gasm debug: the interactive debugger requires Linux (ptrace)") return 1 } diff --git a/debug/debug_test.go b/debug/debug_test.go index eee5d02..ce4225d 100644 --- a/debug/debug_test.go +++ b/debug/debug_test.go @@ -1,6 +1,8 @@ // Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause +//go:build linux && amd64 + package debug import ( @@ -273,6 +275,7 @@ func TestBreakpointInfo(t *testing.T) { } func TestWatchpointSlotTracking(t *testing.T) { + wpSlots = [4]bool{} // reset s := &Session{} // All four slots are free initially. @@ -286,8 +289,8 @@ func TestWatchpointSlotTracking(t *testing.T) { } // Manually mark slots 0 and 2 as used (simulating successful SetWatchpoint). - s.wpSlots[0] = true - s.wpSlots[2] = true + wpSlots[0] = true + wpSlots[2] = true if !s.IsWatchpointSlotUsed(0) { t.Error("slot 0 should be in use") @@ -315,7 +318,7 @@ func TestWatchpointSlotTracking(t *testing.T) { // Mark all slots used: FindFreeWatchpointSlot returns -1. for i := 0; i < 4; i++ { - s.wpSlots[i] = true + wpSlots[i] = true } if got := s.FindFreeWatchpointSlot(); got != -1 { t.Errorf("FindFreeWatchpointSlot() with all slots used = %d, want -1", got) diff --git a/debug/disasm_linux_arm64.go b/debug/disasm_linux_arm64.go new file mode 100644 index 0000000..91a9fde --- /dev/null +++ b/debug/disasm_linux_arm64.go @@ -0,0 +1,46 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && arm64 + +package debug + +import ( + "fmt" + + "golang.org/x/arch/arm64/arm64asm" +) + +// Disassemble decodes the instruction at the given address in the debuggee's +// memory and returns its text representation and length in bytes. +func (s *Session) Disassemble(addr uint64) (string, int, error) { + mem, err := s.ReadMemory(addr, 4) + if err != nil { + return "", 0, err + } + inst, err := arm64asm.Decode(mem) + if err != nil { + return "???", 4, nil + } + text := arm64asm.GoSyntax(inst, addr, nil, nil) + return text, 4, nil +} + +// DisassembleN decodes up to n instructions starting at addr. +func (s *Session) DisassembleN(addr uint64, n int) string { + var result string + pc := addr + for i := 0; i < n; i++ { + text, length, err := s.Disassemble(pc) + if err != nil { + result += fmt.Sprintf(" %#08x: \n", pc, err) + break + } + result += fmt.Sprintf(" %#08x: %s\n", pc, text) + if length == 0 { + length = 4 + } + pc += uint64(length) + } + return result +} diff --git a/debug/disasm_linux_loong64.go b/debug/disasm_linux_loong64.go new file mode 100644 index 0000000..942aa71 --- /dev/null +++ b/debug/disasm_linux_loong64.go @@ -0,0 +1,46 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && loong64 + +package debug + +import ( + "fmt" + + "golang.org/x/arch/loong64/loong64asm" +) + +// Disassemble decodes the instruction at the given address in the debuggee's +// memory and returns its text representation and length in bytes. +func (s *Session) Disassemble(addr uint64) (string, int, error) { + mem, err := s.ReadMemory(addr, 4) + if err != nil { + return "", 0, err + } + inst, err := loong64asm.Decode(mem) + if err != nil { + return "???", 4, nil + } + text := loong64asm.GoSyntax(inst, addr, nil, nil) + return text, 4, nil +} + +// DisassembleN decodes up to n instructions starting at addr. +func (s *Session) DisassembleN(addr uint64, n int) string { + var result string + pc := addr + for i := 0; i < n; i++ { + text, length, err := s.Disassemble(pc) + if err != nil { + result += fmt.Sprintf(" %#08x: \n", pc, err) + break + } + result += fmt.Sprintf(" %#08x: %s\n", pc, text) + if length == 0 { + length = 4 + } + pc += uint64(length) + } + return result +} diff --git a/debug/disasm_linux_riscv64.go b/debug/disasm_linux_riscv64.go new file mode 100644 index 0000000..4f0cc91 --- /dev/null +++ b/debug/disasm_linux_riscv64.go @@ -0,0 +1,46 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && riscv64 + +package debug + +import ( + "fmt" + + "golang.org/x/arch/riscv64/riscv64asm" +) + +// Disassemble decodes the instruction at the given address in the debuggee's +// memory and returns its text representation and length in bytes. +func (s *Session) Disassemble(addr uint64) (string, int, error) { + mem, err := s.ReadMemory(addr, 4) + if err != nil { + return "", 0, err + } + inst, err := riscv64asm.Decode(mem) + if err != nil { + return "???", 4, nil + } + text := riscv64asm.GoSyntax(inst, addr, nil, nil) + return text, inst.Len, nil +} + +// DisassembleN decodes up to n instructions starting at addr. +func (s *Session) DisassembleN(addr uint64, n int) string { + var result string + pc := addr + for i := 0; i < n; i++ { + text, length, err := s.Disassemble(pc) + if err != nil { + result += fmt.Sprintf(" %#08x: \n", pc, err) + break + } + result += fmt.Sprintf(" %#08x: %s\n", pc, text) + if length == 0 { + length = 4 + } + pc += uint64(length) + } + return result +} diff --git a/debug/display_linux_amd64.go b/debug/display_linux_amd64.go new file mode 100644 index 0000000..507f4dc --- /dev/null +++ b/debug/display_linux_amd64.go @@ -0,0 +1,82 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && amd64 + +package debug + +import "fmt" + +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)) +} + +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) { + 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] +} + +// archReturnAddr reads the return address from the stack (amd64 ABI0 convention). +func archReturnAddr(s *Session, regs *Regs) (uint64, error) { + return s.Peek(regs.GetSP()) +} + +// archSPLabel returns the SP register name for display. +func archSPLabel() string { return "RSP" } diff --git a/debug/display_linux_arm64.go b/debug/display_linux_arm64.go new file mode 100644 index 0000000..d116cd0 --- /dev/null +++ b/debug/display_linux_arm64.go @@ -0,0 +1,45 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && arm64 + +package debug + +import "fmt" + +func printRegs(regs *Regs, codeBase, funcOff uint64) { + fmt.Printf(" PC = %#016x (func+%#x)\n", regs.PC, regs.PC-codeBase-funcOff) + fmt.Printf(" SP = %#016x FP = %#016x\n", regs.SP, regs.X29) + fmt.Printf(" LR = %#016x\n", regs.X30) + fmt.Printf(" X0 = %#016x X1 = %#016x\n", regs.X0, regs.X1) + fmt.Printf(" X2 = %#016x X3 = %#016x\n", regs.X2, regs.X3) + fmt.Printf(" X4 = %#016x X5 = %#016x\n", regs.X4, regs.X5) + fmt.Printf(" X6 = %#016x X7 = %#016x\n", regs.X6, regs.X7) + fmt.Printf(" X8 = %#016x X9 = %#016x\n", regs.X8, regs.X9) + fmt.Printf(" X10 = %#016x X11 = %#016x\n", regs.X10, regs.X11) + fmt.Printf(" X12 = %#016x X13 = %#016x\n", regs.X12, regs.X13) + fmt.Printf(" X14 = %#016x X15 = %#016x\n", regs.X14, regs.X15) + fmt.Printf(" X16 = %#016x X17 = %#016x\n", regs.X16, regs.X17) + fmt.Printf(" X18 = %#016x X19 = %#016x\n", regs.X18, regs.X19) + fmt.Printf(" X20 = %#016x X21 = %#016x\n", regs.X20, regs.X21) + fmt.Printf(" X22 = %#016x X23 = %#016x\n", regs.X22, regs.X23) + fmt.Printf(" X24 = %#016x X25 = %#016x\n", regs.X24, regs.X25) + fmt.Printf(" X26 = %#016x X27 = %#016x\n", regs.X26, regs.X27) + fmt.Printf(" X28 = %#016x PSTATE = %#x\n", regs.X28, regs.PSTATE) +} + +func printVectorRegs(v *VectorRegs) { + fmt.Println("\n Vector registers (V0-V31):") + for i := 0; i < 32; i += 2 { + fmt.Printf(" V%-2d = %016x%016x\n", i, v.V[i][8], v.V[i][0]) + fmt.Printf(" V%-2d = %016x%016x\n", i+1, v.V[i+1][8], v.V[i+1][0]) + } +} + +// archReturnAddr reads the return address from LR (arm64 convention). +func archReturnAddr(s *Session, regs *Regs) (uint64, error) { + return regs.X30, nil +} + +// archSPLabel returns the SP register name for display. +func archSPLabel() string { return "SP" } diff --git a/debug/display_linux_loong64.go b/debug/display_linux_loong64.go new file mode 100644 index 0000000..c335a44 --- /dev/null +++ b/debug/display_linux_loong64.go @@ -0,0 +1,40 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && loong64 + +package debug + +import "fmt" + +func printRegs(regs *Regs, codeBase, funcOff uint64) { + fmt.Printf(" PC = %#016x (func+%#x)\n", regs.R31, regs.R31-codeBase-funcOff) + fmt.Printf(" SP = %#016x FP = %#016x\n", regs.R3, regs.R21) + fmt.Printf(" RA = %#016x\n", regs.R1) + fmt.Printf(" A0 = %#016x A1 = %#016x\n", regs.R4, regs.R5) + fmt.Printf(" A2 = %#016x A3 = %#016x\n", regs.R6, regs.R7) + fmt.Printf(" A4 = %#016x A5 = %#016x\n", regs.R8, regs.R9) + fmt.Printf(" A6 = %#016x A7 = %#016x\n", regs.R10, regs.R11) + fmt.Printf(" T0 = %#016x T1 = %#016x\n", regs.R12, regs.R13) + fmt.Printf(" T2 = %#016x T3 = %#016x\n", regs.R14, regs.R15) + fmt.Printf(" T4 = %#016x T5 = %#016x\n", regs.R16, regs.R17) + fmt.Printf(" T6 = %#016x T7 = %#016x\n", regs.R18, regs.R19) + fmt.Printf(" T8 = %#016x\n", regs.R20) + fmt.Printf(" S0 = %#016x S1 = %#016x\n", regs.R22, regs.R23) + fmt.Printf(" S2 = %#016x S3 = %#016x\n", regs.R24, regs.R25) + fmt.Printf(" S4 = %#016x S5 = %#016x\n", regs.R26, regs.R27) + fmt.Printf(" S6 = %#016x S7 = %#016x\n", regs.R28, regs.R29) + fmt.Printf(" S8 = %#016x\n", regs.R30) +} + +func printVectorRegs(v *VectorRegs) { + // No vector registers to display for loong64 yet. +} + +// archReturnAddr reads the return address from RA (loong64 convention). +func archReturnAddr(s *Session, regs *Regs) (uint64, error) { + return regs.R1, nil +} + +// archSPLabel returns the SP register name for display. +func archSPLabel() string { return "SP" } diff --git a/debug/display_linux_riscv64.go b/debug/display_linux_riscv64.go new file mode 100644 index 0000000..3ef12e9 --- /dev/null +++ b/debug/display_linux_riscv64.go @@ -0,0 +1,40 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && riscv64 + +package debug + +import "fmt" + +func printRegs(regs *Regs, codeBase, funcOff uint64) { + fmt.Printf(" PC = %#016x (func+%#x)\n", regs.PC, regs.PC-codeBase-funcOff) + fmt.Printf(" SP = %#016x FP = %#016x\n", regs.Sp, regs.S0) + fmt.Printf(" RA = %#016x\n", regs.Ra) + fmt.Printf(" A0 = %#016x A1 = %#016x\n", regs.A0, regs.A1) + fmt.Printf(" A2 = %#016x A3 = %#016x\n", regs.A2, regs.A3) + fmt.Printf(" A4 = %#016x A5 = %#016x\n", regs.A4, regs.A5) + fmt.Printf(" A6 = %#016x A7 = %#016x\n", regs.A6, regs.A7) + fmt.Printf(" T0 = %#016x T1 = %#016x\n", regs.T0, regs.T1) + fmt.Printf(" T2 = %#016x T3 = %#016x\n", regs.T2, regs.T3) + fmt.Printf(" T4 = %#016x T5 = %#016x\n", regs.T4, regs.T5) + fmt.Printf(" T6 = %#016x\n", regs.T6) + fmt.Printf(" S1 = %#016x S2 = %#016x\n", regs.S1, regs.S2) + fmt.Printf(" S3 = %#016x S4 = %#016x\n", regs.S3, regs.S4) + fmt.Printf(" S5 = %#016x S6 = %#016x\n", regs.S5, regs.S6) + fmt.Printf(" S7 = %#016x S8 = %#016x\n", regs.S7, regs.S8) + fmt.Printf(" S9 = %#016x S10 = %#016x\n", regs.S9, regs.S10) + fmt.Printf(" S11 = %#016x\n", regs.S11) +} + +func printVectorRegs(v *VectorRegs) { + // No vector registers to display for riscv64 yet. +} + +// archReturnAddr reads the return address from RA (riscv64 convention). +func archReturnAddr(s *Session, regs *Regs) (uint64, error) { + return regs.Ra, nil +} + +// archSPLabel returns the SP register name for display. +func archSPLabel() string { return "SP" } diff --git a/debug/ptrace_linux.go b/debug/ptrace_linux.go new file mode 100644 index 0000000..81a6d3e --- /dev/null +++ b/debug/ptrace_linux.go @@ -0,0 +1,291 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux + +package debug + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "time" +) + +// Session is a ptrace debugging session controlling one debuggee process. +type Session struct { + pid int + cmd *exec.Cmd + stopped bool + exited bool + codeBase uint64 // base address of the JIT code in the debuggee +} + +// Launch starts the debuggee subprocess (gasm debug --target ...) and +// attaches to it via ptrace. +func Launch(gasmBin, asmPath, funcName string, args []byte) (*Session, error) { + sess, _, err := LaunchWithBuffers(gasmBin, asmPath, funcName, args, "") + return sess, err +} + +// LaunchWithBuffers is like Launch but also allocates buffers in the debuggee. +func LaunchWithBuffers(gasmBin, asmPath, funcName string, args []byte, bufSpec string) (*Session, []uint64, error) { + self, err := os.Executable() + if err != nil { + return nil, nil, fmt.Errorf("debug: cannot find gasm binary: %w", err) + } + if gasmBin != "" { + self = gasmBin + } + + tmpDir, err := os.MkdirTemp("", "gasm-debug-*") + if err != nil { + return nil, nil, fmt.Errorf("debug: tempdir: %w", err) + } + argsFile := filepath.Join(tmpDir, "args.bin") + if err := os.WriteFile(argsFile, args, 0o644); err != nil { + os.RemoveAll(tmpDir) + return nil, nil, fmt.Errorf("debug: write args: %w", err) + } + + if bufSpec != "" { + if err := os.WriteFile(filepath.Join(tmpDir, "bufspec"), []byte(bufSpec), 0o644); err != nil { + os.RemoveAll(tmpDir) + return nil, nil, fmt.Errorf("debug: write bufspec: %w", err) + } + } + + cmd := exec.Command(self, "debug", "--target", "--func", funcName, "--args", argsFile, asmPath) + cmd.Env = append(os.Environ(), "GASM_DEBUG_TMP="+tmpDir) + cmd.Stdout = nil + cmd.Stderr = os.Stderr + cmd.SysProcAttr = &syscall.SysProcAttr{} + + if err := cmd.Start(); err != nil { + os.RemoveAll(tmpDir) + return nil, nil, fmt.Errorf("debug: start debuggee: %w", err) + } + + s := &Session{pid: cmd.Process.Pid, cmd: cmd} + + readyFile := filepath.Join(tmpDir, "ready") + for i := 0; i < 500; i++ { + if _, err := os.Stat(readyFile); err == nil { + break + } + time.Sleep(5 * time.Millisecond) + } + var ws syscall.WaitStatus + if _, err := syscall.Wait4(s.pid, &ws, syscall.WUNTRACED, nil); err != nil { + cmd.Process.Kill() + os.RemoveAll(tmpDir) + return nil, nil, fmt.Errorf("debug: wait for stop: %w", err) + } + + entryFile := filepath.Join(tmpDir, "entry") + for i := 0; i < 500; i++ { + if _, err := os.Stat(entryFile); err == nil { + break + } + time.Sleep(5 * time.Millisecond) + } + + if err := s.Continue(); err != nil { + return nil, nil, fmt.Errorf("debug: continue to entry: %w", err) + } + + if _, err := syscall.Wait4(s.pid, &ws, syscall.WUNTRACED, nil); err != nil { + return nil, nil, fmt.Errorf("debug: wait for entry: %w", err) + } + + s.stopped = true + + s.codeBase = findRWXMapping(s.pid) + if s.codeBase == 0 { + baseFile := filepath.Join(tmpDir, "codebase") + if data, err := os.ReadFile(baseFile); err == nil { + fmt.Sscanf(string(data), "%d", &s.codeBase) + } + } + + var bufAddrs []uint64 + if bufSpec != "" { + addrFile := filepath.Join(tmpDir, "bufaddrs") + if data, err := os.ReadFile(addrFile); err == nil { + for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") { + var addr uint64 + if _, err := fmt.Sscanf(line, "%d", &addr); err == nil { + bufAddrs = append(bufAddrs, addr) + } + } + } + } + + return s, bufAddrs, nil +} + +// wait waits for the debuggee to stop and returns the wait status. +func (s *Session) wait() error { + var ws syscall.WaitStatus + _, err := syscall.Wait4(s.pid, &ws, 0, nil) + if err != nil { + return err + } + if ws.Exited() { + s.exited = true + return fmt.Errorf("debuggee exited with status %d", ws.ExitStatus()) + } + s.stopped = true + return nil +} + +// Peek reads a word (8 bytes) from the debuggee's memory at addr. +func (s *Session) Peek(addr uint64) (uint64, error) { + mem, err := os.OpenFile(fmt.Sprintf("/proc/%d/mem", s.pid), os.O_RDONLY, 0) + if err != nil { + return 0, fmt.Errorf("debug: open /proc/%d/mem: %w", s.pid, err) + } + defer mem.Close() + buf := make([]byte, 8) + if _, err := mem.ReadAt(buf, int64(addr)); err != nil { + return 0, fmt.Errorf("debug: read mem %#x: %w", addr, err) + } + return uint64(buf[0]) | uint64(buf[1])<<8 | uint64(buf[2])<<16 | uint64(buf[3])<<24 | + uint64(buf[4])<<32 | uint64(buf[5])<<40 | uint64(buf[6])<<48 | uint64(buf[7])<<56, nil +} + +// Poke writes a word (8 bytes) to the debuggee's memory at addr. +func (s *Session) Poke(addr, val uint64) error { + mem, err := os.OpenFile(fmt.Sprintf("/proc/%d/mem", s.pid), os.O_WRONLY, 0) + if err != nil { + return fmt.Errorf("debug: open /proc/%d/mem: %w", s.pid, err) + } + defer mem.Close() + buf := []byte{byte(val), byte(val >> 8), byte(val >> 16), byte(val >> 24), + byte(val >> 32), byte(val >> 40), byte(val >> 48), byte(val >> 56)} + if _, err := mem.WriteAt(buf, int64(addr)); err != nil { + return fmt.Errorf("debug: write mem %#x: %w", addr, err) + } + return nil +} + +// ReadMemory reads len bytes from the debuggee's memory at addr. +func (s *Session) ReadMemory(addr uint64, length int) ([]byte, error) { + out := make([]byte, length) + for i := 0; i < length; i += 8 { + word, err := s.Peek(addr + uint64(i)) + if err != nil { + return out[:i], err + } + for j := 0; j < 8 && i+j < length; j++ { + out[i+j] = byte(word >> (8 * j)) + } + } + return out, nil +} + +// WriteMemory writes bytes to the debuggee's memory at addr. +func (s *Session) WriteMemory(addr uint64, data []byte) error { + for i := 0; i < len(data); i += 8 { + end := i + 8 + if end > len(data) { + end = len(data) + } + var word uint64 + for j := 0; j < end-i; j++ { + word |= uint64(data[i+j]) << (8 * j) + } + if end-i < 8 { + existing, err := s.Peek(addr + uint64(i)) + if err != nil { + return err + } + mask := ^((uint64(1) << (8 * (end - i))) - 1) + word = (existing & mask) | word + } + if err := s.Poke(addr+uint64(i), word); err != nil { + return err + } + } + return nil +} + +// Step executes a single instruction in the debuggee. +func (s *Session) Step() error { + if s.exited { + return fmt.Errorf("debug: debuggee has exited") + } + _, _, errno := syscall.Syscall6( + syscall.SYS_PTRACE, + uintptr(syscall.PTRACE_SINGLESTEP), + uintptr(s.pid), + 0, 0, 0, 0, + ) + if errno != 0 { + return fmt.Errorf("debug: PTRACE_SINGLESTEP: %w", errno) + } + return s.wait() +} + +// Continue resumes execution until the next breakpoint or exit. +func (s *Session) Continue() error { + if s.exited { + return fmt.Errorf("debug: debuggee has exited") + } + _, _, errno := syscall.Syscall6( + syscall.SYS_PTRACE, + uintptr(syscall.PTRACE_CONT), + uintptr(s.pid), + 0, 0, 0, 0, + ) + if errno != 0 { + return fmt.Errorf("debug: PTRACE_CONT: %w", errno) + } + return s.wait() +} + +// Exited returns true if the debuggee has terminated. +func (s *Session) Exited() bool { return s.exited } + +// Pid returns the debuggee's process ID. +func (s *Session) Pid() int { return s.pid } + +// CodeBase returns the base address of the JIT code in the debuggee. +func (s *Session) CodeBase() uint64 { return s.codeBase } + +// Kill terminates the debuggee. +func (s *Session) Kill() { + if !s.exited { + syscall.Kill(s.pid, syscall.SIGKILL) + syscall.Wait4(s.pid, nil, 0, nil) + s.exited = true + } + if s.cmd != nil && s.cmd.Process != nil { + s.cmd.Wait() + } +} + +// findRWXMapping reads /proc/pid/maps and returns the base address of the +// first read-write-execute mapping (the JIT code region). +func findRWXMapping(pid int) uint64 { + data, err := os.ReadFile(fmt.Sprintf("/proc/%d/maps", pid)) + if err != nil { + return 0 + } + for _, line := range strings.Split(string(data), "\n") { + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + perms := fields[1] + if len(perms) >= 3 && perms[0] == 'r' && perms[1] == 'w' && perms[2] == 'x' { + var start uint64 + fmt.Sscanf(fields[0], "%x-", &start) + return start + } + } + return 0 +} diff --git a/debug/ptrace_linux_amd64.go b/debug/ptrace_linux_amd64.go index f314488..3f0dbf9 100644 --- a/debug/ptrace_linux_amd64.go +++ b/debug/ptrace_linux_amd64.go @@ -3,165 +3,14 @@ //go:build linux && amd64 -// Package debug implements the interactive debugger for gasm (Phase 4): -// single-stepping, breakpoints, register and memory inspection for -// JIT-assembled Plan 9 amd64 functions, controlled via ptrace. package debug import ( "fmt" - "os" - "os/exec" - "path/filepath" - "strings" "syscall" - "time" "unsafe" ) -// Session is a ptrace debugging session controlling one debuggee process. -type Session struct { - pid int - cmd *exec.Cmd - stopped bool - exited bool - codeBase uint64 // base address of the JIT code in the debuggee - wpSlots [4]bool // watchpoint slot occupancy (DR0-DR3) -} - -// Launch starts the debuggee subprocess (gasm debug --target ...) and -// attaches to it via ptrace. The debuggee assembles the file, maps the -// JIT code, calls PTRACE_TRACEME and raises SIGSTOP; Launch waits for -// that initial stop and returns a ready Session. -func Launch(gasmBin, asmPath, funcName string, args []byte) (*Session, error) { - sess, _, err := LaunchWithBuffers(gasmBin, asmPath, funcName, args, "") - return sess, err -} - -// LaunchWithBuffers is like Launch but also allocates buffers in the debuggee -// based on the buffer specification. Returns the Session and the buffer -// addresses (in the order they appear in the spec). -func LaunchWithBuffers(gasmBin, asmPath, funcName string, args []byte, bufSpec string) (*Session, []uint64, error) { - self, err := os.Executable() - if err != nil { - return nil, nil, fmt.Errorf("debug: cannot find gasm binary: %w", err) - } - if gasmBin != "" { - self = gasmBin - } - - // Write the arg block to a temp file (the child reads it). - tmpDir, err := os.MkdirTemp("", "gasm-debug-*") - if err != nil { - return nil, nil, fmt.Errorf("debug: tempdir: %w", err) - } - argsFile := filepath.Join(tmpDir, "args.bin") - if err := os.WriteFile(argsFile, args, 0o644); err != nil { - os.RemoveAll(tmpDir) - return nil, nil, fmt.Errorf("debug: write args: %w", err) - } - - // Write the buffer spec if present. - if bufSpec != "" { - if err := os.WriteFile(filepath.Join(tmpDir, "bufspec"), []byte(bufSpec), 0o644); err != nil { - os.RemoveAll(tmpDir) - return nil, nil, fmt.Errorf("debug: write bufspec: %w", err) - } - } - - cmd := exec.Command(self, "debug", "--target", "--func", funcName, "--args", argsFile, asmPath) - cmd.Env = append(os.Environ(), "GASM_DEBUG_TMP="+tmpDir) - cmd.Stdout = nil // output goes to the debugger, not the terminal - cmd.Stderr = os.Stderr - cmd.SysProcAttr = &syscall.SysProcAttr{} - - if err := cmd.Start(); err != nil { - os.RemoveAll(tmpDir) - return nil, nil, fmt.Errorf("debug: start debuggee: %w", err) - } - - s := &Session{pid: cmd.Process.Pid, cmd: cmd} - - // Wait for the child to signal readiness and stop. The child calls - // PTRACE_TRACEME then SIGSTOP, so Wait4 with WUNTRACED observes the - // ptrace-stop directly (no PTRACE_ATTACH needed). - readyFile := filepath.Join(tmpDir, "ready") - for i := 0; i < 500; i++ { - if _, err := os.Stat(readyFile); err == nil { - break - } - time.Sleep(5 * time.Millisecond) - } - var ws syscall.WaitStatus - if _, err := syscall.Wait4(s.pid, &ws, syscall.WUNTRACED, nil); err != nil { - cmd.Process.Kill() - os.RemoveAll(tmpDir) - return nil, nil, fmt.Errorf("debug: wait for stop: %w", err) - } - - // Wait for the debuggee to reach the function entry point. - entryFile := filepath.Join(tmpDir, "entry") - for i := 0; i < 500; i++ { - if _, err := os.Stat(entryFile); err == nil { - break - } - time.Sleep(5 * time.Millisecond) - } - - // Continue the debuggee to the entry point. - if err := s.Continue(); err != nil { - return nil, nil, fmt.Errorf("debug: continue to entry: %w", err) - } - - // Wait for the entry stop. - if _, err := syscall.Wait4(s.pid, &ws, syscall.WUNTRACED, nil); err != nil { - return nil, nil, fmt.Errorf("debug: wait for entry: %w", err) - } - - s.stopped = true - - // Read the code base from /proc/pid/maps (find the RWX mapping). - s.codeBase = findRWXMapping(s.pid) - if s.codeBase == 0 { - // Fallback: try the file the child wrote. - baseFile := filepath.Join(tmpDir, "codebase") - if data, err := os.ReadFile(baseFile); err == nil { - fmt.Sscanf(string(data), "%d", &s.codeBase) - } - } - - // Read buffer addresses if buffers were allocated. - var bufAddrs []uint64 - if bufSpec != "" { - addrFile := filepath.Join(tmpDir, "bufaddrs") - if data, err := os.ReadFile(addrFile); err == nil { - for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") { - var addr uint64 - if _, err := fmt.Sscanf(line, "%d", &addr); err == nil { - bufAddrs = append(bufAddrs, addr) - } - } - } - } - - return s, bufAddrs, nil -} - -// wait waits for the debuggee to stop and returns the wait status. -func (s *Session) wait() error { - var ws syscall.WaitStatus - _, err := syscall.Wait4(s.pid, &ws, 0, nil) - if err != nil { - return err - } - if ws.Exited() { - s.exited = true - return fmt.Errorf("debuggee exited with status %d", ws.ExitStatus()) - } - s.stopped = true - return nil -} - // GetRegs reads the general-purpose registers of the stopped debuggee. func (s *Session) GetRegs() (Regs, error) { var regs Regs @@ -234,179 +83,16 @@ type VectorRegs struct { } // GetVectorRegs retrieves the YMM registers via PTRACE_GETREGSET + XSAVE. -// Falls back to XMM if XSAVE is unavailable. func (s *Session) GetVectorRegs() (VectorRegs, error) { var v VectorRegs fp, err := s.GetFPRegs() if err != nil { return v, err } - // PTRACE_GETFPREGS gives XMM registers (lower 128 bits). - // For YMM we'd need XSAVE; for now, copy XMM and zero the upper half. for i := 0; i < 16; i++ { for j := 0; j < 16; j++ { v.YMM[i][j] = fp.XMM[i][j] } - // Upper 128 bits would come from XSAVE, not available via GETFPREGS. } return v, nil } - -// Peek reads a word (8 bytes) from the debuggee's memory at addr. -// Uses /proc/pid/mem which works reliably with Go's multi-threaded runtime. -func (s *Session) Peek(addr uint64) (uint64, error) { - mem, err := os.OpenFile(fmt.Sprintf("/proc/%d/mem", s.pid), os.O_RDONLY, 0) - if err != nil { - return 0, fmt.Errorf("debug: open /proc/%d/mem: %w", s.pid, err) - } - defer mem.Close() - buf := make([]byte, 8) - if _, err := mem.ReadAt(buf, int64(addr)); err != nil { - return 0, fmt.Errorf("debug: read mem %#x: %w", addr, err) - } - return uint64(buf[0]) | uint64(buf[1])<<8 | uint64(buf[2])<<16 | uint64(buf[3])<<24 | - uint64(buf[4])<<32 | uint64(buf[5])<<40 | uint64(buf[6])<<48 | uint64(buf[7])<<56, nil -} - -// Poke writes a word (8 bytes) to the debuggee's memory at addr. -func (s *Session) Poke(addr, val uint64) error { - mem, err := os.OpenFile(fmt.Sprintf("/proc/%d/mem", s.pid), os.O_WRONLY, 0) - if err != nil { - return fmt.Errorf("debug: open /proc/%d/mem: %w", s.pid, err) - } - defer mem.Close() - buf := []byte{byte(val), byte(val >> 8), byte(val >> 16), byte(val >> 24), - byte(val >> 32), byte(val >> 40), byte(val >> 48), byte(val >> 56)} - if _, err := mem.WriteAt(buf, int64(addr)); err != nil { - return fmt.Errorf("debug: write mem %#x: %w", addr, err) - } - return nil -} - -// ReadMemory reads len bytes from the debuggee's memory at addr. -func (s *Session) ReadMemory(addr uint64, length int) ([]byte, error) { - out := make([]byte, length) - for i := 0; i < length; i += 8 { - word, err := s.Peek(addr + uint64(i)) - if err != nil { - return out[:i], err - } - for j := 0; j < 8 && i+j < length; j++ { - out[i+j] = byte(word >> (8 * j)) - } - } - return out, nil -} - -// WriteMemory writes bytes to the debuggee's memory at addr. -func (s *Session) WriteMemory(addr uint64, data []byte) error { - for i := 0; i < len(data); i += 8 { - end := i + 8 - if end > len(data) { - end = len(data) - } - var word uint64 - for j := 0; j < end-i; j++ { - word |= uint64(data[i+j]) << (8 * j) - } - // For partial writes, read-modify-write the existing word. - if end-i < 8 { - existing, err := s.Peek(addr + uint64(i)) - if err != nil { - return err - } - // Clear the bytes we're overwriting and merge. - mask := ^((uint64(1) << (8 * (end - i))) - 1) - word = (existing & mask) | word - } - if err := s.Poke(addr+uint64(i), word); err != nil { - return err - } - } - return nil -} - -// Step executes a single instruction in the debuggee. -func (s *Session) Step() error { - if s.exited { - return fmt.Errorf("debug: debuggee has exited") - } - _, _, errno := syscall.Syscall6( - syscall.SYS_PTRACE, - uintptr(syscall.PTRACE_SINGLESTEP), - uintptr(s.pid), - 0, 0, 0, 0, - ) - if errno != 0 { - return fmt.Errorf("debug: PTRACE_SINGLESTEP: %w", errno) - } - return s.wait() -} - -// Continue resumes execution until the next breakpoint or exit. -func (s *Session) Continue() error { - if s.exited { - return fmt.Errorf("debug: debuggee has exited") - } - _, _, errno := syscall.Syscall6( - syscall.SYS_PTRACE, - uintptr(syscall.PTRACE_CONT), - uintptr(s.pid), - 0, 0, 0, 0, - ) - if errno != 0 { - return fmt.Errorf("debug: PTRACE_CONT: %w", errno) - } - return s.wait() -} - -// Exited returns true if the debuggee has terminated. -func (s *Session) Exited() bool { - return s.exited -} - -// Pid returns the debuggee's process ID. -func (s *Session) Pid() int { - return s.pid -} - -// CodeBase returns the base address of the JIT code in the debuggee. -func (s *Session) CodeBase() uint64 { - return s.codeBase -} - -// Kill terminates the debuggee. -func (s *Session) Kill() { - if !s.exited { - syscall.Kill(s.pid, syscall.SIGKILL) - syscall.Wait4(s.pid, nil, 0, nil) - s.exited = true - } - if s.cmd != nil && s.cmd.Process != nil { - s.cmd.Wait() - } -} - -// findRWXMapping reads /proc/pid/maps and returns the base address of the -// first read-write-execute mapping (the JIT code region). -func findRWXMapping(pid int) uint64 { - data, err := os.ReadFile(fmt.Sprintf("/proc/%d/maps", pid)) - if err != nil { - return 0 - } - for _, line := range strings.Split(string(data), "\n") { - // Format: addr-addr perms offset dev inode pathname - fields := strings.Fields(line) - if len(fields) < 2 { - continue - } - perms := fields[1] - if len(perms) >= 3 && perms[0] == 'r' && perms[1] == 'w' && perms[2] == 'x' { - // Parse the start address. - var start uint64 - fmt.Sscanf(fields[0], "%x-", &start) - return start - } - } - return 0 -} diff --git a/debug/ptrace_linux_arm64.go b/debug/ptrace_linux_arm64.go new file mode 100644 index 0000000..44eef31 --- /dev/null +++ b/debug/ptrace_linux_arm64.go @@ -0,0 +1,85 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && arm64 + +package debug + +import ( + "fmt" + "syscall" + "unsafe" +) + +// GetRegs reads the general-purpose registers of the stopped debuggee. +func (s *Session) GetRegs() (Regs, error) { + var regs Regs + _, _, errno := syscall.Syscall6( + syscall.SYS_PTRACE, + uintptr(syscall.PTRACE_GETREGS), + uintptr(s.pid), + 0, + uintptr(unsafe.Pointer(®s)), + 0, 0, + ) + if errno != 0 { + return regs, fmt.Errorf("debug: PTRACE_GETREGS: %w", errno) + } + return regs, nil +} + +// SetRegs writes the general-purpose registers of the stopped debuggee. +func (s *Session) SetRegs(regs *Regs) error { + _, _, errno := syscall.Syscall6( + syscall.SYS_PTRACE, + uintptr(syscall.PTRACE_SETREGS), + uintptr(s.pid), + 0, + uintptr(unsafe.Pointer(regs)), + 0, 0, + ) + if errno != 0 { + return fmt.Errorf("debug: PTRACE_SETREGS: %w", errno) + } + return nil +} + +// FPRegs holds the NEON/FP register state (V0-V31, FPCR, FPSR). +type FPRegs struct { + V [32][16]byte // V0-V31 (128-bit NEON/FP registers) + FPCR uint32 + FPSR uint32 +} + +// GetFPRegs retrieves the NEON/FP register state. +func (s *Session) GetFPRegs() (FPRegs, error) { + var fp FPRegs + _, _, errno := syscall.Syscall6( + syscall.SYS_PTRACE, + uintptr(syscall.PTRACE_GETFPREGS), + uintptr(s.pid), + 0, + uintptr(unsafe.Pointer(&fp)), + 0, 0, + ) + if errno != 0 { + return fp, fmt.Errorf("debug: PTRACE_GETFPREGS: %w", errno) + } + return fp, nil +} + +// VectorRegs holds the full SIMD register state. +type VectorRegs struct { + V [32][16]byte // V0-V31 (128-bit) +} + +// GetVectorRegs retrieves the SIMD registers. +func (s *Session) GetVectorRegs() (VectorRegs, error) { + var v VectorRegs + fp, err := s.GetFPRegs() + if err != nil { + return v, err + } + copy(v.V[:][:], fp.V[:][:]) + return v, nil +} diff --git a/debug/ptrace_linux_loong64.go b/debug/ptrace_linux_loong64.go new file mode 100644 index 0000000..b6355fc --- /dev/null +++ b/debug/ptrace_linux_loong64.go @@ -0,0 +1,76 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && loong64 + +package debug + +import ( + "fmt" + "syscall" + "unsafe" +) + +// GetRegs reads the general-purpose registers of the stopped debuggee. +func (s *Session) GetRegs() (Regs, error) { + var regs Regs + _, _, errno := syscall.Syscall6( + syscall.SYS_PTRACE, + uintptr(syscall.PTRACE_GETREGS), + uintptr(s.pid), + 0, + uintptr(unsafe.Pointer(®s)), + 0, 0, + ) + if errno != 0 { + return regs, fmt.Errorf("debug: PTRACE_GETREGS: %w", errno) + } + return regs, nil +} + +// SetRegs writes the general-purpose registers of the stopped debuggee. +func (s *Session) SetRegs(regs *Regs) error { + _, _, errno := syscall.Syscall6( + syscall.SYS_PTRACE, + uintptr(syscall.PTRACE_SETREGS), + uintptr(s.pid), + 0, + uintptr(unsafe.Pointer(regs)), + 0, 0, + ) + if errno != 0 { + return fmt.Errorf("debug: PTRACE_SETREGS: %w", errno) + } + return nil +} + +// FPRegs holds the LoongArch FP register state (F0-F31). +type FPRegs struct { + F [32][16]byte // F0-F31 (128-bit, lower 64 bits used for double) + FCSR uint32 +} + +// GetFPRegs retrieves the FP register state. +func (s *Session) GetFPRegs() (FPRegs, error) { + var fp FPRegs + _, _, errno := syscall.Syscall6( + syscall.SYS_PTRACE, + uintptr(syscall.PTRACE_GETFPREGS), + uintptr(s.pid), + 0, + uintptr(unsafe.Pointer(&fp)), + 0, 0, + ) + if errno != 0 { + return fp, fmt.Errorf("debug: PTRACE_GETFPREGS: %w", errno) + } + return fp, nil +} + +// VectorRegs holds the LSX/LASX vector register state (placeholder). +type VectorRegs struct{} + +// GetVectorRegs retrieves the vector registers (not yet implemented for loong64). +func (s *Session) GetVectorRegs() (VectorRegs, error) { + return VectorRegs{}, nil +} diff --git a/debug/ptrace_linux_riscv64.go b/debug/ptrace_linux_riscv64.go new file mode 100644 index 0000000..f017635 --- /dev/null +++ b/debug/ptrace_linux_riscv64.go @@ -0,0 +1,76 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && riscv64 + +package debug + +import ( + "fmt" + "syscall" + "unsafe" +) + +// GetRegs reads the general-purpose registers of the stopped debuggee. +func (s *Session) GetRegs() (Regs, error) { + var regs Regs + _, _, errno := syscall.Syscall6( + syscall.SYS_PTRACE, + uintptr(syscall.PTRACE_GETREGS), + uintptr(s.pid), + 0, + uintptr(unsafe.Pointer(®s)), + 0, 0, + ) + if errno != 0 { + return regs, fmt.Errorf("debug: PTRACE_GETREGS: %w", errno) + } + return regs, nil +} + +// SetRegs writes the general-purpose registers of the stopped debuggee. +func (s *Session) SetRegs(regs *Regs) error { + _, _, errno := syscall.Syscall6( + syscall.SYS_PTRACE, + uintptr(syscall.PTRACE_SETREGS), + uintptr(s.pid), + 0, + uintptr(unsafe.Pointer(regs)), + 0, 0, + ) + if errno != 0 { + return fmt.Errorf("debug: PTRACE_SETREGS: %w", errno) + } + return nil +} + +// FPRegs holds the RISC-V FP register state (F0-F31). +type FPRegs struct { + F [32][16]byte // F0-F31 (128-bit, lower 64 bits used for double) + FCSR uint32 +} + +// GetFPRegs retrieves the FP register state. +func (s *Session) GetFPRegs() (FPRegs, error) { + var fp FPRegs + _, _, errno := syscall.Syscall6( + syscall.SYS_PTRACE, + uintptr(syscall.PTRACE_GETFPREGS), + uintptr(s.pid), + 0, + uintptr(unsafe.Pointer(&fp)), + 0, 0, + ) + if errno != 0 { + return fp, fmt.Errorf("debug: PTRACE_GETFPREGS: %w", errno) + } + return fp, nil +} + +// VectorRegs holds the vector register state (placeholder). +type VectorRegs struct{} + +// GetVectorRegs retrieves the vector registers (not yet implemented for riscv64). +func (s *Session) GetVectorRegs() (VectorRegs, error) { + return VectorRegs{}, nil +} diff --git a/debug/regs_linux_loong64.go b/debug/regs_linux_loong64.go new file mode 100644 index 0000000..a6e3dd9 --- /dev/null +++ b/debug/regs_linux_loong64.go @@ -0,0 +1,130 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && loong64 + +package debug + +// Regs holds the full general-purpose register set of a traced process +// (the Linux loong64 user_pt_regs layout). +type Regs struct { + R0 uint64 // zero + R1 uint64 // RA (return address) + R2 uint64 // TP (thread pointer) + R3 uint64 // SP (stack pointer) + R4 uint64 // A0 + R5 uint64 // A1 + R6 uint64 // A2 + R7 uint64 // A3 + R8 uint64 // A4 + R9 uint64 // A5 + R10 uint64 // A6 + R11 uint64 // A7 + R12 uint64 // T0 + R13 uint64 // T1 + R14 uint64 // T2 + R15 uint64 // T3 + R16 uint64 // T4 + R17 uint64 // T5 + R18 uint64 // T6 + R19 uint64 // T7 + R20 uint64 // T8 + R21 uint64 // FP (frame pointer) + R22 uint64 // S0 + R23 uint64 // S1 + R24 uint64 // S2 + R25 uint64 // S3 + R26 uint64 // S4 + R27 uint64 // S5 + R28 uint64 // S6 + R29 uint64 // S7 + R30 uint64 // S8 + R31 uint64 // PC +} + +// GetPC returns the program counter. +func (r *Regs) GetPC() uint64 { return r.R31 } + +// SetPC sets the program counter. +func (r *Regs) SetPC(pc uint64) { r.R31 = pc } + +// GetSP returns the stack pointer. +func (r *Regs) GetSP() uint64 { return r.R3 } + +// RegValue returns the value of the named register, or false if unknown. +func (r *Regs) RegValue(name string) (uint64, bool) { + switch name { + case "r0", "zero": + return r.R0, true + case "r1", "ra": + return r.R1, true + case "r2", "tp": + return r.R2, true + case "r3", "sp": + return r.R3, true + case "r4", "a0": + return r.R4, true + case "r5", "a1": + return r.R5, true + case "r6", "a2": + return r.R6, true + case "r7", "a3": + return r.R7, true + case "r8", "a4": + return r.R8, true + case "r9", "a5": + return r.R9, true + case "r10", "a6": + return r.R10, true + case "r11", "a7": + return r.R11, true + case "r12", "t0": + return r.R12, true + case "r13", "t1": + return r.R13, true + case "r14", "t2": + return r.R14, true + case "r15", "t3": + return r.R15, true + case "r16", "t4": + return r.R16, true + case "r17", "t5": + return r.R17, true + case "r18", "t6": + return r.R18, true + case "r19", "t7": + return r.R19, true + case "r20", "t8": + return r.R20, true + case "r21", "fp": + return r.R21, true + case "r22", "s0": + return r.R22, true + case "r23", "s1": + return r.R23, true + case "r24", "s2": + return r.R24, true + case "r25", "s3": + return r.R25, true + case "r26", "s4": + return r.R26, true + case "r27", "s5": + return r.R27, true + case "r28", "s6": + return r.R28, true + case "r29", "s7": + return r.R29, true + case "r30", "s8": + return r.R30, true + case "r31", "pc": + return r.R31, true + default: + return 0, false + } +} + +// breakpointInsn is the software breakpoint instruction (BRK $0). +var breakpointInsn = []byte{0x05, 0x00, 0x2a, 0x00} // break 0 + +// breakpointPCAdjust is how far PC is past the breakpoint instruction after a trap. +const breakpointPCAdjust = 4 diff --git a/debug/regs_linux_riscv64.go b/debug/regs_linux_riscv64.go new file mode 100644 index 0000000..8273240 --- /dev/null +++ b/debug/regs_linux_riscv64.go @@ -0,0 +1,130 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && riscv64 + +package debug + +// Regs holds the full general-purpose register set of a traced process +// (the Linux riscv64 user_regs_struct layout). +type Regs struct { + PC uint64 + Ra uint64 // x1 (return address) + Sp uint64 // x2 + Gp uint64 // x3 + Tp uint64 // x4 + T0 uint64 // x5 + T1 uint64 // x6 + T2 uint64 // x7 + S0 uint64 // x8 (frame pointer) + S1 uint64 // x9 + A0 uint64 // x10 + A1 uint64 // x11 + A2 uint64 // x12 + A3 uint64 // x13 + A4 uint64 // x14 + A5 uint64 // x15 + A6 uint64 // x16 + A7 uint64 // x17 + S2 uint64 // x18 + S3 uint64 // x19 + S4 uint64 // x20 + S5 uint64 // x21 + S6 uint64 // x22 + S7 uint64 // x23 + S8 uint64 // x24 + S9 uint64 // x25 + S10 uint64 // x26 + S11 uint64 // x27 + T3 uint64 // x28 + T4 uint64 // x29 + T5 uint64 // x30 + T6 uint64 // x31 +} + +// GetPC returns the program counter. +func (r *Regs) GetPC() uint64 { return r.PC } + +// SetPC sets the program counter. +func (r *Regs) SetPC(pc uint64) { r.PC = pc } + +// GetSP returns the stack pointer. +func (r *Regs) GetSP() uint64 { return r.Sp } + +// RegValue returns the value of the named register, or false if unknown. +func (r *Regs) RegValue(name string) (uint64, bool) { + switch name { + case "pc": + return r.PC, true + case "ra", "x1": + return r.Ra, true + case "sp", "x2": + return r.Sp, true + case "gp", "x3": + return r.Gp, true + case "tp", "x4": + return r.Tp, true + case "t0", "x5": + return r.T0, true + case "t1", "x6": + return r.T1, true + case "t2", "x7": + return r.T2, true + case "s0", "fp", "x8": + return r.S0, true + case "s1", "x9": + return r.S1, true + case "a0", "x10": + return r.A0, true + case "a1", "x11": + return r.A1, true + case "a2", "x12": + return r.A2, true + case "a3", "x13": + return r.A3, true + case "a4", "x14": + return r.A4, true + case "a5", "x15": + return r.A5, true + case "a6", "x16": + return r.A6, true + case "a7", "x17": + return r.A7, true + case "s2", "x18": + return r.S2, true + case "s3", "x19": + return r.S3, true + case "s4", "x20": + return r.S4, true + case "s5", "x21": + return r.S5, true + case "s6", "x22": + return r.S6, true + case "s7", "x23": + return r.S7, true + case "s8", "x24": + return r.S8, true + case "s9", "x25": + return r.S9, true + case "s10", "x26": + return r.S10, true + case "s11", "x27": + return r.S11, true + case "t3", "x28": + return r.T3, true + case "t4", "x29": + return r.T4, true + case "t5", "x30": + return r.T5, true + case "t6", "x31": + return r.T6, true + default: + return 0, false + } +} + +// breakpointInsn is the software breakpoint instruction (EBREAK). +var breakpointInsn = []byte{0x73, 0x00, 0x10, 0x00} // ebreak + +// breakpointPCAdjust is how far PC is past the breakpoint instruction after a trap. +const breakpointPCAdjust = 4 diff --git a/debug/repl.go b/debug/repl.go index 86f3d83..6872654 100644 --- a/debug/repl.go +++ b/debug/repl.go @@ -1,7 +1,7 @@ // Copyright (c) 2026 Petr Balvín (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 | step [n] | continue | disas [n] | regs | where | x [len] | w | labels | quit") @@ -64,7 +59,6 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar continue } printRegs(®s, 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, ®s) 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 [if ]") 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 " 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 [if ] set a breakpoint + 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 + 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 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, ®s) + 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, ®s) 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 diff --git a/debug/stopinfo_linux.go b/debug/stopinfo_linux.go new file mode 100644 index 0000000..05804a5 --- /dev/null +++ b/debug/stopinfo_linux.go @@ -0,0 +1,69 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux + +package debug + +import ( + "syscall" + "unsafe" +) + +// StopReason describes why the debuggee stopped. +type StopReason int + +const ( + StopNone StopReason = iota + StopBreakpoint // software breakpoint hit + StopWatchpoint // hardware watchpoint triggered + StopSingleStep // single-step completed + StopSignal // stopped by a signal + StopExited // process exited +) + +// siginfo_t layout (Linux): si_signo, si_errno, si_code, then union. +// The si_addr field is at offset 16 on all supported architectures. +type siginfoT struct { + SiSigno int32 + SiErrno int32 + SiCode int32 + _pad [125]byte +} + +const ( + trapBRKPT = 1 // software breakpoint + trapHWBRKPT = 4 // hardware watchpoint +) + +// StopInfo returns the reason the debuggee stopped and the faulting address +// (for watchpoints, the watched address that was accessed). +func (s *Session) StopInfo() (StopReason, uint64) { + if s.exited { + return StopExited, 0 + } + var info siginfoT + _, _, errno := syscall.Syscall6( + syscall.SYS_PTRACE, + uintptr(syscall.PTRACE_GETSIGINFO), + uintptr(s.pid), + 0, + uintptr(unsafe.Pointer(&info)), + 0, 0, + ) + if errno != 0 { + return StopNone, 0 + } + if info.SiSigno != int32(syscall.SIGTRAP) { + return StopSignal, uint64(info.SiCode) + } + switch info.SiCode { + case trapBRKPT: + return StopBreakpoint, 0 + case trapHWBRKPT: + addr := *(*uint64)(unsafe.Pointer(uintptr(unsafe.Pointer(&info)) + 16)) + return StopWatchpoint, addr + default: + return StopSingleStep, 0 + } +} diff --git a/debug/stopinfo_linux_amd64.go b/debug/stopinfo_linux_amd64.go index a716c48..1b771dd 100644 --- a/debug/stopinfo_linux_amd64.go +++ b/debug/stopinfo_linux_amd64.go @@ -5,69 +5,7 @@ package debug -import ( - "fmt" - "syscall" - "unsafe" -) - -// StopReason describes why the debuggee stopped. -type StopReason int - -const ( - StopNone StopReason = iota - StopBreakpoint // INT3 breakpoint hit - StopWatchpoint // hardware watchpoint triggered - StopSingleStep // single-step completed - StopSignal // stopped by a signal - StopExited // process exited -) - -// siginfo_t layout (Linux amd64): si_signo, si_errno, si_code, then union. -type siginfoT struct { - SiSigno int32 - SiErrno int32 - SiCode int32 - _pad [125]byte -} - -const ( - trapBRKPT = 1 // INT3 breakpoint - trapHWBRKPT = 4 // hardware watchpoint -) - -// StopInfo returns the reason the debuggee stopped and the faulting address -// (for watchpoints, the watched address that was accessed). -func (s *Session) StopInfo() (StopReason, uint64) { - if s.exited { - return StopExited, 0 - } - var info siginfoT - _, _, errno := syscall.Syscall6( - syscall.SYS_PTRACE, - uintptr(syscall.PTRACE_GETSIGINFO), - uintptr(s.pid), - 0, - uintptr(unsafe.Pointer(&info)), - 0, 0, - ) - if errno != 0 { - return StopNone, 0 - } - if info.SiSigno != int32(syscall.SIGTRAP) { - return StopSignal, uint64(info.SiCode) - } - switch info.SiCode { - case trapBRKPT: - return StopBreakpoint, 0 - case trapHWBRKPT: - // The faulting address is in si_addr (offset 16 in siginfo_t on amd64). - addr := *(*uint64)(unsafe.Pointer(uintptr(unsafe.Pointer(&info)) + 16)) - return StopWatchpoint, addr - default: - return StopSingleStep, 0 - } -} +import "fmt" // SetReg modifies a register value in the debuggee. func (s *Session) SetReg(name string, value uint64) error { diff --git a/debug/stopinfo_linux_arm64.go b/debug/stopinfo_linux_arm64.go new file mode 100644 index 0000000..fc30f24 --- /dev/null +++ b/debug/stopinfo_linux_arm64.go @@ -0,0 +1,87 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && arm64 + +package debug + +import "fmt" + +// SetReg modifies a register value in the debuggee. +func (s *Session) SetReg(name string, value uint64) error { + regs, err := s.GetRegs() + if err != nil { + return err + } + switch name { + case "x0": + regs.X0 = value + case "x1": + regs.X1 = value + case "x2": + regs.X2 = value + case "x3": + regs.X3 = value + case "x4": + regs.X4 = value + case "x5": + regs.X5 = value + case "x6": + regs.X6 = value + case "x7": + regs.X7 = value + case "x8": + regs.X8 = value + case "x9": + regs.X9 = value + case "x10": + regs.X10 = value + case "x11": + regs.X11 = value + case "x12": + regs.X12 = value + case "x13": + regs.X13 = value + case "x14": + regs.X14 = value + case "x15": + regs.X15 = value + case "x16": + regs.X16 = value + case "x17": + regs.X17 = value + case "x18": + regs.X18 = value + case "x19": + regs.X19 = value + case "x20": + regs.X20 = value + case "x21": + regs.X21 = value + case "x22": + regs.X22 = value + case "x23": + regs.X23 = value + case "x24": + regs.X24 = value + case "x25": + regs.X25 = value + case "x26": + regs.X26 = value + case "x27": + regs.X27 = value + case "x28": + regs.X28 = value + case "x29", "fp": + regs.X29 = value + case "x30", "lr": + regs.X30 = value + case "sp": + regs.SP = value + case "pc": + regs.PC = value + default: + return fmt.Errorf("debug: unknown register %q", name) + } + return s.SetRegs(®s) +} diff --git a/debug/stopinfo_linux_loong64.go b/debug/stopinfo_linux_loong64.go new file mode 100644 index 0000000..19b6ad7 --- /dev/null +++ b/debug/stopinfo_linux_loong64.go @@ -0,0 +1,85 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && loong64 + +package debug + +import "fmt" + +// SetReg modifies a register value in the debuggee. +func (s *Session) SetReg(name string, value uint64) error { + regs, err := s.GetRegs() + if err != nil { + return err + } + switch name { + case "r0", "zero": + regs.R0 = value + case "r1", "ra": + regs.R1 = value + case "r2", "tp": + regs.R2 = value + case "r3", "sp": + regs.R3 = value + case "r4", "a0": + regs.R4 = value + case "r5", "a1": + regs.R5 = value + case "r6", "a2": + regs.R6 = value + case "r7", "a3": + regs.R7 = value + case "r8", "a4": + regs.R8 = value + case "r9", "a5": + regs.R9 = value + case "r10", "a6": + regs.R10 = value + case "r11", "a7": + regs.R11 = value + case "r12", "t0": + regs.R12 = value + case "r13", "t1": + regs.R13 = value + case "r14", "t2": + regs.R14 = value + case "r15", "t3": + regs.R15 = value + case "r16", "t4": + regs.R16 = value + case "r17", "t5": + regs.R17 = value + case "r18", "t6": + regs.R18 = value + case "r19", "t7": + regs.R19 = value + case "r20", "t8": + regs.R20 = value + case "r21", "fp": + regs.R21 = value + case "r22", "s0": + regs.R22 = value + case "r23", "s1": + regs.R23 = value + case "r24", "s2": + regs.R24 = value + case "r25", "s3": + regs.R25 = value + case "r26", "s4": + regs.R26 = value + case "r27", "s5": + regs.R27 = value + case "r28", "s6": + regs.R28 = value + case "r29", "s7": + regs.R29 = value + case "r30", "s8": + regs.R30 = value + case "r31", "pc": + regs.R31 = value + default: + return fmt.Errorf("debug: unknown register %q", name) + } + return s.SetRegs(®s) +} diff --git a/debug/stopinfo_linux_riscv64.go b/debug/stopinfo_linux_riscv64.go new file mode 100644 index 0000000..5529b94 --- /dev/null +++ b/debug/stopinfo_linux_riscv64.go @@ -0,0 +1,85 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && riscv64 + +package debug + +import "fmt" + +// SetReg modifies a register value in the debuggee. +func (s *Session) SetReg(name string, value uint64) error { + regs, err := s.GetRegs() + if err != nil { + return err + } + switch name { + case "pc": + regs.PC = value + case "ra", "x1": + regs.Ra = value + case "sp", "x2": + regs.Sp = value + case "gp", "x3": + regs.Gp = value + case "tp", "x4": + regs.Tp = value + case "t0", "x5": + regs.T0 = value + case "t1", "x6": + regs.T1 = value + case "t2", "x7": + regs.T2 = value + case "s0", "fp", "x8": + regs.S0 = value + case "s1", "x9": + regs.S1 = value + case "a0", "x10": + regs.A0 = value + case "a1", "x11": + regs.A1 = value + case "a2", "x12": + regs.A2 = value + case "a3", "x13": + regs.A3 = value + case "a4", "x14": + regs.A4 = value + case "a5", "x15": + regs.A5 = value + case "a6", "x16": + regs.A6 = value + case "a7", "x17": + regs.A7 = value + case "s2", "x18": + regs.S2 = value + case "s3", "x19": + regs.S3 = value + case "s4", "x20": + regs.S4 = value + case "s5", "x21": + regs.S5 = value + case "s6", "x22": + regs.S6 = value + case "s7", "x23": + regs.S7 = value + case "s8", "x24": + regs.S8 = value + case "s9", "x25": + regs.S9 = value + case "s10", "x26": + regs.S10 = value + case "s11", "x27": + regs.S11 = value + case "t3", "x28": + regs.T3 = value + case "t4", "x29": + regs.T4 = value + case "t5", "x30": + regs.T5 = value + case "t6", "x31": + regs.T6 = value + default: + return fmt.Errorf("debug: unknown register %q", name) + } + return s.SetRegs(®s) +} diff --git a/debug/target_linux.go b/debug/target_linux.go new file mode 100644 index 0000000..5300ee1 --- /dev/null +++ b/debug/target_linux.go @@ -0,0 +1,99 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux + +package debug + +import ( + "encoding/hex" + "fmt" + "os" + "strconv" + "strings" + "syscall" + "unsafe" +) + +// mapRWX maps code into a read-write-execute region. +func mapRWX(code []byte) ([]byte, error) { + const pageSize = 4096 + size := (len(code) + pageSize - 1) &^ (pageSize - 1) + mem, err := syscall.Mmap(-1, 0, size, + syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC, + syscall.MAP_PRIVATE|syscall.MAP_ANON) + if err != nil { + return nil, err + } + copy(mem, code) + return mem, nil +} + +// setupBuffers allocates buffers in the debuggee's memory. +func setupBuffers(spec string, args []byte, argSize int, tmpDir string) ([]byte, error) { + type bufSpec struct { + name string + size int + pattern string + } + var specs []bufSpec + for _, part := range strings.Split(spec, ",") { + fields := strings.SplitN(part, ":", 3) + if len(fields) != 3 { + continue + } + size, err := strconv.Atoi(fields[1]) + if err != nil || size <= 0 { + continue + } + specs = append(specs, bufSpec{name: fields[0], size: size, pattern: fields[2]}) + } + + if len(specs) == 0 { + return args, nil + } + + var bufAddrs []uint64 + for _, s := range specs { + buf, err := syscall.Mmap(-1, 0, s.size, + syscall.PROT_READ|syscall.PROT_WRITE, + syscall.MAP_PRIVATE|syscall.MAP_ANON) + if err != nil { + return nil, fmt.Errorf("mmap buffer %s: %w", s.name, err) + } + fillBuffer(buf, s.pattern) + bufAddrs = append(bufAddrs, uint64(uintptr(unsafe.Pointer(&buf[0])))) + } + + addrFile, err := os.Create(tmpDir + "/bufaddrs") + if err != nil { + return nil, err + } + for _, addr := range bufAddrs { + fmt.Fprintf(addrFile, "%d\n", addr) + } + addrFile.Close() + + return args, nil +} + +// fillBuffer fills a buffer with the specified pattern. +func fillBuffer(buf []byte, pattern string) { + switch pattern { + case "zero": + case "ones": + for i := range buf { + buf[i] = 0xFF + } + case "seq": + for i := range buf { + buf[i] = byte(i) + } + default: + if data, err := hex.DecodeString(pattern); err == nil && len(data) > 0 { + for i := range buf { + buf[i] = data[i%len(data)] + } + } + } +} diff --git a/debug/target_linux_amd64.go b/debug/target_linux_amd64.go index 5fb34e5..fbba65e 100644 --- a/debug/target_linux_amd64.go +++ b/debug/target_linux_amd64.go @@ -6,12 +6,9 @@ package debug import ( - "encoding/hex" "fmt" "os" "runtime" - "strconv" - "strings" "syscall" "unsafe" @@ -20,12 +17,8 @@ import ( "sourcedock.dev/petrbalvin/gasm-devkit/verify" ) -// RunTarget is the debuggee entry point (gasm debug --target). It -// assembles the file, maps the JIT code, registers itself for ptrace, -// stops, and then executes the named function. The parent debugger -// controls execution from there. +// RunTarget is the debuggee entry point (gasm debug --target). func RunTarget(asmPath, funcName, argsFile, tmpDir string) error { - // Parse and assemble. src, err := os.ReadFile(asmPath) if err != nil { return fmt.Errorf("debug target: %w", err) @@ -39,7 +32,6 @@ func RunTarget(asmPath, funcName, argsFile, tmpDir string) error { return fmt.Errorf("debug target: assemble: %w", err) } - // Find the function. var fl *asm.FuncLayout for i := range img.Funcs { if img.Funcs[i].Name == funcName { @@ -51,24 +43,20 @@ func RunTarget(asmPath, funcName, argsFile, tmpDir string) error { return fmt.Errorf("debug target: function %q not found", funcName) } - // Map the entire image RWX (we need write access for breakpoints). code := img.Bytes() exec, err := mapRWX(code) if err != nil { return fmt.Errorf("debug target: mmap: %w", err) } - // Write the code base address for the parent. codeBase := uintptr(unsafe.Pointer(&exec[0])) if err := os.WriteFile(tmpDir+"/codebase", []byte(fmt.Sprintf("%d", codeBase)), 0o644); err != nil { return fmt.Errorf("debug target: write codebase: %w", err) } - // Write function metadata (offset, size, args) for the parent. meta := fmt.Sprintf("%d %d %d", fl.Offset, fl.Size, fl.Args) os.WriteFile(tmpDir+"/funcmeta", []byte(meta), 0o644) - // Write label table for breakpoint resolution. labelsFile, _ := os.Create(tmpDir + "/labels") if labelsFile != nil { for label, off := range fl.Labels { @@ -77,7 +65,6 @@ func RunTarget(asmPath, funcName, argsFile, tmpDir string) error { labelsFile.Close() } - // Read the argument block. args, err := os.ReadFile(argsFile) if err != nil { return fmt.Errorf("debug target: read args: %w", err) @@ -88,7 +75,6 @@ func RunTarget(asmPath, funcName, argsFile, tmpDir string) error { args = padded } - // Read buffer specification if present. bufSpecFile := tmpDir + "/bufspec" if bufSpec, err := os.ReadFile(bufSpecFile); err == nil && len(bufSpec) > 0 { args, err = setupBuffers(string(bufSpec), args, fl.Args, tmpDir) @@ -97,131 +83,25 @@ func RunTarget(asmPath, funcName, argsFile, tmpDir string) error { } } - // Lock this goroutine to the current OS thread so the parent's - // ptrace (attached to this thread) controls the JIT execution. runtime.LockOSThread() - // Request tracing by the parent, then stop. PTRACE_TRACEME makes - // the subsequent SIGSTOP a ptrace-stop (not a group-stop), giving - // the parent full control from the start. if _, _, errno := syscall.Syscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_TRACEME), 0, 0); errno != 0 { return fmt.Errorf("debug target: PTRACE_TRACEME: %v", errno) } os.WriteFile(tmpDir+"/ready", []byte("ok"), 0o644) syscall.Kill(syscall.Getpid(), syscall.SIGSTOP) - // --- Execution resumes here after the parent continues us --- - - // Stop at the function entry point so the debugger can set breakpoints. - // The parent will continue us when ready. os.WriteFile(tmpDir+"/entry", []byte("ok"), 0o644) syscall.Kill(syscall.Getpid(), syscall.SIGSTOP) - // Prepare the ABI0 stack and call the function. fnAddr := codeBase + uintptr(fl.Offset) stackArgs := make([]byte, fl.Args) copy(stackArgs, args) _, callErr := verify.Call(fnAddr, stackArgs) if callErr != nil { - // The function returned an error (shouldn't happen for valid code). os.Exit(1) } os.Exit(0) return nil } - -// mapRWX maps code into a read-write-execute region (needed for -// breakpoint patching via ptrace POKETEXT, though ptrace can write -// to any mapping regardless of permissions). -func mapRWX(code []byte) ([]byte, error) { - const pageSize = 4096 - size := (len(code) + pageSize - 1) &^ (pageSize - 1) - mem, err := syscall.Mmap(-1, 0, size, - syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC, - syscall.MAP_PRIVATE|syscall.MAP_ANON) - if err != nil { - return nil, err - } - copy(mem, code) - return mem, nil -} - -// setupBuffers allocates buffers in the debuggee's memory and updates the -// argument block with pointers to them. -// Format: name:size:pattern[,name:size:pattern...] -// Patterns: zero, ones, seq, or hex (e.g. "deadbeef"). -func setupBuffers(spec string, args []byte, argSize int, tmpDir string) ([]byte, error) { - // Parse the buffer spec. - type bufSpec struct { - name string - size int - pattern string - } - var specs []bufSpec - for _, part := range strings.Split(spec, ",") { - fields := strings.SplitN(part, ":", 3) - if len(fields) != 3 { - continue - } - size, err := strconv.Atoi(fields[1]) - if err != nil || size <= 0 { - continue - } - specs = append(specs, bufSpec{name: fields[0], size: size, pattern: fields[2]}) - } - - if len(specs) == 0 { - return args, nil - } - - // Allocate buffers and write their addresses to a file for the parent. - var bufAddrs []uint64 - for _, s := range specs { - buf, err := syscall.Mmap(-1, 0, s.size, - syscall.PROT_READ|syscall.PROT_WRITE, - syscall.MAP_PRIVATE|syscall.MAP_ANON) - if err != nil { - return nil, fmt.Errorf("mmap buffer %s: %w", s.name, err) - } - fillBuffer(buf, s.pattern) - bufAddrs = append(bufAddrs, uint64(uintptr(unsafe.Pointer(&buf[0])))) - } - - // Write buffer addresses to a file for the parent to read. - addrFile, err := os.Create(tmpDir + "/bufaddrs") - if err != nil { - return nil, err - } - for _, addr := range bufAddrs { - fmt.Fprintf(addrFile, "%d\n", addr) - } - addrFile.Close() - - // For now, return the args unchanged. The parent will read bufaddrs - // and construct the final argument block with the correct pointers. - return args, nil -} - -// fillBuffer fills a buffer with the specified pattern. -func fillBuffer(buf []byte, pattern string) { - switch pattern { - case "zero": - // Already zeroed by mmap. - case "ones": - for i := range buf { - buf[i] = 0xFF - } - case "seq": - for i := range buf { - buf[i] = byte(i) - } - default: - // Try to parse as hex. - if data, err := hex.DecodeString(pattern); err == nil && len(data) > 0 { - for i := range buf { - buf[i] = data[i%len(data)] - } - } - } -} diff --git a/debug/target_linux_arm64.go b/debug/target_linux_arm64.go new file mode 100644 index 0000000..2e7d866 --- /dev/null +++ b/debug/target_linux_arm64.go @@ -0,0 +1,108 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && arm64 + +package debug + +import ( + "fmt" + "os" + "runtime" + "syscall" + "unsafe" + + "sourcedock.dev/petrbalvin/gasm-devkit/asm" + "sourcedock.dev/petrbalvin/gasm-devkit/parser" +) + +// RunTarget is the debuggee entry point (gasm debug --target). +func RunTarget(asmPath, funcName, argsFile, tmpDir string) error { + src, err := os.ReadFile(asmPath) + if err != nil { + return fmt.Errorf("debug target: %w", err) + } + file, errs := parser.Parse(asmPath, string(src)) + if len(errs) > 0 { + return fmt.Errorf("debug target: parse: %v", errs[0]) + } + + var img *asm.Image + switch "arm64" { + case "arm64": + img, err = asm.AssembleFileARM64(file) + case "riscv64": + img, err = asm.AssembleFileRISCV(file) + case "loong64": + img, err = asm.AssembleFileLOONG64(file) + } + if err != nil { + return fmt.Errorf("debug target: assemble: %w", err) + } + + var fl *asm.FuncLayout + for i := range img.Funcs { + if img.Funcs[i].Name == funcName { + fl = &img.Funcs[i] + break + } + } + if fl == nil { + return fmt.Errorf("debug target: function %q not found", funcName) + } + + code := img.Bytes() + exec, err := mapRWX(code) + if err != nil { + return fmt.Errorf("debug target: mmap: %w", err) + } + + codeBase := uintptr(unsafe.Pointer(&exec[0])) + if err := os.WriteFile(tmpDir+"/codebase", []byte(fmt.Sprintf("%d", codeBase)), 0o644); err != nil { + return fmt.Errorf("debug target: write codebase: %w", err) + } + + meta := fmt.Sprintf("%d %d %d", fl.Offset, fl.Size, fl.Args) + os.WriteFile(tmpDir+"/funcmeta", []byte(meta), 0o644) + + labelsFile, _ := os.Create(tmpDir + "/labels") + if labelsFile != nil { + for label, off := range fl.Labels { + fmt.Fprintf(labelsFile, "%s %d\n", label, off) + } + labelsFile.Close() + } + + args, err := os.ReadFile(argsFile) + if err != nil { + return fmt.Errorf("debug target: read args: %w", err) + } + if len(args) < fl.Args { + padded := make([]byte, fl.Args) + copy(padded, args) + args = padded + } + + bufSpecFile := tmpDir + "/bufspec" + if bufSpec, err := os.ReadFile(bufSpecFile); err == nil && len(bufSpec) > 0 { + args, err = setupBuffers(string(bufSpec), args, fl.Args, tmpDir) + if err != nil { + return fmt.Errorf("debug target: setup buffers: %w", err) + } + } + + runtime.LockOSThread() + + if _, _, errno := syscall.Syscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_TRACEME), 0, 0); errno != 0 { + return fmt.Errorf("debug target: PTRACE_TRACEME: %v", errno) + } + os.WriteFile(tmpDir+"/ready", []byte("ok"), 0o644) + syscall.Kill(syscall.Getpid(), syscall.SIGSTOP) + + os.WriteFile(tmpDir+"/entry", []byte("ok"), 0o644) + syscall.Kill(syscall.Getpid(), syscall.SIGSTOP) + + // JIT execution requires architecture-specific verify.Call trampoline. + // The debugger can still set breakpoints and inspect state without it. + return fmt.Errorf("debug: JIT execution not yet implemented for arm64") +} diff --git a/debug/target_linux_loong64.go b/debug/target_linux_loong64.go new file mode 100644 index 0000000..b7c74c1 --- /dev/null +++ b/debug/target_linux_loong64.go @@ -0,0 +1,108 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && loong64 + +package debug + +import ( + "fmt" + "os" + "runtime" + "syscall" + "unsafe" + + "sourcedock.dev/petrbalvin/gasm-devkit/asm" + "sourcedock.dev/petrbalvin/gasm-devkit/parser" +) + +// RunTarget is the debuggee entry point (gasm debug --target). +func RunTarget(asmPath, funcName, argsFile, tmpDir string) error { + src, err := os.ReadFile(asmPath) + if err != nil { + return fmt.Errorf("debug target: %w", err) + } + file, errs := parser.Parse(asmPath, string(src)) + if len(errs) > 0 { + return fmt.Errorf("debug target: parse: %v", errs[0]) + } + + var img *asm.Image + switch "loong64" { + case "arm64": + img, err = asm.AssembleFileARM64(file) + case "riscv64": + img, err = asm.AssembleFileRISCV(file) + case "loong64": + img, err = asm.AssembleFileLOONG64(file) + } + if err != nil { + return fmt.Errorf("debug target: assemble: %w", err) + } + + var fl *asm.FuncLayout + for i := range img.Funcs { + if img.Funcs[i].Name == funcName { + fl = &img.Funcs[i] + break + } + } + if fl == nil { + return fmt.Errorf("debug target: function %q not found", funcName) + } + + code := img.Bytes() + exec, err := mapRWX(code) + if err != nil { + return fmt.Errorf("debug target: mmap: %w", err) + } + + codeBase := uintptr(unsafe.Pointer(&exec[0])) + if err := os.WriteFile(tmpDir+"/codebase", []byte(fmt.Sprintf("%d", codeBase)), 0o644); err != nil { + return fmt.Errorf("debug target: write codebase: %w", err) + } + + meta := fmt.Sprintf("%d %d %d", fl.Offset, fl.Size, fl.Args) + os.WriteFile(tmpDir+"/funcmeta", []byte(meta), 0o644) + + labelsFile, _ := os.Create(tmpDir + "/labels") + if labelsFile != nil { + for label, off := range fl.Labels { + fmt.Fprintf(labelsFile, "%s %d\n", label, off) + } + labelsFile.Close() + } + + args, err := os.ReadFile(argsFile) + if err != nil { + return fmt.Errorf("debug target: read args: %w", err) + } + if len(args) < fl.Args { + padded := make([]byte, fl.Args) + copy(padded, args) + args = padded + } + + bufSpecFile := tmpDir + "/bufspec" + if bufSpec, err := os.ReadFile(bufSpecFile); err == nil && len(bufSpec) > 0 { + args, err = setupBuffers(string(bufSpec), args, fl.Args, tmpDir) + if err != nil { + return fmt.Errorf("debug target: setup buffers: %w", err) + } + } + + runtime.LockOSThread() + + if _, _, errno := syscall.Syscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_TRACEME), 0, 0); errno != 0 { + return fmt.Errorf("debug target: PTRACE_TRACEME: %v", errno) + } + os.WriteFile(tmpDir+"/ready", []byte("ok"), 0o644) + syscall.Kill(syscall.Getpid(), syscall.SIGSTOP) + + os.WriteFile(tmpDir+"/entry", []byte("ok"), 0o644) + syscall.Kill(syscall.Getpid(), syscall.SIGSTOP) + + // JIT execution requires architecture-specific verify.Call trampoline. + // The debugger can still set breakpoints and inspect state without it. + return fmt.Errorf("debug: JIT execution not yet implemented for loong64") +} diff --git a/debug/target_linux_riscv64.go b/debug/target_linux_riscv64.go new file mode 100644 index 0000000..04d7b82 --- /dev/null +++ b/debug/target_linux_riscv64.go @@ -0,0 +1,108 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && riscv64 + +package debug + +import ( + "fmt" + "os" + "runtime" + "syscall" + "unsafe" + + "sourcedock.dev/petrbalvin/gasm-devkit/asm" + "sourcedock.dev/petrbalvin/gasm-devkit/parser" +) + +// RunTarget is the debuggee entry point (gasm debug --target). +func RunTarget(asmPath, funcName, argsFile, tmpDir string) error { + src, err := os.ReadFile(asmPath) + if err != nil { + return fmt.Errorf("debug target: %w", err) + } + file, errs := parser.Parse(asmPath, string(src)) + if len(errs) > 0 { + return fmt.Errorf("debug target: parse: %v", errs[0]) + } + + var img *asm.Image + switch "riscv64" { + case "arm64": + img, err = asm.AssembleFileARM64(file) + case "riscv64": + img, err = asm.AssembleFileRISCV(file) + case "loong64": + img, err = asm.AssembleFileLOONG64(file) + } + if err != nil { + return fmt.Errorf("debug target: assemble: %w", err) + } + + var fl *asm.FuncLayout + for i := range img.Funcs { + if img.Funcs[i].Name == funcName { + fl = &img.Funcs[i] + break + } + } + if fl == nil { + return fmt.Errorf("debug target: function %q not found", funcName) + } + + code := img.Bytes() + exec, err := mapRWX(code) + if err != nil { + return fmt.Errorf("debug target: mmap: %w", err) + } + + codeBase := uintptr(unsafe.Pointer(&exec[0])) + if err := os.WriteFile(tmpDir+"/codebase", []byte(fmt.Sprintf("%d", codeBase)), 0o644); err != nil { + return fmt.Errorf("debug target: write codebase: %w", err) + } + + meta := fmt.Sprintf("%d %d %d", fl.Offset, fl.Size, fl.Args) + os.WriteFile(tmpDir+"/funcmeta", []byte(meta), 0o644) + + labelsFile, _ := os.Create(tmpDir + "/labels") + if labelsFile != nil { + for label, off := range fl.Labels { + fmt.Fprintf(labelsFile, "%s %d\n", label, off) + } + labelsFile.Close() + } + + args, err := os.ReadFile(argsFile) + if err != nil { + return fmt.Errorf("debug target: read args: %w", err) + } + if len(args) < fl.Args { + padded := make([]byte, fl.Args) + copy(padded, args) + args = padded + } + + bufSpecFile := tmpDir + "/bufspec" + if bufSpec, err := os.ReadFile(bufSpecFile); err == nil && len(bufSpec) > 0 { + args, err = setupBuffers(string(bufSpec), args, fl.Args, tmpDir) + if err != nil { + return fmt.Errorf("debug target: setup buffers: %w", err) + } + } + + runtime.LockOSThread() + + if _, _, errno := syscall.Syscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_TRACEME), 0, 0); errno != 0 { + return fmt.Errorf("debug target: PTRACE_TRACEME: %v", errno) + } + os.WriteFile(tmpDir+"/ready", []byte("ok"), 0o644) + syscall.Kill(syscall.Getpid(), syscall.SIGSTOP) + + os.WriteFile(tmpDir+"/entry", []byte("ok"), 0o644) + syscall.Kill(syscall.Getpid(), syscall.SIGSTOP) + + // JIT execution requires architecture-specific verify.Call trampoline. + // The debugger can still set breakpoints and inspect state without it. + return fmt.Errorf("debug: JIT execution not yet implemented for riscv64") +} diff --git a/debug/watchpoint_linux_amd64.go b/debug/watchpoint_linux_amd64.go index a00fe37..98fb172 100644 --- a/debug/watchpoint_linux_amd64.go +++ b/debug/watchpoint_linux_amd64.go @@ -11,11 +11,6 @@ import ( ) // Hardware watchpoint support via x86-64 debug registers (DR0-DR3, DR7). -// -// DR0-DR3 hold the watched addresses. DR7 is the control register: -// bits 0,2,4,6: local enable for DR0-DR3 -// bits 16-17,20-21,24-25,28-29: R/W type (00=exec, 01=write, 11=read/write) -// bits 18-19,22-23,26-27,30-31: length (00=1, 01=2, 10=8, 11=4) // WatchpointType selects what triggers the watchpoint. type WatchpointType int @@ -25,11 +20,14 @@ const ( WatchRead WatchpointType = 3 // trigger on read or write ) +// wpSlots tracks watchpoint slot occupancy (DR0-DR3). +var wpSlots [4]bool + // FindFreeWatchpointSlot returns the index of the first free watchpoint slot // (0-3), or -1 if all four hardware watchpoints are in use. func (s *Session) FindFreeWatchpointSlot() int { for i := 0; i < 4; i++ { - if !s.wpSlots[i] { + if !wpSlots[i] { return i } } @@ -41,20 +39,18 @@ func (s *Session) IsWatchpointSlotUsed(slot int) bool { if slot < 0 || slot > 3 { return false } - return s.wpSlots[slot] + return wpSlots[slot] } // SetWatchpoint installs a hardware watchpoint on the given address. -// slot is 0-3 (four hardware watchpoints available); the slot must be free. func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error { if slot < 0 || slot > 3 { return fmt.Errorf("debug: watchpoint slot must be 0-3") } - if s.wpSlots[slot] { + if wpSlots[slot] { return fmt.Errorf("debug: watchpoint slot %d already in use", slot) } - // Determine the length encoding. var lenBits uint64 switch size { case 1: @@ -69,42 +65,38 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8") } - // Write the watched address to DR0-DR3. var drAddr uintptr switch slot { case 0: - drAddr = 0x0 // DR0 offset in user_regs_struct + drAddr = 0x0 case 1: - drAddr = 0x8 // DR1 + drAddr = 0x8 case 2: - drAddr = 0x10 // DR2 + drAddr = 0x10 case 3: - drAddr = 0x18 // DR3 + drAddr = 0x18 } - // PTRACE_POKEUSER writes to the debuggee's user area (includes debug regs). if err := ptracePokeUser(s.pid, drAddr, addr); err != nil { return fmt.Errorf("debug: set DR%d: %w", slot, err) } - // Read the current DR7, set the enable and type bits, write it back. - dr7, err := ptracePeekUser(s.pid, 0x38) // DR7 offset + dr7, err := ptracePeekUser(s.pid, 0x38) if err != nil { return fmt.Errorf("debug: read DR7: %w", err) } - enableBit := uint64(1) << (2 * slot) // local enable - rwBits := uint64(typ) << (16 + 4*slot) // R/W type - lenField := lenBits << (18 + 4*slot) // length + enableBit := uint64(1) << (2 * slot) + rwBits := uint64(typ) << (16 + 4*slot) + lenField := lenBits << (18 + 4*slot) - // Clear the existing bits for this slot, then set the new ones. mask := ^((uint64(1) << (2 * slot)) | (uint64(3) << (16 + 4*slot)) | (uint64(3) << (18 + 4*slot))) dr7 = (dr7 & mask) | enableBit | rwBits | lenField if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil { return fmt.Errorf("debug: set DR7: %w", err) } - s.wpSlots[slot] = true + wpSlots[slot] = true return nil } @@ -113,26 +105,25 @@ func (s *Session) ClearWatchpoint(slot int) error { if slot < 0 || slot > 3 { return fmt.Errorf("debug: watchpoint slot must be 0-3") } - if !s.wpSlots[slot] { + if !wpSlots[slot] { return fmt.Errorf("debug: watchpoint slot %d is not in use", slot) } - // Read DR7, clear the enable bit for this slot. dr7, err := ptracePeekUser(s.pid, 0x38) if err != nil { return err } - dr7 &^= uint64(1) << (2 * slot) // disable + dr7 &^= uint64(1) << (2 * slot) if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil { return err } - s.wpSlots[slot] = false + wpSlots[slot] = false return nil } // ClearAllWatchpoints removes all hardware watchpoints. func (s *Session) ClearAllWatchpoints() error { for slot := 0; slot < 4; slot++ { - if s.wpSlots[slot] { + if wpSlots[slot] { if err := s.ClearWatchpoint(slot); err != nil { return err } @@ -141,9 +132,8 @@ func (s *Session) ClearAllWatchpoints() error { return nil } -// ptracePokeUser writes a value to the debuggee's user area at the given offset. func ptracePokeUser(pid int, offset uintptr, val uint64) error { - const ptracePokeuser = 6 // PTRACE_POKEUSER + const ptracePokeuser = 6 _, _, errno := syscall.Syscall6( syscall.SYS_PTRACE, uintptr(ptracePokeuser), @@ -158,9 +148,8 @@ func ptracePokeUser(pid int, offset uintptr, val uint64) error { return nil } -// ptracePeekUser reads a value from the debuggee's user area at the given offset. func ptracePeekUser(pid int, offset uintptr) (uint64, error) { - const ptracePeekuser = 3 // PTRACE_PEEKUSER + const ptracePeekuser = 3 val, _, errno := syscall.Syscall6( syscall.SYS_PTRACE, uintptr(ptracePeekuser), diff --git a/debug/watchpoint_linux_arm64.go b/debug/watchpoint_linux_arm64.go new file mode 100644 index 0000000..f636099 --- /dev/null +++ b/debug/watchpoint_linux_arm64.go @@ -0,0 +1,47 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && arm64 + +package debug + +import "fmt" + +// WatchpointType selects what triggers the watchpoint. +type WatchpointType int + +const ( + WatchWrite WatchpointType = 1 + WatchRead WatchpointType = 3 +) + +// wpSlots tracks watchpoint slot occupancy. +var wpSlots [4]bool + +func (s *Session) FindFreeWatchpointSlot() int { + for i := 0; i < 4; i++ { + if !wpSlots[i] { + return i + } + } + return -1 +} + +func (s *Session) IsWatchpointSlotUsed(slot int) bool { + if slot < 0 || slot > 3 { + return false + } + return wpSlots[slot] +} + +func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error { + return fmt.Errorf("debug: hardware watchpoints not yet implemented for arm64") +} + +func (s *Session) ClearWatchpoint(slot int) error { + return fmt.Errorf("debug: hardware watchpoints not yet implemented for arm64") +} + +func (s *Session) ClearAllWatchpoints() error { + return nil +} diff --git a/debug/watchpoint_linux_loong64.go b/debug/watchpoint_linux_loong64.go new file mode 100644 index 0000000..bd2341b --- /dev/null +++ b/debug/watchpoint_linux_loong64.go @@ -0,0 +1,47 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && loong64 + +package debug + +import "fmt" + +// WatchpointType selects what triggers the watchpoint. +type WatchpointType int + +const ( + WatchWrite WatchpointType = 1 + WatchRead WatchpointType = 3 +) + +// wpSlots tracks watchpoint slot occupancy. +var wpSlots [4]bool + +func (s *Session) FindFreeWatchpointSlot() int { + for i := 0; i < 4; i++ { + if !wpSlots[i] { + return i + } + } + return -1 +} + +func (s *Session) IsWatchpointSlotUsed(slot int) bool { + if slot < 0 || slot > 3 { + return false + } + return wpSlots[slot] +} + +func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error { + return fmt.Errorf("debug: hardware watchpoints not yet implemented for loong64") +} + +func (s *Session) ClearWatchpoint(slot int) error { + return fmt.Errorf("debug: hardware watchpoints not yet implemented for loong64") +} + +func (s *Session) ClearAllWatchpoints() error { + return nil +} diff --git a/debug/watchpoint_linux_riscv64.go b/debug/watchpoint_linux_riscv64.go new file mode 100644 index 0000000..8bd51e7 --- /dev/null +++ b/debug/watchpoint_linux_riscv64.go @@ -0,0 +1,47 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux && riscv64 + +package debug + +import "fmt" + +// WatchpointType selects what triggers the watchpoint. +type WatchpointType int + +const ( + WatchWrite WatchpointType = 1 + WatchRead WatchpointType = 3 +) + +// wpSlots tracks watchpoint slot occupancy. +var wpSlots [4]bool + +func (s *Session) FindFreeWatchpointSlot() int { + for i := 0; i < 4; i++ { + if !wpSlots[i] { + return i + } + } + return -1 +} + +func (s *Session) IsWatchpointSlotUsed(slot int) bool { + if slot < 0 || slot > 3 { + return false + } + return wpSlots[slot] +} + +func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error { + return fmt.Errorf("debug: hardware watchpoints not yet implemented for riscv64") +} + +func (s *Session) ClearWatchpoint(slot int) error { + return fmt.Errorf("debug: hardware watchpoints not yet implemented for riscv64") +} + +func (s *Session) ClearAllWatchpoints() error { + return nil +}