feat(verify): add --call and --buf flags for single-function invocation

Assisted-by: GLM 5.2
This commit is contained in:
2026-08-05 20:46:39 +02:00
parent ece0d3f127
commit b0c62be8ce
6 changed files with 518 additions and 17 deletions
+140 -2
View File
@@ -823,7 +823,7 @@ func cmdVerifyRISCV(path string, groundTruth, profile bool) int {
}
func cmdVerify(args []string) int {
fs := newCommand("verify", "gasm verify [-smoke] [-abi] [-profile] <file.s>", `
fs := newCommand("verify", "gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <file.s>", `
Assemble FILE (amd64), map it into executable memory and report the available
functions. This confirms the assembled image is self-consistent (no
unresolved external symbols) and executable — the prerequisite for dynamic
@@ -836,7 +836,18 @@ that tolerate nil pointers and zero lengths in their arguments.
With -abi, each function is called with sentinel values in the callee-saved
registers (BP, R14) and a red-zone canary below SP; violations are reported.
With -fuzz, each function with a // func signature is differentially fuzzed
against the go-tool-asm version in a subprocess (so a crash on a partial
function is reported, not fatal).
With -ground-truth, the assembled machine code is compared byte-for-byte
against go tool asm (relocation sites masked), reporting any encoding drift.
With -profile, the static basic-block structure is listed for each function.
With -call, a single function is invoked with user-supplied buffers (-buf)
instead of the smoke/abi/fuzz sweeps. Useful for partial functions (e.g.
decoders) that crash on random input but should succeed on valid data.
`)
smoke := fs.Bool("smoke", false, "call each NOSPLIT function with zeroed args")
abi := fs.Bool("abi", false, "run ABI-checking calls (sentinel registers + red zone)")
@@ -846,9 +857,12 @@ With -profile, the static basic-block structure is listed for each function.
fuzz := fs.Bool("fuzz", false, "differential fuzz: JIT both gasm and go-tool-asm versions, compare outputs")
fuzzN := fs.Int("n", 1000, "number of fuzz iterations per function")
fuzzOne := fs.String("fuzz-one", "", "") // hidden: fuzz a single function (subprocess mode)
call := fs.String("call", "", "call a single function with -buf instead of the sweeps")
bufSpec := fs.String("buf", "", "buffer spec for -call: name:size:pattern[,name:size:pattern] (zero, ones, seq, or hex)")
repeat := fs.Int("repeat", 1, "number of times to repeat a -call invocation")
fs.Parse(args)
if fs.NArg() != 1 {
fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-profile] <file.s>")
fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <file.s>")
return 2
}
path := fs.Arg(0)
@@ -874,6 +888,11 @@ With -profile, the static basic-block structure is listed for each function.
fmt.Printf("%s: %d functions JIT-loaded\n", path, len(names))
rc := 0
// Single-function call mode: invoke one function with user-supplied buffers.
if *call != "" {
return cmdVerifyCall(k, path, *call, *bufSpec, *repeat)
}
// Subprocess mode: fuzz a single function and exit.
if *fuzzOne != "" {
gt, err := verify.GroundTruth(path)
@@ -1095,3 +1114,122 @@ func fuzzInSubprocess(path, funcName string, n int) string {
}
return strings.TrimSpace(string(out))
}
// cmdVerifyCall implements `gasm verify --call <func> [--buf spec] [--repeat n]`.
// It invokes a single function with user-supplied buffers and prints the arg
// block before and after the call, so the user can inspect return values and
// any output written to the buffers.
func cmdVerifyCall(k *verify.Kernel, path, funcName, bufSpec string, repeat int) int {
fl, err := k.Func(funcName)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1
}
if !fl.NoSplit {
fmt.Fprintf(os.Stderr, "gasm verify: %s is not NOSPLIT (frame=%d); --call supports NOSPLIT functions only\n", funcName, fl.Frame)
return 1
}
// Parse the // func signature to lay out the argument block.
src, err := readSource(path)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1
}
sig, ok := verify.ExtractFuncSig(src, funcName)
if !ok {
fmt.Fprintf(os.Stderr, "gasm verify: no // func signature found for %s\n", funcName)
return 1
}
layout := verify.ArgLayout(sig)
// Allocate the requested buffers (if any) and build the arg block.
specs, err := verify.ParseBufSpec(bufSpec)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1
}
var pool verify.BufPool
if err := pool.Alloc(specs); err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1
}
defer pool.Close()
args := pool.BuildArgs(layout, fl.Args)
fmt.Printf("%s: %d bytes, args=%d\n", funcName, fl.Size, fl.Args)
fmt.Printf(" signature: func %s(%s) %s\n", sig.Name, formatParams(sig.Params), formatResults(sig.Results))
if len(specs) > 0 {
fmt.Printf(" buffers:\n")
for _, s := range specs {
fmt.Printf(" %s: %d bytes, pattern=%s\n", s.Name, s.Size, s.Pattern)
}
}
fmt.Printf(" args before: %s\n", hexDump(args))
rc := 0
for i := 0; i < repeat; i++ {
out, err := k.CallFunc(funcName, args)
if err != nil {
fmt.Printf(" call %d: FAIL — %v\n", i+1, err)
rc = 1
continue
}
if repeat == 1 {
fmt.Printf(" args after: %s\n", hexDump(out))
} else if i == repeat-1 {
fmt.Printf(" args after %d calls: %s\n", repeat, hexDump(out))
}
fmt.Printf(" call %d: OK\n", i+1)
}
return rc
}
// formatParams renders a parameter list as "a []byte, b []byte".
func formatParams(ps []verify.Param) string {
var parts []string
for _, p := range ps {
if p.Name != "" {
parts = append(parts, p.Name+" "+p.Typ)
} else {
parts = append(parts, p.Typ)
}
}
return strings.Join(parts, ", ")
}
// formatResults renders a result list as "(n int, code int)" or "int".
func formatResults(rs []verify.Param) string {
if len(rs) == 0 {
return ""
}
if len(rs) == 1 && rs[0].Name == "" {
return rs[0].Typ
}
return "(" + formatParams(rs) + ")"
}
// hexDump returns a one-line hex dump of buf, truncated to 64 bytes.
func hexDump(buf []byte) string {
const max = 64
n := len(buf)
if n > max {
n = max
}
var sb strings.Builder
for i := 0; i < n; i++ {
if i > 0 {
sb.WriteByte(' ')
}
fmt.Fprintf(&sb, "%02x", buf[i])
}
return fmt.Sprintf("%s%s (%d bytes)", sb.String(), truncMark(len(buf), max), len(buf))
}
// truncMark returns "…" when the buffer is longer than max, else "".
func truncMark(n, max int) string {
if n > max {
return "…"
}
return ""
}