92 lines
2.6 KiB
Go
92 lines
2.6 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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")
|
|
}
|