feat(debug): multi-architecture debugger support for arm64, riscv64, loong64
Assisted-by: MiMo V2.5 Pro
This commit is contained in:
@@ -0,0 +1,193 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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 <file.s> --func <name>", `
|
||||||
|
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 <label|addr> 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 <file.s> --func <name>")
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
//go:build !(linux && amd64)
|
//go:build !linux
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -11,6 +11,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func cmdDebug(args []string) int {
|
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
|
return 1
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-3
@@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build linux && amd64
|
||||||
|
|
||||||
package debug
|
package debug
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -273,6 +275,7 @@ func TestBreakpointInfo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWatchpointSlotTracking(t *testing.T) {
|
func TestWatchpointSlotTracking(t *testing.T) {
|
||||||
|
wpSlots = [4]bool{} // reset
|
||||||
s := &Session{}
|
s := &Session{}
|
||||||
|
|
||||||
// All four slots are free initially.
|
// 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).
|
// Manually mark slots 0 and 2 as used (simulating successful SetWatchpoint).
|
||||||
s.wpSlots[0] = true
|
wpSlots[0] = true
|
||||||
s.wpSlots[2] = true
|
wpSlots[2] = true
|
||||||
|
|
||||||
if !s.IsWatchpointSlotUsed(0) {
|
if !s.IsWatchpointSlotUsed(0) {
|
||||||
t.Error("slot 0 should be in use")
|
t.Error("slot 0 should be in use")
|
||||||
@@ -315,7 +318,7 @@ func TestWatchpointSlotTracking(t *testing.T) {
|
|||||||
|
|
||||||
// Mark all slots used: FindFreeWatchpointSlot returns -1.
|
// Mark all slots used: FindFreeWatchpointSlot returns -1.
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
s.wpSlots[i] = true
|
wpSlots[i] = true
|
||||||
}
|
}
|
||||||
if got := s.FindFreeWatchpointSlot(); got != -1 {
|
if got := s.FindFreeWatchpointSlot(); got != -1 {
|
||||||
t.Errorf("FindFreeWatchpointSlot() with all slots used = %d, want -1", got)
|
t.Errorf("FindFreeWatchpointSlot() with all slots used = %d, want -1", got)
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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: <error: %v>\n", pc, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
result += fmt.Sprintf(" %#08x: %s\n", pc, text)
|
||||||
|
if length == 0 {
|
||||||
|
length = 4
|
||||||
|
}
|
||||||
|
pc += uint64(length)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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: <error: %v>\n", pc, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
result += fmt.Sprintf(" %#08x: %s\n", pc, text)
|
||||||
|
if length == 0 {
|
||||||
|
length = 4
|
||||||
|
}
|
||||||
|
pc += uint64(length)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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: <error: %v>\n", pc, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
result += fmt.Sprintf(" %#08x: %s\n", pc, text)
|
||||||
|
if length == 0 {
|
||||||
|
length = 4
|
||||||
|
}
|
||||||
|
pc += uint64(length)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build linux && amd64
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import "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" }
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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" }
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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" }
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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" }
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
|
||||||
|
}
|
||||||
@@ -3,165 +3,14 @@
|
|||||||
|
|
||||||
//go:build linux && amd64
|
//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
|
package debug
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
|
||||||
"unsafe"
|
"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.
|
// GetRegs reads the general-purpose registers of the stopped debuggee.
|
||||||
func (s *Session) GetRegs() (Regs, error) {
|
func (s *Session) GetRegs() (Regs, error) {
|
||||||
var regs Regs
|
var regs Regs
|
||||||
@@ -234,179 +83,16 @@ type VectorRegs struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetVectorRegs retrieves the YMM registers via PTRACE_GETREGSET + XSAVE.
|
// GetVectorRegs retrieves the YMM registers via PTRACE_GETREGSET + XSAVE.
|
||||||
// Falls back to XMM if XSAVE is unavailable.
|
|
||||||
func (s *Session) GetVectorRegs() (VectorRegs, error) {
|
func (s *Session) GetVectorRegs() (VectorRegs, error) {
|
||||||
var v VectorRegs
|
var v VectorRegs
|
||||||
fp, err := s.GetFPRegs()
|
fp, err := s.GetFPRegs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return v, err
|
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 i := 0; i < 16; i++ {
|
||||||
for j := 0; j < 16; j++ {
|
for j := 0; j < 16; j++ {
|
||||||
v.YMM[i][j] = fp.XMM[i][j]
|
v.YMM[i][j] = fp.XMM[i][j]
|
||||||
}
|
}
|
||||||
// Upper 128 bits would come from XSAVE, not available via GETFPREGS.
|
|
||||||
}
|
}
|
||||||
return v, nil
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
|
||||||
+29
-124
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
//go:build linux && amd64
|
//go:build linux
|
||||||
|
|
||||||
package debug
|
package debug
|
||||||
|
|
||||||
@@ -26,15 +26,10 @@ type SourceLine struct {
|
|||||||
Line int
|
Line int
|
||||||
}
|
}
|
||||||
|
|
||||||
// REPL runs the interactive debugger loop. On entry, the debuggee is
|
// REPL runs the interactive debugger loop.
|
||||||
// stopped in the Go runtime (after PTRACE_TRACEME + SIGSTOP). The REPL
|
|
||||||
// sets a temporary breakpoint at the function entry, continues to it, and
|
|
||||||
// then presents the prompt — so the user starts debugging at the first
|
|
||||||
// instruction of the assembled function.
|
|
||||||
func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, argsSize int, labels []Label, lines []SourceLine) {
|
func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, argsSize int, labels []Label, lines []SourceLine) {
|
||||||
entryAddr := codeBase + uint64(funcOffset)
|
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.Printf("stopped at function entry: %#x (%d bytes)\n", entryAddr, funcSize)
|
||||||
fmt.Println("commands: break <label|addr> | step [n] | continue | disas [n] | regs | where | x <addr> [len] | w <addr> <val...> | labels | quit")
|
fmt.Println("commands: break <label|addr> | step [n] | continue | disas [n] | regs | where | x <addr> [len] | w <addr> <val...> | labels | quit")
|
||||||
|
|
||||||
@@ -64,7 +59,6 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
printRegs(®s, codeBase, uint64(funcOffset))
|
printRegs(®s, codeBase, uint64(funcOffset))
|
||||||
// Also show vector registers.
|
|
||||||
vregs, err := s.GetVectorRegs()
|
vregs, err := s.GetVectorRegs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf(" (vector regs unavailable: %v)\n", err)
|
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() {
|
if !s.Exited() {
|
||||||
regs, _ := s.GetRegs()
|
regs, _ := s.GetRegs()
|
||||||
text, _, _ := s.Disassemble(regs.RIP)
|
pc := regs.GetPC()
|
||||||
fmt.Printf("=> %#x (func+%#x): %s\n", regs.RIP, regs.RIP-codeBase-uint64(funcOffset), text)
|
text, _, _ := s.Disassemble(pc)
|
||||||
|
fmt.Printf("=> %#x (func+%#x): %s\n", pc, pc-codeBase-uint64(funcOffset), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "next", "n":
|
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()
|
regs, _ := s.GetRegs()
|
||||||
text, instLen, _ := s.Disassemble(regs.RIP)
|
pc := regs.GetPC()
|
||||||
if strings.HasPrefix(strings.ToLower(text), "call") {
|
text, instLen, _ := s.Disassemble(pc)
|
||||||
// Set a temporary breakpoint after the CALL.
|
if strings.HasPrefix(strings.ToLower(text), "call") || strings.HasPrefix(strings.ToLower(text), "bl") {
|
||||||
afterAddr := regs.RIP + uint64(instLen)
|
afterAddr := pc + uint64(instLen)
|
||||||
bp, err := bm.Set(afterAddr, "(next)")
|
bp, err := bm.Set(afterAddr, "(next)")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("cannot set next breakpoint: %v\n", err)
|
fmt.Printf("cannot set next breakpoint: %v\n", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Continue until the breakpoint.
|
|
||||||
for _, b := range bm.All() {
|
for _, b := range bm.All() {
|
||||||
bm.Reinsert(b.Addr)
|
bm.Reinsert(b.Addr)
|
||||||
}
|
}
|
||||||
@@ -119,7 +111,6 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
|||||||
bm.Clear(afterAddr)
|
bm.Clear(afterAddr)
|
||||||
_ = bp
|
_ = bp
|
||||||
} else {
|
} else {
|
||||||
// Not a CALL — just single-step.
|
|
||||||
if err := s.Step(); err != nil {
|
if err := s.Step(); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
continue
|
continue
|
||||||
@@ -127,26 +118,23 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
|||||||
}
|
}
|
||||||
if !s.Exited() {
|
if !s.Exited() {
|
||||||
regs, _ := s.GetRegs()
|
regs, _ := s.GetRegs()
|
||||||
text, _, _ := s.Disassemble(regs.RIP)
|
pc := regs.GetPC()
|
||||||
fmt.Printf("=> %#x (func+%#x): %s\n", regs.RIP, regs.RIP-codeBase-uint64(funcOffset), text)
|
text, _, _ := s.Disassemble(pc)
|
||||||
|
fmt.Printf("=> %#x (func+%#x): %s\n", pc, pc-codeBase-uint64(funcOffset), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "finish", "fin":
|
case "finish", "fin":
|
||||||
// Run until the current function returns.
|
|
||||||
// For NOSPLIT frame=0: return address is at [RSP].
|
|
||||||
regs, _ := s.GetRegs()
|
regs, _ := s.GetRegs()
|
||||||
retAddr, err := s.Peek(regs.RSP)
|
retAddr, err := archReturnAddr(s, ®s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("cannot read return address: %v\n", err)
|
fmt.Printf("cannot read return address: %v\n", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Set a temporary breakpoint at the return address.
|
|
||||||
bp, err := bm.Set(retAddr, "(finish)")
|
bp, err := bm.Set(retAddr, "(finish)")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("cannot set finish breakpoint: %v\n", err)
|
fmt.Printf("cannot set finish breakpoint: %v\n", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Continue until the breakpoint.
|
|
||||||
for _, b := range bm.All() {
|
for _, b := range bm.All() {
|
||||||
bm.Reinsert(b.Addr)
|
bm.Reinsert(b.Addr)
|
||||||
}
|
}
|
||||||
@@ -164,7 +152,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
|||||||
fmt.Println("debuggee exited")
|
fmt.Println("debuggee exited")
|
||||||
} else {
|
} else {
|
||||||
regs, _ := s.GetRegs()
|
regs, _ := s.GetRegs()
|
||||||
fmt.Printf("finished, now at %#x\n", regs.RIP)
|
fmt.Printf("finished, now at %#x\n", regs.GetPC())
|
||||||
}
|
}
|
||||||
|
|
||||||
case "continue", "c":
|
case "continue", "c":
|
||||||
@@ -172,9 +160,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
|||||||
fmt.Println("debuggee exited")
|
fmt.Println("debuggee exited")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Loop: continue until a breakpoint fires (condition met) or exit.
|
|
||||||
for {
|
for {
|
||||||
// Re-insert all breakpoints before continuing.
|
|
||||||
for _, bp := range bm.All() {
|
for _, bp := range bm.All() {
|
||||||
bm.Reinsert(bp.Addr)
|
bm.Reinsert(bp.Addr)
|
||||||
}
|
}
|
||||||
@@ -186,7 +172,6 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
|||||||
fmt.Println("debuggee exited")
|
fmt.Println("debuggee exited")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// Check for watchpoint hits.
|
|
||||||
reason, wpAddr := s.StopInfo()
|
reason, wpAddr := s.StopInfo()
|
||||||
if reason == StopWatchpoint {
|
if reason == StopWatchpoint {
|
||||||
fmt.Printf("watchpoint hit at %#x\n", wpAddr)
|
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))
|
fmt.Printf("breakpoint hit: %s (func+%#x)\n", name, bp.Addr-codeBase-uint64(funcOffset))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// Condition not met (or single-step trap) — re-insert and continue.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "break", "b":
|
case "break", "b":
|
||||||
@@ -209,11 +193,9 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
|||||||
fmt.Println("usage: break <label|addr|line> [if <reg> <op> <val>]")
|
fmt.Println("usage: break <label|addr|line> [if <reg> <op> <val>]")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Try as a line number first.
|
|
||||||
var addr uint64
|
var addr uint64
|
||||||
var label string
|
var label string
|
||||||
if lineNum, err := strconv.Atoi(parts[1]); err == nil && lineNum > 0 {
|
if lineNum, err := strconv.Atoi(parts[1]); err == nil && lineNum > 0 {
|
||||||
// Find the byte offset for this line.
|
|
||||||
off := offsetForLine(lines, lineNum)
|
off := offsetForLine(lines, lineNum)
|
||||||
if off < 0 {
|
if off < 0 {
|
||||||
fmt.Printf("no instruction at line %d\n", lineNum)
|
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])
|
fmt.Printf("unknown label, address, or line: %s\n", parts[1])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Parse optional condition: "if <reg> <op> <value>"
|
|
||||||
var cond *Condition
|
var cond *Condition
|
||||||
if len(parts) >= 6 && parts[2] == "if" {
|
if len(parts) >= 6 && parts[2] == "if" {
|
||||||
val, err := strconv.ParseUint(parts[5], 0, 64)
|
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":
|
case "x":
|
||||||
regs, _ := s.GetRegs()
|
regs, _ := s.GetRegs()
|
||||||
addr := regs.RIP // default: current PC
|
addr := regs.GetPC()
|
||||||
length := 64
|
length := 64
|
||||||
if len(parts) > 1 {
|
if len(parts) > 1 {
|
||||||
addr, _ = resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
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)
|
fmt.Printf("invalid value: %s\n", arg)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Write as 8-byte word if it looks like a large value, else single byte.
|
|
||||||
if v > 255 {
|
if v > 255 {
|
||||||
for j := 0; j < 8; j++ {
|
for j := 0; j < 8; j++ {
|
||||||
bytes = append(bytes, byte(v>>(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()
|
regs, _ := s.GetRegs()
|
||||||
fmt.Print(s.DisassembleN(regs.RIP, n))
|
fmt.Print(s.DisassembleN(regs.GetPC(), n))
|
||||||
|
|
||||||
case "where":
|
case "where":
|
||||||
regs, _ := s.GetRegs()
|
regs, _ := s.GetRegs()
|
||||||
funcOff := int(regs.RIP - codeBase - uint64(funcOffset))
|
funcOff := int(regs.GetPC() - codeBase - uint64(funcOffset))
|
||||||
line := lineAt(lines, funcOff)
|
line := lineAt(lines, funcOff)
|
||||||
label := nearestLabel(labels, funcOff)
|
label := nearestLabel(labels, funcOff)
|
||||||
fmt.Printf(" func+%#x", funcOff)
|
fmt.Printf(" func+%#x", funcOff)
|
||||||
@@ -381,32 +361,32 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
case "help", "h", "?":
|
case "help", "h", "?":
|
||||||
fmt.Println(` break <label|addr> [if <reg> <op> <val>] set a breakpoint
|
fmt.Printf(` break <label|addr> [if <reg> <op> <val>] set a breakpoint
|
||||||
delete <label|addr> remove a breakpoint
|
delete <label|addr> remove a breakpoint
|
||||||
info break list all breakpoints
|
info break list all breakpoints
|
||||||
watch <addr> [r|w] [size] set a hardware watchpoint (write by default)
|
watch <addr> [r|w] [size] set a hardware watchpoint (write by default)
|
||||||
unwatch [<slot>] clear one or all watchpoints
|
unwatch [<slot>] clear one or all watchpoints
|
||||||
step [n], s single-step n instructions
|
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
|
continue, c run until breakpoint or exit
|
||||||
disas [n], u disassemble n instructions at PC
|
disas [n], u disassemble n instructions at PC
|
||||||
regs print registers and RFLAGS
|
regs print registers
|
||||||
where show source line and nearest label
|
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
|
x [addr] [len] hex-dump memory
|
||||||
w <addr> <val...> write bytes to memory
|
w <addr> <val...> write bytes to memory
|
||||||
labels, l list function labels
|
labels, l list function labels
|
||||||
help, h, ? this help
|
help, h, ? this help
|
||||||
quit, q kill debuggee and exit`)
|
quit, q kill debuggee and exit`, archSPLabel())
|
||||||
|
|
||||||
case "stack":
|
case "stack":
|
||||||
regs, _ := s.GetRegs()
|
regs, _ := s.GetRegs()
|
||||||
// For NOSPLIT frame=0: [RSP] = return address, [RSP+8..] = args.
|
sp := regs.GetSP()
|
||||||
retAddr, _ := s.Peek(regs.RSP)
|
retAddr, _ := archReturnAddr(s, ®s)
|
||||||
fmt.Printf(" [RSP] return addr = %#x\n", retAddr)
|
fmt.Printf(" [%s] return addr = %#x\n", archSPLabel(), retAddr)
|
||||||
if argsSize > 0 {
|
if argsSize > 0 {
|
||||||
fmt.Printf(" args (%d bytes at RSP+8):\n", argsSize)
|
fmt.Printf(" args (%d bytes at %s+8):\n", argsSize, archSPLabel())
|
||||||
argBytes, err := s.ReadMemory(regs.RSP+8, argsSize)
|
argBytes, err := s.ReadMemory(sp+8, argsSize)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for i := 0; i < argsSize; i += 8 {
|
for i := 0; i < argsSize; i += 8 {
|
||||||
var v uint64
|
var v uint64
|
||||||
@@ -420,7 +400,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
|||||||
|
|
||||||
case "bt", "backtrace":
|
case "bt", "backtrace":
|
||||||
regs, _ := s.GetRegs()
|
regs, _ := s.GetRegs()
|
||||||
funcOff := int(regs.RIP - codeBase - uint64(funcOffset))
|
funcOff := int(regs.GetPC() - codeBase - uint64(funcOffset))
|
||||||
line := lineAt(lines, funcOff)
|
line := lineAt(lines, funcOff)
|
||||||
label := nearestLabel(labels, funcOff)
|
label := nearestLabel(labels, funcOff)
|
||||||
fmt.Printf(" #0 func+%#x", 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.Printf(" [line %d]", line)
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
retAddr, _ := s.Peek(regs.RSP)
|
retAddr, _ := archReturnAddr(s, ®s)
|
||||||
fmt.Printf(" #1 return to %#x\n", retAddr)
|
fmt.Printf(" #1 return to %#x\n", retAddr)
|
||||||
|
|
||||||
case "watch":
|
case "watch":
|
||||||
@@ -499,74 +479,6 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
|||||||
s.Kill()
|
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) {
|
func hexDump(addr uint64, data []byte) {
|
||||||
for i := 0; i < len(data); i += 16 {
|
for i := 0; i < len(data); i += 16 {
|
||||||
end := 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) {
|
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") {
|
if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
|
||||||
v, err := strconv.ParseUint(s, 0, 64)
|
v, err := strconv.ParseUint(s, 0, 64)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return v, ""
|
return v, ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Try as func+offset.
|
|
||||||
if strings.HasPrefix(s, "+") {
|
if strings.HasPrefix(s, "+") {
|
||||||
off, err := strconv.ParseUint(s[1:], 0, 64)
|
off, err := strconv.ParseUint(s[1:], 0, 64)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return codeBase + funcOff + off, fmt.Sprintf("func+%#x", off)
|
return codeBase + funcOff + off, fmt.Sprintf("func+%#x", off)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Try as a label name.
|
|
||||||
for _, l := range labels {
|
for _, l := range labels {
|
||||||
if l.Name == s {
|
if l.Name == s {
|
||||||
return codeBase + funcOff + uint64(l.Offset), l.Name
|
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, ""
|
return 0, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// lineAt returns the source line for a given function-relative offset.
|
|
||||||
func lineAt(lines []SourceLine, offset int) int {
|
func lineAt(lines []SourceLine, offset int) int {
|
||||||
if len(lines) == 0 {
|
if len(lines) == 0 {
|
||||||
return 0
|
return 0
|
||||||
@@ -637,8 +545,6 @@ func lineAt(lines []SourceLine, offset int) int {
|
|||||||
return 0
|
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 {
|
func offsetForLine(lines []SourceLine, line int) int {
|
||||||
for _, le := range lines {
|
for _, le := range lines {
|
||||||
if le.Line == line {
|
if le.Line == line {
|
||||||
@@ -648,7 +554,6 @@ func offsetForLine(lines []SourceLine, line int) int {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// nearestLabel returns the name of the label at or just before the offset.
|
|
||||||
func nearestLabel(labels []Label, offset int) string {
|
func nearestLabel(labels []Label, offset int) string {
|
||||||
best := ""
|
best := ""
|
||||||
bestOff := -1
|
bestOff := -1
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,69 +5,7 @@
|
|||||||
|
|
||||||
package debug
|
package debug
|
||||||
|
|
||||||
import (
|
import "fmt"
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetReg modifies a register value in the debuggee.
|
// SetReg modifies a register value in the debuggee.
|
||||||
func (s *Session) SetReg(name string, value uint64) error {
|
func (s *Session) SetReg(name string, value uint64) error {
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-121
@@ -6,12 +6,9 @@
|
|||||||
package debug
|
package debug
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
@@ -20,12 +17,8 @@ import (
|
|||||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunTarget is the debuggee entry point (gasm debug --target). It
|
// RunTarget is the debuggee entry point (gasm debug --target).
|
||||||
// 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.
|
|
||||||
func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
||||||
// Parse and assemble.
|
|
||||||
src, err := os.ReadFile(asmPath)
|
src, err := os.ReadFile(asmPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("debug target: %w", err)
|
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)
|
return fmt.Errorf("debug target: assemble: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the function.
|
|
||||||
var fl *asm.FuncLayout
|
var fl *asm.FuncLayout
|
||||||
for i := range img.Funcs {
|
for i := range img.Funcs {
|
||||||
if img.Funcs[i].Name == funcName {
|
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)
|
return fmt.Errorf("debug target: function %q not found", funcName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map the entire image RWX (we need write access for breakpoints).
|
|
||||||
code := img.Bytes()
|
code := img.Bytes()
|
||||||
exec, err := mapRWX(code)
|
exec, err := mapRWX(code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("debug target: mmap: %w", err)
|
return fmt.Errorf("debug target: mmap: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the code base address for the parent.
|
|
||||||
codeBase := uintptr(unsafe.Pointer(&exec[0]))
|
codeBase := uintptr(unsafe.Pointer(&exec[0]))
|
||||||
if err := os.WriteFile(tmpDir+"/codebase", []byte(fmt.Sprintf("%d", codeBase)), 0o644); err != nil {
|
if err := os.WriteFile(tmpDir+"/codebase", []byte(fmt.Sprintf("%d", codeBase)), 0o644); err != nil {
|
||||||
return fmt.Errorf("debug target: write codebase: %w", err)
|
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)
|
meta := fmt.Sprintf("%d %d %d", fl.Offset, fl.Size, fl.Args)
|
||||||
os.WriteFile(tmpDir+"/funcmeta", []byte(meta), 0o644)
|
os.WriteFile(tmpDir+"/funcmeta", []byte(meta), 0o644)
|
||||||
|
|
||||||
// Write label table for breakpoint resolution.
|
|
||||||
labelsFile, _ := os.Create(tmpDir + "/labels")
|
labelsFile, _ := os.Create(tmpDir + "/labels")
|
||||||
if labelsFile != nil {
|
if labelsFile != nil {
|
||||||
for label, off := range fl.Labels {
|
for label, off := range fl.Labels {
|
||||||
@@ -77,7 +65,6 @@ func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
|||||||
labelsFile.Close()
|
labelsFile.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the argument block.
|
|
||||||
args, err := os.ReadFile(argsFile)
|
args, err := os.ReadFile(argsFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("debug target: read args: %w", err)
|
return fmt.Errorf("debug target: read args: %w", err)
|
||||||
@@ -88,7 +75,6 @@ func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
|||||||
args = padded
|
args = padded
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read buffer specification if present.
|
|
||||||
bufSpecFile := tmpDir + "/bufspec"
|
bufSpecFile := tmpDir + "/bufspec"
|
||||||
if bufSpec, err := os.ReadFile(bufSpecFile); err == nil && len(bufSpec) > 0 {
|
if bufSpec, err := os.ReadFile(bufSpecFile); err == nil && len(bufSpec) > 0 {
|
||||||
args, err = setupBuffers(string(bufSpec), args, fl.Args, tmpDir)
|
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()
|
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 {
|
if _, _, errno := syscall.Syscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_TRACEME), 0, 0); errno != 0 {
|
||||||
return fmt.Errorf("debug target: PTRACE_TRACEME: %v", errno)
|
return fmt.Errorf("debug target: PTRACE_TRACEME: %v", errno)
|
||||||
}
|
}
|
||||||
os.WriteFile(tmpDir+"/ready", []byte("ok"), 0o644)
|
os.WriteFile(tmpDir+"/ready", []byte("ok"), 0o644)
|
||||||
syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
|
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)
|
os.WriteFile(tmpDir+"/entry", []byte("ok"), 0o644)
|
||||||
syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
|
syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
|
||||||
|
|
||||||
// Prepare the ABI0 stack and call the function.
|
|
||||||
fnAddr := codeBase + uintptr(fl.Offset)
|
fnAddr := codeBase + uintptr(fl.Offset)
|
||||||
stackArgs := make([]byte, fl.Args)
|
stackArgs := make([]byte, fl.Args)
|
||||||
copy(stackArgs, args)
|
copy(stackArgs, args)
|
||||||
|
|
||||||
_, callErr := verify.Call(fnAddr, stackArgs)
|
_, callErr := verify.Call(fnAddr, stackArgs)
|
||||||
if callErr != nil {
|
if callErr != nil {
|
||||||
// The function returned an error (shouldn't happen for valid code).
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
return nil
|
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)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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")
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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")
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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")
|
||||||
|
}
|
||||||
@@ -11,11 +11,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Hardware watchpoint support via x86-64 debug registers (DR0-DR3, DR7).
|
// 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.
|
// WatchpointType selects what triggers the watchpoint.
|
||||||
type WatchpointType int
|
type WatchpointType int
|
||||||
@@ -25,11 +20,14 @@ const (
|
|||||||
WatchRead WatchpointType = 3 // trigger on read or write
|
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
|
// FindFreeWatchpointSlot returns the index of the first free watchpoint slot
|
||||||
// (0-3), or -1 if all four hardware watchpoints are in use.
|
// (0-3), or -1 if all four hardware watchpoints are in use.
|
||||||
func (s *Session) FindFreeWatchpointSlot() int {
|
func (s *Session) FindFreeWatchpointSlot() int {
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
if !s.wpSlots[i] {
|
if !wpSlots[i] {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,20 +39,18 @@ func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
|||||||
if slot < 0 || slot > 3 {
|
if slot < 0 || slot > 3 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return s.wpSlots[slot]
|
return wpSlots[slot]
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetWatchpoint installs a hardware watchpoint on the given address.
|
// 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 {
|
func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error {
|
||||||
if slot < 0 || slot > 3 {
|
if slot < 0 || slot > 3 {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-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)
|
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the length encoding.
|
|
||||||
var lenBits uint64
|
var lenBits uint64
|
||||||
switch size {
|
switch size {
|
||||||
case 1:
|
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")
|
return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the watched address to DR0-DR3.
|
|
||||||
var drAddr uintptr
|
var drAddr uintptr
|
||||||
switch slot {
|
switch slot {
|
||||||
case 0:
|
case 0:
|
||||||
drAddr = 0x0 // DR0 offset in user_regs_struct
|
drAddr = 0x0
|
||||||
case 1:
|
case 1:
|
||||||
drAddr = 0x8 // DR1
|
drAddr = 0x8
|
||||||
case 2:
|
case 2:
|
||||||
drAddr = 0x10 // DR2
|
drAddr = 0x10
|
||||||
case 3:
|
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 {
|
if err := ptracePokeUser(s.pid, drAddr, addr); err != nil {
|
||||||
return fmt.Errorf("debug: set DR%d: %w", slot, err)
|
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, err := ptracePeekUser(s.pid, 0x38) // DR7 offset
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("debug: read DR7: %w", err)
|
return fmt.Errorf("debug: read DR7: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
enableBit := uint64(1) << (2 * slot) // local enable
|
enableBit := uint64(1) << (2 * slot)
|
||||||
rwBits := uint64(typ) << (16 + 4*slot) // R/W type
|
rwBits := uint64(typ) << (16 + 4*slot)
|
||||||
lenField := lenBits << (18 + 4*slot) // length
|
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)))
|
mask := ^((uint64(1) << (2 * slot)) | (uint64(3) << (16 + 4*slot)) | (uint64(3) << (18 + 4*slot)))
|
||||||
dr7 = (dr7 & mask) | enableBit | rwBits | lenField
|
dr7 = (dr7 & mask) | enableBit | rwBits | lenField
|
||||||
|
|
||||||
if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil {
|
if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil {
|
||||||
return fmt.Errorf("debug: set DR7: %w", err)
|
return fmt.Errorf("debug: set DR7: %w", err)
|
||||||
}
|
}
|
||||||
s.wpSlots[slot] = true
|
wpSlots[slot] = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,26 +105,25 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
if slot < 0 || slot > 3 {
|
if slot < 0 || slot > 3 {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-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)
|
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)
|
dr7, err := ptracePeekUser(s.pid, 0x38)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dr7 &^= uint64(1) << (2 * slot) // disable
|
dr7 &^= uint64(1) << (2 * slot)
|
||||||
if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil {
|
if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.wpSlots[slot] = false
|
wpSlots[slot] = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearAllWatchpoints removes all hardware watchpoints.
|
// ClearAllWatchpoints removes all hardware watchpoints.
|
||||||
func (s *Session) ClearAllWatchpoints() error {
|
func (s *Session) ClearAllWatchpoints() error {
|
||||||
for slot := 0; slot < 4; slot++ {
|
for slot := 0; slot < 4; slot++ {
|
||||||
if s.wpSlots[slot] {
|
if wpSlots[slot] {
|
||||||
if err := s.ClearWatchpoint(slot); err != nil {
|
if err := s.ClearWatchpoint(slot); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -141,9 +132,8 @@ func (s *Session) ClearAllWatchpoints() error {
|
|||||||
return nil
|
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 {
|
func ptracePokeUser(pid int, offset uintptr, val uint64) error {
|
||||||
const ptracePokeuser = 6 // PTRACE_POKEUSER
|
const ptracePokeuser = 6
|
||||||
_, _, errno := syscall.Syscall6(
|
_, _, errno := syscall.Syscall6(
|
||||||
syscall.SYS_PTRACE,
|
syscall.SYS_PTRACE,
|
||||||
uintptr(ptracePokeuser),
|
uintptr(ptracePokeuser),
|
||||||
@@ -158,9 +148,8 @@ func ptracePokeUser(pid int, offset uintptr, val uint64) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ptracePeekUser reads a value from the debuggee's user area at the given offset.
|
|
||||||
func ptracePeekUser(pid int, offset uintptr) (uint64, error) {
|
func ptracePeekUser(pid int, offset uintptr) (uint64, error) {
|
||||||
const ptracePeekuser = 3 // PTRACE_PEEKUSER
|
const ptracePeekuser = 3
|
||||||
val, _, errno := syscall.Syscall6(
|
val, _, errno := syscall.Syscall6(
|
||||||
syscall.SYS_PTRACE,
|
syscall.SYS_PTRACE,
|
||||||
uintptr(ptracePeekuser),
|
uintptr(ptracePeekuser),
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user