// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause //go:build freebsd && amd64 package debug import ( "os/exec" "path/filepath" "runtime" "testing" "sourcedock.dev/petrbalvin/gasm-sdk/verify" ) // TestLaunchAndBreakpoint is the FreeBSD twin of the Linux integration // test: it drives the whole launch, breakpoint, trap and register-rewind // flow end to end. It needs a real FreeBSD kernel (ptrace does not work // under emulation), so it only runs where it can. func TestLaunchAndBreakpoint(t *testing.T) { if runtime.GOARCH != "amd64" { t.Skip("runs only on amd64 hosts") } // The tracer is the OS thread that forked the debuggee (PT_TRACE_ME // binds the relation to that thread); every ptrace request must come // from the same thread, so pin the test goroutine to one thread. runtime.LockOSThread() defer runtime.UnlockOSThread() bin := filepath.Join(t.TempDir(), "gasm") out, err := exec.Command("go", "build", "-o", bin, "sourcedock.dev/petrbalvin/gasm-sdk/cmd/gasm").CombinedOutput() if err != nil { t.Fatalf("build gasm: %v: %s", err, out) } const kernelPath = "../testdata/verify/basic_amd64.s" k, err := verify.Load(kernelPath) if err != nil { t.Fatalf("Load: %v", err) } t.Cleanup(k.Close) fl, err := k.Func("wideCopy") if err != nil { t.Fatalf("Func: %v", err) } sess, err := Launch(bin, kernelPath, "wideCopy", make([]byte, fl.Args)) if err != nil { t.Fatalf("Launch: %v", err) } t.Cleanup(sess.Kill) bm := NewBreakpoints(sess) entry := sess.CodeBase() + uint64(fl.Offset) if _, err := bm.Set(entry, "entry"); err != nil { t.Fatalf("Set: %v", err) } // The INT3 must be visible in the debuggee's memory. word, err := sess.Peek(entry) if err != nil { t.Fatalf("Peek: %v", err) } if b := word & 0xFF; b != 0xCC { t.Fatalf("int3 not patched: first byte %#02x at %#x", b, entry) } // The debuggee raises a second SIGSTOP after the launch barrier (the // child's RunTarget marks its entry), so like the REPL and the cover // mode the test keeps resuming until the breakpoint trap arrives. for range 10 { if err := sess.Continue(); err != nil { t.Fatalf("Continue: %v", err) } if sess.Exited() { t.Fatal("debuggee exited instead of trapping on the breakpoint") } regs, err := sess.GetRegs() if err != nil { t.Fatalf("GetRegs: %v", err) } if bp := bm.HandleTrap(®s); bp != nil { if bp.Addr != entry { t.Fatalf("trap at %#x, want %#x", bp.Addr, entry) } return // trap on the entry breakpoint: the whole flow works } } t.Fatal("no breakpoint trap after 10 resumes") }