127 lines
3.6 KiB
Go
127 lines
3.6 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 (
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
"os/exec"
|
||
|
|
"path/filepath"
|
||
|
|
"runtime"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||
|
|
)
|
||
|
|
|
||
|
|
// buildGasm produces the gasm binary the debugger spawns as its debuggee.
|
||
|
|
func buildGasm(t *testing.T) string {
|
||
|
|
t.Helper()
|
||
|
|
if p := os.Getenv("GASM_TEST_BIN"); p != "" {
|
||
|
|
return p
|
||
|
|
}
|
||
|
|
bin := filepath.Join(t.TempDir(), "gasm")
|
||
|
|
cmd := exec.Command("go", "build", "-o", bin, "sourcedock.dev/petrbalvin/gasm-devkit/cmd/gasm")
|
||
|
|
out, err := cmd.CombinedOutput()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("build gasm: %v: %s", err, out)
|
||
|
|
}
|
||
|
|
return bin
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLaunchAndBreakpoint drives a real ptrace session end to end: launch the
|
||
|
|
// debuggee, break on the first instruction of the function and expect a
|
||
|
|
// breakpoint trap instead of a clean exit.
|
||
|
|
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 (PTRACE_TRACEME
|
||
|
|
// 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 := buildGasm(t)
|
||
|
|
|
||
|
|
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 {
|
||
|
|
st, _ := os.ReadFile(fmt.Sprintf("/proc/%d/stat", sess.Pid()))
|
||
|
|
status, _ := os.ReadFile(fmt.Sprintf("/proc/%d/status", sess.Pid()))
|
||
|
|
t.Fatalf("Continue: %v\nstate: %s\n%s", err, fieldName(st), statusDump(status))
|
||
|
|
}
|
||
|
|
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")
|
||
|
|
}
|
||
|
|
|
||
|
|
func fieldName(stat []byte) string {
|
||
|
|
f := strings.Split(string(stat), " ")
|
||
|
|
if len(f) > 2 {
|
||
|
|
return "state=" + f[2]
|
||
|
|
}
|
||
|
|
return "no stat"
|
||
|
|
}
|
||
|
|
|
||
|
|
func statusDump(b []byte) string {
|
||
|
|
var out []string
|
||
|
|
for _, l := range strings.Split(string(b), "\n") {
|
||
|
|
if strings.HasPrefix(l, "State") || strings.HasPrefix(l, "Pid") ||
|
||
|
|
strings.HasPrefix(l, "PPid") || strings.HasPrefix(l, "TracerPid") ||
|
||
|
|
strings.HasPrefix(l, "Threads") || strings.HasPrefix(l, "SigPnd") ||
|
||
|
|
strings.HasPrefix(l, "SigBlk") || strings.HasPrefix(l, "SigIgn") {
|
||
|
|
out = append(out, l)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return strings.Join(out, "\n")
|
||
|
|
}
|