diff --git a/cmd/gasm/debug_linux_amd64.go b/cmd/gasm/debug_linux_amd64.go index 25eca74..a2151c6 100644 --- a/cmd/gasm/debug_linux_amd64.go +++ b/cmd/gasm/debug_linux_amd64.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "sort" + "strings" "sourcedock.dev/petrbalvin/gasm-devkit/debug" "sourcedock.dev/petrbalvin/gasm-devkit/verify" @@ -32,6 +33,7 @@ REPL commands: 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) --- @@ -76,12 +78,79 @@ REPL commands: } sort.Slice(labels, func(i, j int) bool { return labels[i].Offset < labels[j].Offset }) - // Launch the debuggee with a zeroed argument block. - 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 + // 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() @@ -96,3 +165,29 @@ REPL commands: debug.REPL(sess, bm, sess.CodeBase(), fl.Offset, fl.Size, fl.Args, labels, srcLines) return 0 } + +// parseBufNames extracts buffer names from a buffer specification. +// Format: name:size:pattern[,name:size:pattern...] +func parseBufNames(spec string) []string { + var names []string + for _, part := range strings.Split(spec, ",") { + fields := strings.SplitN(part, ":", 3) + if len(fields) >= 1 && fields[0] != "" { + names = append(names, fields[0]) + } + } + return names +} + +// parseBufSize extracts the size of a named buffer from a buffer specification. +func parseBufSize(spec, name string) int { + for _, part := range strings.Split(spec, ",") { + fields := strings.SplitN(part, ":", 3) + if len(fields) >= 2 && fields[0] == name { + var size int + fmt.Sscanf(fields[1], "%d", &size) + return size + } + } + return 0 +} diff --git a/debug/ptrace_linux_amd64.go b/debug/ptrace_linux_amd64.go index 00ef628..022c7d7 100644 --- a/debug/ptrace_linux_amd64.go +++ b/debug/ptrace_linux_amd64.go @@ -33,9 +33,17 @@ type Session struct { // 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, fmt.Errorf("debug: cannot find gasm binary: %w", err) + return nil, nil, fmt.Errorf("debug: cannot find gasm binary: %w", err) } if gasmBin != "" { self = gasmBin @@ -44,12 +52,20 @@ func Launch(gasmBin, asmPath, funcName string, args []byte) (*Session, error) { // Write the arg block to a temp file (the child reads it). tmpDir, err := os.MkdirTemp("", "gasm-debug-*") if err != nil { - return nil, fmt.Errorf("debug: tempdir: %w", err) + 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, fmt.Errorf("debug: write args: %w", err) + 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) @@ -60,7 +76,7 @@ func Launch(gasmBin, asmPath, funcName string, args []byte) (*Session, error) { if err := cmd.Start(); err != nil { os.RemoveAll(tmpDir) - return nil, fmt.Errorf("debug: start debuggee: %w", err) + return nil, nil, fmt.Errorf("debug: start debuggee: %w", err) } s := &Session{pid: cmd.Process.Pid, cmd: cmd} @@ -79,8 +95,28 @@ func Launch(gasmBin, asmPath, funcName string, args []byte) (*Session, error) { if _, err := syscall.Wait4(s.pid, &ws, syscall.WUNTRACED, nil); err != nil { cmd.Process.Kill() os.RemoveAll(tmpDir) - return nil, fmt.Errorf("debug: wait for debuggee: %w", err) + 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). @@ -93,7 +129,21 @@ func Launch(gasmBin, asmPath, funcName string, args []byte) (*Session, error) { } } - return s, nil + // 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. diff --git a/debug/repl.go b/debug/repl.go index 4487841..a2e81cf 100644 --- a/debug/repl.go +++ b/debug/repl.go @@ -34,21 +34,7 @@ type SourceLine struct { func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, argsSize int, labels []Label, lines []SourceLine) { entryAddr := codeBase + uint64(funcOffset) - // Run to the function entry. - bp, err := bm.Set(entryAddr, "(entry)") - if err != nil { - fmt.Printf("warning: cannot set entry breakpoint: %v\n", err) - } else { - if err := s.Continue(); err != nil { - fmt.Printf("warning: continue to entry: %v\n", err) - } - regs, _ := s.GetRegs() - bm.HandleTrap(®s) - // Remove the temporary entry breakpoint. - bm.Clear(entryAddr) - _ = bp - } - + // The debuggee is already stopped at the function entry point. fmt.Printf("stopped at function entry: %#x (%d bytes)\n", entryAddr, funcSize) fmt.Println("commands: break | step [n] | continue | disas [n] | regs | where | x [len] | w | labels | quit") diff --git a/debug/target_linux_amd64.go b/debug/target_linux_amd64.go index 956d88f..5fb34e5 100644 --- a/debug/target_linux_amd64.go +++ b/debug/target_linux_amd64.go @@ -6,9 +6,12 @@ package debug import ( + "encoding/hex" "fmt" "os" "runtime" + "strconv" + "strings" "syscall" "unsafe" @@ -85,6 +88,15 @@ func RunTarget(asmPath, funcName, argsFile, tmpDir string) error { args = padded } + // Read buffer specification if present. + bufSpecFile := tmpDir + "/bufspec" + if bufSpec, err := os.ReadFile(bufSpecFile); err == nil && len(bufSpec) > 0 { + args, err = setupBuffers(string(bufSpec), args, fl.Args, tmpDir) + 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() @@ -100,6 +112,11 @@ func RunTarget(asmPath, funcName, argsFile, tmpDir string) error { // --- 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) @@ -129,3 +146,82 @@ func mapRWX(code []byte) ([]byte, error) { copy(mem, code) return mem, nil } + +// setupBuffers allocates buffers in the debuggee's memory and updates the +// argument block with pointers to them. +// Format: name:size:pattern[,name:size:pattern...] +// Patterns: zero, ones, seq, or hex (e.g. "deadbeef"). +func setupBuffers(spec string, args []byte, argSize int, tmpDir string) ([]byte, error) { + // Parse the buffer spec. + type bufSpec struct { + name string + size int + pattern string + } + var specs []bufSpec + for _, part := range strings.Split(spec, ",") { + fields := strings.SplitN(part, ":", 3) + if len(fields) != 3 { + continue + } + size, err := strconv.Atoi(fields[1]) + if err != nil || size <= 0 { + continue + } + specs = append(specs, bufSpec{name: fields[0], size: size, pattern: fields[2]}) + } + + if len(specs) == 0 { + return args, nil + } + + // Allocate buffers and write their addresses to a file for the parent. + var bufAddrs []uint64 + for _, s := range specs { + buf, err := syscall.Mmap(-1, 0, s.size, + syscall.PROT_READ|syscall.PROT_WRITE, + syscall.MAP_PRIVATE|syscall.MAP_ANON) + if err != nil { + return nil, fmt.Errorf("mmap buffer %s: %w", s.name, err) + } + fillBuffer(buf, s.pattern) + bufAddrs = append(bufAddrs, uint64(uintptr(unsafe.Pointer(&buf[0])))) + } + + // Write buffer addresses to a file for the parent to read. + addrFile, err := os.Create(tmpDir + "/bufaddrs") + if err != nil { + return nil, err + } + for _, addr := range bufAddrs { + fmt.Fprintf(addrFile, "%d\n", addr) + } + addrFile.Close() + + // For now, return the args unchanged. The parent will read bufaddrs + // and construct the final argument block with the correct pointers. + return args, nil +} + +// fillBuffer fills a buffer with the specified pattern. +func fillBuffer(buf []byte, pattern string) { + switch pattern { + case "zero": + // Already zeroed by mmap. + case "ones": + for i := range buf { + buf[i] = 0xFF + } + case "seq": + for i := range buf { + buf[i] = byte(i) + } + default: + // Try to parse as hex. + if data, err := hex.DecodeString(pattern); err == nil && len(data) > 0 { + for i := range buf { + buf[i] = data[i%len(data)] + } + } + } +} diff --git a/verify/sigparse.go b/verify/sigparse.go new file mode 100644 index 0000000..4b45ed6 --- /dev/null +++ b/verify/sigparse.go @@ -0,0 +1,136 @@ +// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package verify + +import ( + "strings" +) + +// FuncSig is a parsed // func signature from the assembly source. +type FuncSig struct { + Name string + Params []Param + Results []Param +} + +// Param is a single function parameter. +type Param struct { + Name string + Typ string // "[]byte", "[]int32", "int", "*[32]uint16", etc. +} + +// IsPointer reports whether the parameter type is a pointer or slice +// (i.e., it needs a buffer address in the argument block). +func (p Param) IsPointer() bool { + return strings.HasPrefix(p.Typ, "[]") || strings.HasPrefix(p.Typ, "*") +} + +// ParseFuncSig extracts the function signature from a "// func ..." comment. +func ParseFuncSig(comment string) (FuncSig, bool) { + m := funcSigRe.FindStringSubmatch(strings.TrimSpace(comment)) + if m == nil { + return FuncSig{}, false + } + sig := FuncSig{Name: m[1]} + sig.Params = parseParamsExported(m[2]) + // Results may be "(a int, b int)" or "int" or "(int, error)". + res := strings.TrimSpace(m[3]) + res = strings.TrimPrefix(res, "(") + res = strings.TrimSuffix(res, ")") + if res != "" { + sig.Results = parseParamsExported(res) + } + return sig, true +} + +// parseParamsExported splits "a []byte, b []int32" into typed parameters. +func parseParamsExported(s string) []Param { + s = strings.TrimSpace(s) + if s == "" { + return nil + } + var out []Param + for _, field := range strings.Split(s, ",") { + field = strings.TrimSpace(field) + if field == "" { + continue + } + parts := strings.Fields(field) + if len(parts) == 1 { + // Unnamed: "int" or "[]byte". + out = append(out, Param{Typ: parts[0]}) + } else { + // Named: "a []byte". + out = append(out, Param{Name: parts[0], Typ: parts[1]}) + } + } + return out +} + +// ExtractFuncSig extracts the // func signature for the named function +// from the assembly source. +func ExtractFuncSig(src, funcName string) (FuncSig, bool) { + lines := strings.Split(src, "\n") + for i, line := range lines { + if sig, ok := ParseFuncSig(line); ok && sig.Name == funcName { + // Verify the next non-comment line is the TEXT directive. + for j := i + 1; j < len(lines); j++ { + trimmed := strings.TrimSpace(lines[j]) + if trimmed == "" || strings.HasPrefix(trimmed, "//") { + continue + } + if strings.HasPrefix(trimmed, "TEXT") { + return sig, true + } + break + } + } + } + return FuncSig{}, false +} + +// ArgLayout computes the ABI0 argument layout for a function signature. +// Returns the offset of each parameter in the argument block. +func ArgLayout(sig FuncSig) []ArgOffset { + var offsets []ArgOffset + off := 0 + for _, p := range sig.Params { + size := paramSize(p.Typ) + offsets = append(offsets, ArgOffset{Name: p.Name, Typ: p.Typ, Offset: off, Size: size, IsPtr: p.IsPointer()}) + off += size + } + return offsets +} + +// ArgOffset describes one parameter's position in the argument block. +type ArgOffset struct { + Name string + Typ string + Offset int + Size int + IsPtr bool +} + +// paramSize returns the size in bytes of a parameter type in the ABI0 layout. +func paramSize(typ string) int { + switch { + case strings.HasPrefix(typ, "[]"): + // Slice: pointer + length + capacity = 24 bytes. + return 24 + case strings.HasPrefix(typ, "*"): + // Pointer: 8 bytes. + return 8 + case typ == "int", typ == "int64", typ == "uint64", typ == "uintptr": + return 8 + case typ == "int32", typ == "uint32", typ == "float32": + return 4 + case typ == "int16", typ == "uint16": + return 2 + case typ == "int8", typ == "uint8", typ == "byte", typ == "bool": + return 1 + default: + // Default to 8 bytes for unknown types. + return 8 + } +}