228 lines
6.4 KiB
Go
228 lines
6.4 KiB
Go
// 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 (
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
|
)
|
|
|
|
// RunTarget is the debuggee entry point (gasm debug --target). It
|
|
// assembles the file, maps the JIT code, registers itself for ptrace,
|
|
// stops, and then executes the named function. The parent debugger
|
|
// controls execution from there.
|
|
func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
|
// Parse and assemble.
|
|
src, err := os.ReadFile(asmPath)
|
|
if err != nil {
|
|
return fmt.Errorf("debug target: %w", err)
|
|
}
|
|
file, errs := parser.Parse(asmPath, string(src))
|
|
if len(errs) > 0 {
|
|
return fmt.Errorf("debug target: parse: %v", errs[0])
|
|
}
|
|
img, err := asm.AssembleFile(file)
|
|
if err != nil {
|
|
return fmt.Errorf("debug target: assemble: %w", err)
|
|
}
|
|
|
|
// Find the function.
|
|
var fl *asm.FuncLayout
|
|
for i := range img.Funcs {
|
|
if img.Funcs[i].Name == funcName {
|
|
fl = &img.Funcs[i]
|
|
break
|
|
}
|
|
}
|
|
if fl == nil {
|
|
return fmt.Errorf("debug target: function %q not found", funcName)
|
|
}
|
|
|
|
// Map the entire image RWX (we need write access for breakpoints).
|
|
code := img.Bytes()
|
|
exec, err := mapRWX(code)
|
|
if err != nil {
|
|
return fmt.Errorf("debug target: mmap: %w", err)
|
|
}
|
|
|
|
// Write the code base address for the parent.
|
|
codeBase := uintptr(unsafe.Pointer(&exec[0]))
|
|
if err := os.WriteFile(tmpDir+"/codebase", []byte(fmt.Sprintf("%d", codeBase)), 0o644); err != nil {
|
|
return fmt.Errorf("debug target: write codebase: %w", err)
|
|
}
|
|
|
|
// Write function metadata (offset, size, args) for the parent.
|
|
meta := fmt.Sprintf("%d %d %d", fl.Offset, fl.Size, fl.Args)
|
|
os.WriteFile(tmpDir+"/funcmeta", []byte(meta), 0o644)
|
|
|
|
// Write label table for breakpoint resolution.
|
|
labelsFile, _ := os.Create(tmpDir + "/labels")
|
|
if labelsFile != nil {
|
|
for label, off := range fl.Labels {
|
|
fmt.Fprintf(labelsFile, "%s %d\n", label, off)
|
|
}
|
|
labelsFile.Close()
|
|
}
|
|
|
|
// Read the argument block.
|
|
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
|
|
}
|
|
|
|
// Read buffer specification if present.
|
|
bufSpecFile := tmpDir + "/bufspec"
|
|
if bufSpec, err := os.ReadFile(bufSpecFile); err == nil && len(bufSpec) > 0 {
|
|
args, err = setupBuffers(string(bufSpec), args, fl.Args, tmpDir)
|
|
if err != nil {
|
|
return fmt.Errorf("debug target: setup buffers: %w", err)
|
|
}
|
|
}
|
|
|
|
// Lock this goroutine to the current OS thread so the parent's
|
|
// ptrace (attached to this thread) controls the JIT execution.
|
|
runtime.LockOSThread()
|
|
|
|
// Request tracing by the parent, then stop. PTRACE_TRACEME makes
|
|
// the subsequent SIGSTOP a ptrace-stop (not a group-stop), giving
|
|
// the parent full control from the start.
|
|
if _, _, errno := syscall.Syscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_TRACEME), 0, 0); errno != 0 {
|
|
return fmt.Errorf("debug target: PTRACE_TRACEME: %v", errno)
|
|
}
|
|
os.WriteFile(tmpDir+"/ready", []byte("ok"), 0o644)
|
|
syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
|
|
|
|
// --- Execution resumes here after the parent continues us ---
|
|
|
|
// Stop at the function entry point so the debugger can set breakpoints.
|
|
// The parent will continue us when ready.
|
|
os.WriteFile(tmpDir+"/entry", []byte("ok"), 0o644)
|
|
syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
|
|
|
|
// Prepare the ABI0 stack and call the function.
|
|
fnAddr := codeBase + uintptr(fl.Offset)
|
|
stackArgs := make([]byte, fl.Args)
|
|
copy(stackArgs, args)
|
|
|
|
_, callErr := verify.Call(fnAddr, stackArgs)
|
|
if callErr != nil {
|
|
// The function returned an error (shouldn't happen for valid code).
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
return nil
|
|
}
|
|
|
|
// mapRWX maps code into a read-write-execute region (needed for
|
|
// breakpoint patching via ptrace POKETEXT, though ptrace can write
|
|
// to any mapping regardless of permissions).
|
|
func mapRWX(code []byte) ([]byte, error) {
|
|
const pageSize = 4096
|
|
size := (len(code) + pageSize - 1) &^ (pageSize - 1)
|
|
mem, err := syscall.Mmap(-1, 0, size,
|
|
syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC,
|
|
syscall.MAP_PRIVATE|syscall.MAP_ANON)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copy(mem, code)
|
|
return mem, nil
|
|
}
|
|
|
|
// setupBuffers allocates buffers in the debuggee's memory and updates the
|
|
// argument block with pointers to them.
|
|
// Format: name:size:pattern[,name:size:pattern...]
|
|
// Patterns: zero, ones, seq, or hex (e.g. "deadbeef").
|
|
func setupBuffers(spec string, args []byte, argSize int, tmpDir string) ([]byte, error) {
|
|
// Parse the buffer spec.
|
|
type bufSpec struct {
|
|
name string
|
|
size int
|
|
pattern string
|
|
}
|
|
var specs []bufSpec
|
|
for _, part := range strings.Split(spec, ",") {
|
|
fields := strings.SplitN(part, ":", 3)
|
|
if len(fields) != 3 {
|
|
continue
|
|
}
|
|
size, err := strconv.Atoi(fields[1])
|
|
if err != nil || size <= 0 {
|
|
continue
|
|
}
|
|
specs = append(specs, bufSpec{name: fields[0], size: size, pattern: fields[2]})
|
|
}
|
|
|
|
if len(specs) == 0 {
|
|
return args, nil
|
|
}
|
|
|
|
// Allocate buffers and write their addresses to a file for the parent.
|
|
var bufAddrs []uint64
|
|
for _, s := range specs {
|
|
buf, err := syscall.Mmap(-1, 0, s.size,
|
|
syscall.PROT_READ|syscall.PROT_WRITE,
|
|
syscall.MAP_PRIVATE|syscall.MAP_ANON)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("mmap buffer %s: %w", s.name, err)
|
|
}
|
|
fillBuffer(buf, s.pattern)
|
|
bufAddrs = append(bufAddrs, uint64(uintptr(unsafe.Pointer(&buf[0]))))
|
|
}
|
|
|
|
// Write buffer addresses to a file for the parent to read.
|
|
addrFile, err := os.Create(tmpDir + "/bufaddrs")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, addr := range bufAddrs {
|
|
fmt.Fprintf(addrFile, "%d\n", addr)
|
|
}
|
|
addrFile.Close()
|
|
|
|
// For now, return the args unchanged. The parent will read bufaddrs
|
|
// and construct the final argument block with the correct pointers.
|
|
return args, nil
|
|
}
|
|
|
|
// fillBuffer fills a buffer with the specified pattern.
|
|
func fillBuffer(buf []byte, pattern string) {
|
|
switch pattern {
|
|
case "zero":
|
|
// Already zeroed by mmap.
|
|
case "ones":
|
|
for i := range buf {
|
|
buf[i] = 0xFF
|
|
}
|
|
case "seq":
|
|
for i := range buf {
|
|
buf[i] = byte(i)
|
|
}
|
|
default:
|
|
// Try to parse as hex.
|
|
if data, err := hex.DecodeString(pattern); err == nil && len(data) > 0 {
|
|
for i := range buf {
|
|
buf[i] = data[i%len(data)]
|
|
}
|
|
}
|
|
}
|
|
}
|