// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause //go:build freebsd && (amd64 || arm64 || riscv64) package debug import ( "encoding/hex" "fmt" "os" "runtime" "strconv" "strings" "syscall" "unsafe" "golang.org/x/sys/unix" "sourcedock.dev/petrbalvin/gasm-devkit/asm" "sourcedock.dev/petrbalvin/gasm-devkit/parser" "sourcedock.dev/petrbalvin/gasm-devkit/verify" ) // 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, tmpDir string) ([]byte, error) { type bufSpec struct { name string size int pattern string } var specs []bufSpec for part := range strings.SplitSeq(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)] } } } } // 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]) } img, err := asm.AssembleFile(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, tmpDir) if err != nil { return fmt.Errorf("debug target: setup buffers: %w", err) } } runtime.LockOSThread() if _, _, errno := unix.RawSyscall(unix.SYS_PTRACE, uintptr(unix.PT_TRACE_ME), 0, 0); errno != 0 { return fmt.Errorf("debug target: PT_TRACE_ME: %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) fnAddr := codeBase + uintptr(fl.Offset) stackArgs := make([]byte, fl.Args) copy(stackArgs, args) if _, callErr := verify.Call(fnAddr, stackArgs); callErr != nil { os.Exit(1) } // Success returns to the caller, which exits with status 0; the JIT // code has already run to its own trampoline by the time Call returns. return nil }