// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause // Command gasm is the developer frontend for GAsm — Go's Plan 9 assembler. // It bundles a token dumper, a parser, a formatter, a linter and a language // server into one binary. Every subcommand works headlessly so it can be // driven from scripts and CI as well as from an editor. package main import ( "bytes" "encoding/json" "flag" "fmt" "io" "io/fs" "os" "os/exec" "path/filepath" "runtime" "slices" "sort" "strconv" "strings" "sync" "syscall" "sourcedock.dev/petrbalvin/gasm-devkit/arch" "sourcedock.dev/petrbalvin/gasm-devkit/asm" "sourcedock.dev/petrbalvin/gasm-devkit/ast" "sourcedock.dev/petrbalvin/gasm-devkit/format" "sourcedock.dev/petrbalvin/gasm-devkit/lexer" "sourcedock.dev/petrbalvin/gasm-devkit/lint" "sourcedock.dev/petrbalvin/gasm-devkit/lsp" "sourcedock.dev/petrbalvin/gasm-devkit/parser" "sourcedock.dev/petrbalvin/gasm-devkit/verify" ) // version is the release version, stamped at build time via // -ldflags "-X main.version=…" (defaulting to the current release). var version = "0.32.0-dev" func main() { if len(os.Args) < 2 { usage(os.Stderr) os.Exit(2) } switch os.Args[1] { case "tokens": os.Exit(cmdTokens(os.Args[2:])) case "parse": os.Exit(cmdParse(os.Args[2:])) case "fmt": os.Exit(cmdFmt(os.Args[2:])) case "lint": os.Exit(cmdLint(os.Args[2:])) case "asm": os.Exit(cmdAsm(os.Args[2:])) case "verify": os.Exit(cmdVerify(os.Args[2:])) case "debug": os.Exit(cmdDebug(os.Args[2:])) case "diff": os.Exit(cmdDiff(os.Args[2:])) case "profile": os.Exit(cmdProfile(os.Args[2:])) case "audit-instructions": if err := cmdAuditInstructions(os.Args[2:]); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } case "scaffold": if err := cmdScaffold(os.Args[2:]); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } case "lsp": os.Exit(cmdLSP(os.Args[2:])) case "version", "--version", "-V": os.Exit(cmdVersion()) case "help", "--help", "-h": usage(os.Stdout) default: fmt.Fprintf(os.Stderr, "gasm: unknown command %q — run \"gasm --help\" for usage\n", os.Args[1]) os.Exit(2) } } // cmdVersion prints the release version. func cmdVersion() int { fmt.Printf("gasm %s\n", version) return 0 } // ANSI color helpers for terminal output. const ( colorReset = "\033[0m" colorBold = "\033[1m" colorCyan = "\033[36m" colorYellow = "\033[33m" colorGray = "\033[90m" ) // isTTY reports whether the writer is a terminal (for color output). func isTTY(w io.Writer) bool { if f, ok := w.(*os.File); ok { stat, _ := f.Stat() return (stat.Mode() & os.ModeCharDevice) != 0 } return false } func usage(w io.Writer) { useColor := isTTY(w) bold, cyan, yellow, gray, reset := "", "", "", "", "" if useColor { bold, cyan, yellow, gray, reset = colorBold, colorCyan, colorYellow, colorGray, colorReset } fmt.Fprintf(w, "%sgasm %s%s — developer tooling for Go's Plan 9 assembler (GAsm)%s\n\n", bold, version, reset, reset) fmt.Fprintf(w, "gasm bundles a lexer, parser, formatter, linter, standalone assembler and\n") fmt.Fprintf(w, "language server for Plan 9 assembly into one self-contained binary.\n\n") fmt.Fprintf(w, "%sUsage:%s\n", yellow, reset) fmt.Fprintf(w, " gasm [arguments]\n") fmt.Fprintf(w, " gasm [flags]\n\n") fmt.Fprintf(w, "%sCommands:%s\n", yellow, reset) commands := []struct{ name, desc string }{ {"tokens", "print the lexical token stream"}, {"parse", "parse and report syntax errors"}, {"fmt", "canonicalise formatting (gofmt for assembly)"}, {"lint", "run static checks"}, {"asm", "assemble .s files to machine code (amd64, arm64, riscv64, loong64)"}, {"verify", "JIT-assemble and run dynamic checks (amd64, arm64, riscv64, loong64)"}, {"debug", "interactive source-level debugger (amd64, arm64, riscv64, loong64)"}, {"diff", "compare machine code of two .s files"}, {"profile", "show basic-block structure of functions"}, {"audit-instructions", "diff the encoder against the Go toolchain's name table"}, {"scaffold", "generate a differential test skeleton for a kernel file"}, {"lsp", "run the language server over stdio"}, {"version", "print the version (same as --version)"}, } for _, c := range commands { fmt.Fprintf(w, " %s%-10s%s %s%s%s\n", cyan, c.name, reset, gray, c.desc, reset) } fmt.Fprintf(w, "\n%sFlags:%s\n", yellow, reset) fmt.Fprintf(w, " %s-h, --help%s %sshow this help%s\n", cyan, reset, gray, reset) fmt.Fprintf(w, " %s-V, --version%s %sprint the version%s\n", cyan, reset, gray, reset) fmt.Fprintf(w, "\nRun \"gasm -h\" for a command's usage and flags.\n\n") fmt.Fprintf(w, "%sExamples:%s\n", yellow, reset) examples := []struct{ cmd, desc string }{ {"gasm fmt", "reformat every .s below the current directory"}, {"gasm lint go-flac/*.s", "run static checks over the kernels"}, {"gasm asm -o k.bin kern_amd64.s", ""}, {"gasm asm --format elf -o k.o kern_amd64.s", ""}, {"gasm asm --format goobj -p pkg/path -o k.o kern_amd64.s", ""}, } for _, e := range examples { if e.desc != "" { fmt.Fprintf(w, " %s%s%s %s%s%s\n", cyan, e.cmd, reset, gray, e.desc, reset) } else { fmt.Fprintf(w, " %s%s%s\n", cyan, e.cmd, reset) } } } // newCommand returns the FlagSet of a subcommand whose -h/--help prints a // proper usage block: the one-line usage, the long description and the flag // defaults. The flag package routes -h/--help to fs.Usage and exits 0. func newCommand(name, usageLine, long string) *flag.FlagSet { fs := flag.NewFlagSet(name, flag.ExitOnError) fs.Usage = func() { w := fs.Output() fmt.Fprintf(w, "Usage: %s\n\n%s\n", usageLine, strings.TrimSpace(long)) hasFlags := false fs.VisitAll(func(*flag.Flag) { hasFlags = true }) if hasFlags { fmt.Fprintln(w, "\nFlags:") fs.PrintDefaults() } } return fs } // readSource returns the contents of path, or stdin when path is "-". // hostArch maps the running GOARCH onto the arch package's identifiers. // It returns arch.Unknown on hosts the toolkit cannot JIT for. func hostArch() arch.Arch { switch runtime.GOARCH { case "amd64": return arch.AMD64 case "arm64": return arch.ARM64 case "riscv64": return arch.RISCV case "loong64": return arch.LOONG64 } return arch.Unknown } func readSource(path string) (string, error) { if path == "-" { b, err := io.ReadAll(os.Stdin) return string(b), err } b, err := os.ReadFile(path) return string(b), err } func cmdTokens(args []string) int { fs := newCommand("tokens", "gasm tokens ", ` Print the lexical token stream of FILE: position, token kind and text, one token per line. FILE may be "-" to read standard input. `) fs.Parse(args) if fs.NArg() != 1 { fmt.Fprintln(os.Stderr, "usage: gasm tokens ") return 2 } src, err := readSource(fs.Arg(0)) if err != nil { fmt.Fprintln(os.Stderr, "gasm:", err) return 1 } for _, tok := range lexer.Tokenize(src) { fmt.Printf("%s\t%s\t%q\n", tok.Pos, tok.Kind, tok.Text) } return 0 } func cmdParse(args []string) int { fs := newCommand("parse", "gasm parse ", ` Parse FILE and report syntax errors on stderr. On success, print how many declarations and TEXT functions the file contains. FILE may be "-" to read standard input. `) fs.Parse(args) if fs.NArg() != 1 { fmt.Fprintln(os.Stderr, "usage: gasm parse ") return 2 } path := fs.Arg(0) src, err := readSource(path) if err != nil { fmt.Fprintln(os.Stderr, "gasm:", err) return 1 } file, errs := parser.Parse(path, src) for _, e := range errs { fmt.Fprintf(os.Stderr, "%s: %v\n", path, e) } if len(errs) > 0 { return 1 } funcs := 0 for _, d := range file.Decls { if _, ok := d.(*ast.Text); ok { funcs++ } } fmt.Printf("%s: OK — %d declarations, %d functions\n", path, len(file.Decls), funcs) return 0 } func cmdFmt(args []string) int { fs := newCommand("fmt", "gasm fmt [-w] [path...]", ` Canonicalise the formatting of Plan 9 assembly sources: indentation, operand spacing, per-function mnemonic alignment and blank-line layout (exactly one blank line before each label, TEXT and GLOBL block). Formatting is idempotent and preserves every line, comments included. With no paths — or a directory path — every .s file below it is reformatted in place and the changed files are listed, the way go fmt does; "." and "_" directories are skipped. Explicit file paths print to stdout unless -w is given. `) write := fs.Bool("w", false, "write result to the source file") fs.Parse(args) // Like go fmt: with no arguments, or with a directory argument, every .s // file below the directory is formatted in place and the names of the // changed files are listed; explicit file arguments keep the -w / stdout // behaviour. paths := fs.Args() dirMode := len(paths) == 0 if dirMode { paths = []string{"."} } var files []string for _, p := range paths { info, err := os.Stat(p) if err != nil { fmt.Fprintln(os.Stderr, "gasm:", err) return 1 } if info.IsDir() { dirMode = true found, err := asmFiles(p) if err != nil { fmt.Fprintln(os.Stderr, "gasm:", err) return 1 } files = append(files, found...) continue } files = append(files, p) } rc := 0 for _, path := range files { src, err := readSource(path) if err != nil { fmt.Fprintln(os.Stderr, "gasm:", err) rc = 1 continue } out := format.Source(src) if dirMode || *write { if out != src { if err := os.WriteFile(path, []byte(out), 0o644); err != nil { fmt.Fprintln(os.Stderr, "gasm:", err) rc = 1 continue } if dirMode { fmt.Println(path) } } continue } fmt.Print(out) } return rc } // asmFiles collects the .s files below dir, skipping directories whose name // starts with "." or "_" — as the go tooling does, which keeps .git and // scratch or reference trees (e.g. _refs) untouched. func asmFiles(dir string) ([]string, error) { var out []string err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { if path != dir && (strings.HasPrefix(d.Name(), ".") || strings.HasPrefix(d.Name(), "_")) { return filepath.SkipDir } return nil } if strings.HasSuffix(d.Name(), ".s") { out = append(out, path) } return nil }) return out, err } func cmdLint(args []string) int { fs := newCommand("lint", "gasm lint ", ` Run the static checks over the given files and print diagnostics as "file:line:col: severity: message [code]". The exit status is non-zero when an error-severity diagnostic is found; warnings (e.g. the register-clobber audit) do not affect it. Rules include unknown-instruction, operand-count, undefined-label, duplicate-label, missing-ret, missing-textflag-include, abi-argsize, unreachable-code, register-clobber and funcdata-pcdata. `) disable := fs.String("disable", "", "comma-separated rule codes to disable") fs.Parse(args) if fs.NArg() == 0 { fmt.Fprintln(os.Stderr, "usage: gasm lint ") return 2 } disabled := map[string]bool{} for code := range strings.SplitSeq(*disable, ",") { if code = strings.TrimSpace(code); code != "" { disabled[code] = true } } hadError := false for _, path := range fs.Args() { src, err := readSource(path) if err != nil { fmt.Fprintln(os.Stderr, "gasm:", err) hadError = true continue } file, errs := parser.Parse(path, src) for _, e := range errs { fmt.Fprintf(os.Stderr, "%s: %v\n", path, e) hadError = true } diags := lint.File(file, lint.Config{Arch: arch.FromFilename(path), Disable: disabled}) for _, d := range diags { fmt.Printf("%s:%d:%d: %s: %s [%s]\n", path, d.Pos.Line, d.Pos.Column, d.Severity, d.Message, d.Code) if d.Severity == lint.Error { hadError = true } } } if hadError { return 1 } return 0 } func cmdLSP(args []string) int { fs := newCommand("lsp", "gasm lsp", ` Run the language server over standard input/output: JSON-RPC 2.0 with Content-Length framing. Point an LSP-capable editor at the binary and associate it with .s files; the target architecture is inferred from the file suffix (_amd64.s, _arm64.s, _riscv64.s, _loong64.s). Provides completion, hover, document symbols, diagnostics and semantic-token highlighting. `) fs.Parse(args) srv := lsp.New(os.Stdin, os.Stdout) if err := srv.Run(); err != nil { fmt.Fprintln(os.Stderr, "gasm lsp:", err) return 1 } return 0 } func cmdAsm(args []string) int { fs := newCommand("asm", "gasm asm [--format raw|elf|goobj] [-p pkg] [-o out] ", ` Assemble FILE without the Go toolchain: every TEXT function is encoded to machine code and printed as a hex dump. Supported architectures: amd64 (including VEX/AVX2 and EVEX/AVX-512), arm64 (AArch64 integer, FP, conditional select, CRC32, and MOV pseudo), riscv64 (RV64IMAFDC + RVC) and loong64 (LoongArch base ISA). With -o the output is written to a file instead. The --format flag selects what is written: raw (the default) concatenates the functions and the data section into one self-consistent image; elf emits a relocatable object (.text/.data sections, a symbol table and one PC32 relocation per static-symbol reference) that links with the system toolchain; goobj emits the Go toolchain's own object format, which cmd/link consumes directly (it requires -p, the package path, and the installed Go toolchain). `) out := fs.String("o", "", "write the output to this file") format := fs.String("format", "raw", "output format: raw (concatenated image), elf or goobj (Go object)") pkg := fs.String("p", "", "package path for --format goobj (qualifies the exported symbols)") fs.Parse(args) if fs.NArg() != 1 { fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|goobj] [-p pkg] [-o out] ") return 2 } path := fs.Arg(0) targetArch := arch.FromFilename(path) src, err := readSource(path) if err != nil { fmt.Fprintln(os.Stderr, "gasm:", err) return 1 } f, errs := parser.Parse(path, src) for _, e := range errs { fmt.Fprintf(os.Stderr, "%s: %v\n", path, e) } if len(errs) > 0 { return 1 } img, err := assembleFile(targetArch, f) if err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", path, err) return 1 } if len(img.Funcs) == 0 { fmt.Fprintln(os.Stderr, "gasm asm: no assemblable TEXT functions found") return 1 } for _, fn := range img.Funcs { code := img.Code[fn.Offset : fn.Offset+fn.Size] fmt.Printf("%s: %d bytes\n", fn.Name, fn.Size) for i := 0; i < len(code); i += 16 { end := min(i+16, len(code)) fmt.Printf(" %04x:", i) for _, b := range code[i:end] { fmt.Printf(" %02x", b) } fmt.Println() } } if len(img.Data) > 0 { fmt.Printf("data: %d bytes at 0x%x\n", len(img.Data), len(img.Code)) for _, d := range f.Decls { g, ok := d.(*ast.Globl) if !ok || g.Name == nil || g.Name.Pseudo != "SB" { continue } size := 0 if g.Size != nil && g.Size.Imm.HasVal { size = int(g.Size.Imm.Val) } fmt.Printf(" %s: %d bytes at 0x%x\n", g.Name.Name, size, img.Symbols[g.Name.Name]) } for i := 0; i < len(img.Data); i += 16 { end := min(i+16, len(img.Data)) fmt.Printf(" %04x:", len(img.Code)+i) for _, b := range img.Data[i:end] { fmt.Printf(" %02x", b) } fmt.Println() } } if *out != "" { var obj []byte var err error var kind string switch *format { case "raw": if len(img.Externals) > 0 { fmt.Fprintf(os.Stderr, "gasm asm: external symbol %q needs an object file (use --format elf)\n", img.Externals[0]) return 1 } obj, kind = img.Bytes(), "raw image" case "elf": switch targetArch { case arch.RISCV: obj, err = img.ELFRISCVObject() case arch.LOONG64: obj, err = img.ELFLOONG64Object() case arch.ARM64: obj, err = img.ELFAARCH64Object() default: obj, err = img.ELFObject() } kind = "ELF object" case "goobj": switch targetArch { case arch.RISCV: obj, err = img.GOObjectRISCV(*pkg, path) case arch.LOONG64: obj, err = img.GOObjectLOONG64(*pkg, path) case arch.ARM64: obj, err = img.GOObjectAARCH64(*pkg, path) default: obj, err = img.GOObject(*pkg, path) } kind = "Go object" default: fmt.Fprintf(os.Stderr, "gasm asm: unknown format %q (want raw, elf or goobj)\n", *format) return 2 } if err != nil { fmt.Fprintln(os.Stderr, "gasm asm:", err) return 1 } if err := os.WriteFile(*out, obj, 0o644); err != nil { fmt.Fprintln(os.Stderr, "gasm asm:", err) return 1 } fmt.Printf("wrote %d bytes to %s (%s)\n", len(obj), *out, kind) } return 0 } // cmdDiff compares the machine code of two assembly files. func cmdDiff(args []string) int { set := newCommand("diff", "gasm diff ", ` Compare the machine code produced by assembling two files. Shows which functions differ and the byte-level differences. Useful for verifying that two implementations produce identical code, or for tracking encoding changes between Go assembler versions. Use --map to compare functions whose names differ between the files, e.g. --map wideCopyAVX2=wideCopyAVX512 pairs the two regardless of suffix. `) mapSpec := set.String("map", "", "comma-separated old=new pairs to match functions with different names") set.Parse(args) if set.NArg() != 2 { fmt.Fprintln(os.Stderr, "usage: gasm diff ") return 2 } path1, path2 := set.Arg(0), set.Arg(1) // Parse the name mapping (file1 name → file2 name). nameMap := make(map[string]string) if *mapSpec != "" { for pair := range strings.SplitSeq(*mapSpec, ",") { old, new, ok := strings.Cut(pair, "=") if !ok || old == "" || new == "" { fmt.Fprintf(os.Stderr, "gasm diff: invalid --map pair %q (expected old=new)\n", pair) return 2 } nameMap[old] = new } } // Assemble both files. img1, err := assemblePath(path1) if err != nil { fmt.Fprintf(os.Stderr, "gasm diff: %s: %v\n", path1, err) return 1 } img2, err := assemblePath(path2) if err != nil { fmt.Fprintf(os.Stderr, "gasm diff: %s: %v\n", path2, err) return 1 } // Compare functions by name, honouring the --map overrides. funcs1 := make(map[string][]byte) for _, fn := range img1.Funcs { funcs1[fn.Name] = img1.Code[fn.Offset : fn.Offset+fn.Size] } funcs2 := make(map[string][]byte) for _, fn := range img2.Funcs { funcs2[fn.Name] = img2.Code[fn.Offset : fn.Offset+fn.Size] } // Track which file2 functions were consumed (by direct match or via --map) // so the "only in file2" pass skips them. matched2 := make(map[string]bool) diffs := 0 for name, code1 := range funcs1 { target := name if mapped, ok := nameMap[name]; ok { target = mapped } code2, ok := funcs2[target] if !ok { fmt.Printf("%s: only in %s\n", name, path1) diffs++ continue } matched2[target] = true label := name if target != name { label = name + " → " + target } if !bytes.Equal(code1, code2) { fmt.Printf("%s: DIFFERS (%d vs %d bytes)\n", label, len(code1), len(code2)) printByteDiff(code1, code2) diffs++ } else { fmt.Printf("%s: identical (%d bytes)\n", label, len(code1)) } } for name := range funcs2 { if !matched2[name] { fmt.Printf("%s: only in %s\n", name, path2) diffs++ } } if diffs == 0 { fmt.Println("all functions identical") return 0 } return 1 } // assembleFile assembles a parsed file for the given architecture and returns the image. func assembleFile(targetArch arch.Arch, f *ast.File) (*asm.Image, error) { switch targetArch { case arch.AMD64: return asm.AssembleFile(f) case arch.RISCV: return asm.AssembleFileRISCV(f) case arch.ARM64: return asm.AssembleFileARM64(f) case arch.LOONG64: return asm.AssembleFileLOONG64(f) default: return nil, fmt.Errorf("unsupported architecture %q", targetArch) } } // assemblePath reads, parses and assembles a file (used by cmdDiff). func assemblePath(path string) (*asm.Image, error) { src, err := readSource(path) if err != nil { return nil, err } f, errs := parser.Parse(path, src) for _, e := range errs { fmt.Fprintf(os.Stderr, "%s: %v\n", path, e) } if len(errs) > 0 { return nil, fmt.Errorf("parse errors") } return assembleFile(arch.FromFilename(path), f) } // printByteDiff shows the first few byte differences between two code blocks. func printByteDiff(a, b []byte) { maxLen := min(len(b), len(a)) shown := 0 for i := 0; i < maxLen && shown < 8; i++ { if a[i] != b[i] { fmt.Printf(" offset %#04x: %02x vs %02x\n", i, a[i], b[i]) shown++ } } if len(a) != len(b) { fmt.Printf(" length: %d vs %d\n", len(a), len(b)) } } // cmdProfile shows the basic-block structure of functions in an assembly file. func cmdProfile(args []string) int { flagSet := newCommand("profile", "gasm profile ", ` Show the basic-block structure of functions in an assembly file. Lists each function's labels, their offsets, and the block boundaries. This is the static structure; for runtime execution counts, use gasm verify --fuzz which exercises the code paths. `) flagSet.Parse(args) if flagSet.NArg() != 1 { fmt.Fprintln(os.Stderr, "usage: gasm profile ") return 2 } path := flagSet.Arg(0) // Load the file to get function metadata. k, err := verify.Load(path) if err != nil { fmt.Fprintf(os.Stderr, "gasm profile: %v\n", err) return 1 } defer k.Close() for _, name := range k.FuncNames() { fl, err := k.Func(name) if err != nil { continue } fmt.Printf("%s: %d bytes, args=%d, frame=%d", name, fl.Size, fl.Args, fl.Frame) if fl.NoSplit { fmt.Printf(" NOSPLIT") } fmt.Println() // Show labels and their offsets. if len(fl.Labels) > 0 { fmt.Println(" labels:") // Sort labels by offset. type labelOff struct { name string off int } var labels []labelOff for name, off := range fl.Labels { labels = append(labels, labelOff{name, off}) } sort.Slice(labels, func(i, j int) bool { return labels[i].off < labels[j].off }) for _, l := range labels { fmt.Printf(" %-20s +%#04x\n", l.name, l.off) } } // Show basic blocks. blocks, err := k.Blocks(name) if err == nil && len(blocks) > 0 { fmt.Printf(" basic blocks: %d\n", len(blocks)) } } return 0 } // cmdVerifyRISCV handles the verify subcommand for RISC-V files. // JIT requires RISC-V hardware; only ground-truth and profile are available. func cmdVerifyRISCV(path string, groundTruth, profile bool) int { src, err := readSource(path) if err != nil { fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err) return 1 } f, errs := parser.Parse(path, src) for _, e := range errs { fmt.Fprintf(os.Stderr, "%s: %v\n", path, e) } if len(errs) > 0 { return 1 } img, err := asm.AssembleFileRISCV(f) if err != nil { fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err) return 1 } if groundTruth { gt, err := verify.GroundTruthRISCV(path) if err != nil { fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err) return 1 } matched, total := 0, 0 for _, fn := range img.Funcs { gasmCode := img.Code[fn.Offset : fn.Offset+fn.Size] goCode, ok := gt[fn.Name] if !ok { fmt.Printf(" %s: SKIP (not in go tool asm output)\n", fn.Name) continue } total++ gasmCmp := make([]byte, len(gasmCode)) goCmp := make([]byte, len(goCode)) copy(gasmCmp, gasmCode) copy(goCmp, goCode) for _, r := range fn.Relocs { for j := r.Off; j < r.Off+4 && j < len(gasmCmp); j++ { gasmCmp[j] = 0 } for j := r.Off; j < r.Off+4 && j < len(goCmp); j++ { goCmp[j] = 0 } } if bytes.Equal(gasmCmp, goCmp) { matched++ if len(fn.Relocs) > 0 { fmt.Printf(" %s: MATCH (%d bytes, %d relocs masked)\n", fn.Name, fn.Size, len(fn.Relocs)) } else { fmt.Printf(" %s: MATCH (%d bytes)\n", fn.Name, fn.Size) } } else { fmt.Printf(" %s: MISMATCH (%d vs %d bytes)\n", fn.Name, fn.Size, len(goCode)) for i := 0; i < len(gasmCode) || i < len(goCode); i += 16 { var gb, gs string for j := i; j < i+16 && j < len(gasmCode); j++ { gb += fmt.Sprintf(" %02x", gasmCode[j]) } for j := i; j < i+16 && j < len(goCode); j++ { gs += fmt.Sprintf(" %02x", goCode[j]) } fmt.Printf(" %04x: gasm:%s\n", i, gb) fmt.Printf(" %04x: gt: %s\n", i, gs) } } } fmt.Printf("%s: %d/%d matched\n", path, matched, total) if matched < total { return 1 } return 0 } if profile { for _, fn := range img.Funcs { fmt.Printf("%s: %d bytes, labels: %v\n", fn.Name, fn.Size, fn.Labels) } return 0 } fmt.Printf("%s: %d functions assembled\n", path, len(img.Funcs)) for _, fn := range img.Funcs { fmt.Printf(" %s: %d bytes\n", fn.Name, fn.Size) } return 0 } // cmdVerifyLOONG64 verifies a loong64 source file against `go tool asm` // (GOARCH=loong64) — the ground-truth oracle — since gasm cannot JIT-load // LoongArch code on an amd64 host. Relocation sites are masked before the // byte comparison, as the toolchain leaves them zero for the linker. func cmdVerifyLOONG64(path string, groundTruth, profile bool) int { src, err := readSource(path) if err != nil { fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err) return 1 } f, errs := parser.Parse(path, src) for _, e := range errs { fmt.Fprintf(os.Stderr, "%s: %v\n", path, e) } if len(errs) > 0 { return 1 } img, err := asm.AssembleFileLOONG64(f) if err != nil { fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err) return 1 } if groundTruth { gt, err := verify.GroundTruthLOONG64(path) if err != nil { fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err) return 1 } matched, total := 0, 0 for _, fn := range img.Funcs { gasmCode := img.Code[fn.Offset : fn.Offset+fn.Size] goCode, ok := gt[fn.Name] if !ok { fmt.Printf(" %s: SKIP (not in go tool asm output)\n", fn.Name) continue } total++ gasmCmp := make([]byte, len(gasmCode)) goCmp := make([]byte, len(goCode)) copy(gasmCmp, gasmCode) copy(goCmp, goCode) for _, r := range fn.Relocs { for j := r.Off; j < r.Off+4 && j < len(gasmCmp); j++ { gasmCmp[j] = 0 } for j := r.Off; j < r.Off+4 && j < len(goCmp); j++ { goCmp[j] = 0 } } if bytes.Equal(gasmCmp, goCmp) { matched++ if len(fn.Relocs) > 0 { fmt.Printf(" %s: MATCH (%d bytes, %d relocs masked)\n", fn.Name, fn.Size, len(fn.Relocs)) } else { fmt.Printf(" %s: MATCH (%d bytes)\n", fn.Name, fn.Size) } } else { fmt.Printf(" %s: MISMATCH (%d vs %d bytes)\n", fn.Name, fn.Size, len(goCode)) for i := 0; i < len(gasmCode) || i < len(goCode); i += 16 { var gb, gs string for j := i; j < i+16 && j < len(gasmCode); j++ { gb += fmt.Sprintf(" %02x", gasmCode[j]) } for j := i; j < i+16 && j < len(goCode); j++ { gs += fmt.Sprintf(" %02x", goCode[j]) } fmt.Printf(" %04x: gasm:%s\n", i, gb) fmt.Printf(" %04x: gt: %s\n", i, gs) } } } fmt.Printf("%s: %d/%d matched\n", path, matched, total) if matched < total { return 1 } return 0 } if profile { for _, fn := range img.Funcs { fmt.Printf("%s: %d bytes, labels: %v\n", fn.Name, fn.Size, fn.Labels) } return 0 } fmt.Printf("%s: %d functions assembled\n", path, len(img.Funcs)) for _, fn := range img.Funcs { fmt.Printf(" %s: %d bytes\n", fn.Name, fn.Size) } return 0 } func cmdVerifyARM64(path string, groundTruth, profile bool) int { src, err := readSource(path) if err != nil { fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err) return 1 } f, errs := parser.Parse(path, src) for _, e := range errs { fmt.Fprintf(os.Stderr, "%s: %v\n", path, e) } if len(errs) > 0 { return 1 } img, err := asm.AssembleFileARM64(f) if err != nil { fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err) return 1 } if groundTruth { gt, err := verify.GroundTruthARM64(path) if err != nil { fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err) return 1 } matched, total := 0, 0 for _, fn := range img.Funcs { gasmCode := img.Code[fn.Offset : fn.Offset+fn.Size] goCode, ok := gt[fn.Name] if !ok { fmt.Printf(" %s: SKIP (not in go tool asm output)\n", fn.Name) continue } total++ gasmCmp := make([]byte, len(gasmCode)) goCmp := make([]byte, len(goCode)) copy(gasmCmp, gasmCode) copy(goCmp, goCode) for _, r := range fn.Relocs { for j := r.Off; j < r.Off+4 && j < len(gasmCmp); j++ { gasmCmp[j] = 0 } for j := r.Off; j < r.Off+4 && j < len(goCmp); j++ { goCmp[j] = 0 } } if bytes.Equal(gasmCmp, goCmp) { matched++ if len(fn.Relocs) > 0 { fmt.Printf(" %s: MATCH (%d bytes, %d relocs masked)\n", fn.Name, fn.Size, len(fn.Relocs)) } else { fmt.Printf(" %s: MATCH (%d bytes)\n", fn.Name, fn.Size) } } else { fmt.Printf(" %s: MISMATCH (%d vs %d bytes)\n", fn.Name, fn.Size, len(goCode)) for i := 0; i < len(gasmCode) || i < len(goCode); i += 16 { var gb, gs string for j := i; j < i+16 && j < len(gasmCode); j++ { gb += fmt.Sprintf(" %02x", gasmCode[j]) } for j := i; j < i+16 && j < len(goCode); j++ { gs += fmt.Sprintf(" %02x", goCode[j]) } fmt.Printf(" %04x: gasm:%s\n", i, gb) fmt.Printf(" %04x: gt: %s\n", i, gs) } } } fmt.Printf("%s: %d/%d matched\n", path, matched, total) if matched < total { return 1 } return 0 } if profile { for _, fn := range img.Funcs { fmt.Printf("%s: %d bytes, labels: %v\n", fn.Name, fn.Size, fn.Labels) } return 0 } fmt.Printf("%s: %d functions assembled\n", path, len(img.Funcs)) for _, fn := range img.Funcs { fmt.Printf(" %s: %d bytes\n", fn.Name, fn.Size) } return 0 } func cmdVerify(args []string) int { set := newCommand("verify", "gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] ", ` 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 testing. With -smoke, each NOSPLIT function is called with a zeroed argument block to confirm the JIT trampoline works end-to-end. This is safe only for functions 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. With -save-corpus (and -fuzz), every input that crashes or mismatches is written to the directory as replayable JSON. -replay re-runs saved entries against the kernel, one child process per entry, so an input that crashed the original run crashes only the child: the report says whether each entry reproduces. `) smoke := set.Bool("smoke", false, "call each NOSPLIT function with zeroed args") abi := set.Bool("abi", false, "run ABI-checking calls (sentinel registers + red zone)") abiN := set.Int("abi-n", 100, "number of ABI check iterations with varied inputs") profile := set.Bool("profile", false, "list basic-block structure per function") groundTruth := set.Bool("ground-truth", false, "compare machine code byte-for-byte against go tool asm") fuzz := set.Bool("fuzz", false, "differential fuzz: JIT both gasm and go-tool-asm versions, compare outputs") fuzzN := set.Int("n", 1000, "number of fuzz iterations per function") call := set.String("call", "", "call a single function with -buf instead of the sweeps") bufSpec := set.String("buf", "", "buffer spec for -call: name:size:pattern[,name:size:pattern] (zero, ones, seq, or hex)") scalarSpec := set.String("args", "", "scalar args for -call: name=value[,name=value] (decimal or 0x hex)") repeat := set.Int("repeat", 1, "number of times to repeat a -call invocation") saveCorpus := set.String("save-corpus", "", "with -fuzz: write each failing input to this directory as replayable JSON") replay := set.String("replay", "", "replay saved corpus entries (JSON files in this directory) against the kernel") set.Parse(args) if set.NArg() != 1 { fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] ") return 2 } path := set.Arg(0) targetArch := arch.FromFilename(path) // JIT execution is enabled for amd64 kernels on amd64 hosts. The // non-amd64 execution trampolines are implemented but not yet // runtime-hardened, so other kernels take the toolchain-comparison // path, which needs no execution. if targetArch != arch.AMD64 || hostArch() != arch.AMD64 { switch targetArch { case arch.RISCV: // RISC-V: ground-truth only (no JIT on non-RISC-V hosts). return cmdVerifyRISCV(path, *groundTruth, *profile) case arch.LOONG64: // LoongArch: ground-truth only (no JIT on non-LoongArch hosts). return cmdVerifyLOONG64(path, *groundTruth, *profile) case arch.ARM64: // AArch64: ground-truth only (no JIT on non-ARM64 hosts). return cmdVerifyARM64(path, *groundTruth, *profile) case arch.AMD64: fmt.Fprintln(os.Stderr, "gasm verify: JIT-based checks need an amd64 host; use --ground-truth here") return 1 default: fmt.Fprintln(os.Stderr, "gasm verify: unsupported architecture") return 1 } } k, err := verify.Load(path) if err != nil { fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err) return 1 } defer k.Close() names := k.FuncNames() 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, *scalarSpec, *repeat) } // Corpus replay: re-run every saved entry in its own child process, so // an input that crashed the original run crashes only the child. if rp := os.Getenv("GASM_VERIFY_REPLAY_ONE"); rp != "" { return cmdReplayOne(k, rp) } if *replay != "" { return cmdVerifyReplay(path, *replay) } // Subprocess mode: fuzz a single function and exit. The parent selects // the function through the environment, so no internal flag leaks into // the -h output. fuzzOne := os.Getenv("GASM_VERIFY_FUZZ_ONE") if fuzzOne != "" { gt, err := verify.GroundTruth(path) if err != nil { fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err) return 1 } src, err := readSource(path) if err != nil { fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err) return 1 } sigs := verify.ExtractSignatures(src) sig, ok := sigs[fuzzOne] if !ok { fmt.Printf("%s: no signature\n", fuzzOne) return 0 } goCode, ok := gt[fuzzOne] if !ok { fmt.Printf("%s: not in go tool asm\n", fuzzOne) return 0 } var onSave func(verify.CorpusEntry) if *saveCorpus != "" { if err := os.MkdirAll(*saveCorpus, 0o755); err != nil { fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err) return 1 } saved := 0 onSave = func(e verify.CorpusEntry) { file := filepath.Join(*saveCorpus, fmt.Sprintf("%s@%d.json", sanitize(e.Func), saved)) saved++ data, err := json.MarshalIndent(e, "", " ") if err != nil { return } _ = os.WriteFile(file, data, 0o644) } } res := k.FuzzFuncHook(fuzzOne, sig, goCode, *fuzzN, 42, onSave) fmt.Printf("%s\n", res) if !res.OK() { return 1 } return 0 } // Subprocess mode: run the smoke/abi checks for a single function and // exit with the accumulated status. The parent interprets a clean exit // as success, a non-zero exit as failure and death-by-signal as a crash. // Like the fuzz mode, the parent selects the function through the // environment instead of an internal flag. if sweepOne := os.Getenv("GASM_VERIFY_SWEEP_ONE"); sweepOne != "" { fl, err := k.Func(sweepOne) if err != nil || !fl.NoSplit { fmt.Fprintf(os.Stderr, "gasm verify: %s: %v\n", sweepOne, err) return 1 } msgs, failed := runSweepChecks(k, path, sweepOne, fl, *smoke, *abi, *abiN) for _, m := range msgs { fmt.Println(m) } if failed { return 1 } return 0 } // Ground-truth comparison: assemble with go tool asm and compare bytes. if *groundTruth { gt, err := verify.GroundTruth(path) if err != nil { fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err) return 1 } matched, total := 0, 0 for _, name := range names { fl, _ := k.Func(name) gasmCode := k.Image().Code[fl.Offset : fl.Offset+fl.Size] goCode, ok := gt[name] if !ok { fmt.Printf(" %s: SKIP (not in go tool asm output)\n", name) continue } total++ // Compare, masking relocation sites (disp32 fields that the // Go linker fills at link time — gasm resolves them internally). gasmCmp := make([]byte, len(gasmCode)) goCmp := make([]byte, len(goCode)) copy(gasmCmp, gasmCode) copy(goCmp, goCode) for _, r := range fl.Relocs { for j := r.Off; j < r.Off+4 && j < len(gasmCmp); j++ { gasmCmp[j] = 0 } for j := r.Off; j < r.Off+4 && j < len(goCmp); j++ { goCmp[j] = 0 } } if bytes.Equal(gasmCmp, goCmp) { matched++ if len(fl.Relocs) > 0 { fmt.Printf(" %s: MATCH (%d bytes, %d relocs masked)\n", name, fl.Size, len(fl.Relocs)) } else { fmt.Printf(" %s: MATCH (%d bytes)\n", name, fl.Size) } } else { fmt.Printf(" %s: MISMATCH (gasm %d bytes, go %d bytes)\n", name, fl.Size, len(goCode)) for i := 0; i < len(gasmCmp) && i < len(goCmp); i++ { if gasmCmp[i] != goCmp[i] { fmt.Printf(" first diff at byte %d: gasm=%02x go=%02x\n", i, gasmCmp[i], goCmp[i]) break } } rc = 1 } } fmt.Printf("ground truth: %d/%d functions byte-identical\n", matched, total) if matched < total { rc = 1 } } // Differential fuzz: JIT both gasm and go-tool-asm, compare outputs. // Each function runs in a subprocess so a crash (partial functions like // decoders that fault on malformed input) doesn't kill the whole run. if *fuzz { gt, err := verify.GroundTruth(path) if err != nil { fmt.Fprintf(os.Stderr, "gasm verify: fuzz: %v\n", err) return 1 } src, err := readSource(path) if err != nil { fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err) return 1 } sigs := verify.ExtractSignatures(src) fuzzed := 0 for _, name := range names { sig, ok := sigs[name] if !ok { fmt.Printf(" %s: SKIP (no // func signature)\n", name) continue } goCode, ok := gt[name] if !ok { fmt.Printf(" %s: SKIP (not in go tool asm output)\n", name) continue } // Run in a subprocess: if the function crashes on random // input (partial function), we report it and move on. var extra []string if *saveCorpus != "" { extra = append(extra, "-save-corpus", *saveCorpus) } res := fuzzInSubprocess(path, name, *fuzzN, extra...) if res != "" { fmt.Printf(" %s\n", res) if strings.Contains(res, "MISMATCH") { rc = 1 } } _ = sig _ = goCode fuzzed++ } fmt.Printf("fuzz: %d functions tested, %d iterations each\n", fuzzed, *fuzzN) } // Print function info and profile. for _, name := range names { fl, _ := k.Func(name) flags := "" if fl.NoSplit { flags = " NOSPLIT" } fmt.Printf(" %s: %d bytes, args=%d, frame=%d%s\n", name, fl.Size, fl.Args, fl.Frame, flags) if *profile { blocks, err := k.Blocks(name) if err != nil { fmt.Printf(" profile: %v\n", err) } else { fmt.Printf(" blocks: %d\n", len(blocks)) } } } // Run smoke and ABI checks in parallel, each function in its own child // process: the JIT'd code runs with zeroed or fuzzed arguments, and a // function that dereferences them faults — the crash is reported as a // CRASH line instead of killing this process (mirrors fuzzInSubprocess). if *smoke || *abi { type checkResult struct { name string msg string fail bool } var mu sync.Mutex var results []checkResult sem := make(chan struct{}, runtime.NumCPU()) var wg sync.WaitGroup for _, name := range names { fl, _ := k.Func(name) if !fl.NoSplit { continue } wg.Add(1) go func(name string) { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() msg, fail := sweepInSubprocess(path, name, *smoke, *abi, *abiN) mu.Lock() results = append(results, checkResult{name: name, msg: msg, fail: fail}) mu.Unlock() }(name) } wg.Wait() // Print results in source order. resultMap := make(map[string]checkResult, len(results)) for _, r := range results { resultMap[r.name] = r } for _, name := range names { if r, ok := resultMap[name]; ok { fmt.Println(r.msg) if r.fail { rc = 1 } } } } return rc } // fuzzInSubprocess runs the fuzz for a single function in a child process. // cmdVerifyReplay replays every saved corpus entry against the kernel, one // child process per entry so an input that crashed the original run crashes // only the child. Exits non-zero when any entry crashes or fails. func cmdVerifyReplay(path, dir string) int { self, err := os.Executable() if err != nil { fmt.Fprintf(os.Stderr, "gasm verify: cannot find self: %v\n", err) return 1 } files, err := filepath.Glob(filepath.Join(dir, "*.json")) if err != nil { fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err) return 1 } if len(files) == 0 { fmt.Fprintf(os.Stderr, "gasm verify: no corpus entries in %s\n", dir) return 1 } slices.Sort(files) rc := 0 for _, f := range files { cmd := exec.Command(self, "verify", path) cmd.Env = append(os.Environ(), "GASM_VERIFY_REPLAY_ONE="+f) out, err := cmd.CombinedOutput() name := filepath.Base(f) switch { case err == nil: fmt.Printf(" %s: OK\n", name) case replayCrashed(err): rc = 1 fmt.Printf(" %s: CRASH (reproduced)\n", name) default: rc = 1 detail := strings.TrimSpace(string(out)) if detail == "" { detail = err.Error() } fmt.Printf(" %s: FAIL (%s)\n", name, detail) } } return rc } // replayCrashed reports whether a replay child died from a signal, which // means the saved input reproduced its original crash. func replayCrashed(err error) bool { exitErr, ok := err.(*exec.ExitError) if !ok { return false } ws, ok := exitErr.Sys().(syscall.WaitStatus) return ok && ws.Signaled() } // cmdReplayOne is the child half of corpus replay: rebuild one entry and // call it, reporting the outcome on stdout. func cmdReplayOne(k *verify.Kernel, file string) int { data, err := os.ReadFile(file) if err != nil { fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err) return 1 } var e verify.CorpusEntry if err := json.Unmarshal(data, &e); err != nil { fmt.Fprintf(os.Stderr, "gasm verify: %s: %v\n", file, err) return 1 } if _, err := k.ReplayEntry(e.Func, e); err != nil { fmt.Printf("%s: %v\n", e.Func, err) return 1 } return 0 } // If the child is killed by a signal (e.g. SIGSEGV from a partial function // faulting on random input), it returns a CRASH report instead of dying. func fuzzInSubprocess(path, funcName string, n int, extra ...string) string { self, err := os.Executable() if err != nil { return fmt.Sprintf("%s: cannot find self: %v", funcName, err) } fuzzChildArgs := append([]string{"verify", "-n", strconv.Itoa(n)}, extra...) fuzzChildArgs = append(fuzzChildArgs, path) cmd := exec.Command(self, fuzzChildArgs...) cmd.Env = append(os.Environ(), "GASM_VERIFY_FUZZ_ONE="+funcName) out, err := cmd.CombinedOutput() if err != nil { // Check if the child was killed by a signal. if exitErr, ok := err.(*exec.ExitError); ok { ws := exitErr.Sys().(syscall.WaitStatus) if ws.Signaled() { return fmt.Sprintf("%s: CRASH (%v — partial function, use --ground-truth)", funcName, ws.Signal()) } } // Non-zero exit without a signal: the fuzz reported mismatches. lines := strings.SplitSeq(strings.TrimSpace(string(out)), "\n") for l := range lines { if strings.Contains(l, funcName) { return strings.TrimSpace(l) } } return fmt.Sprintf("%s: FAIL (exit %v)", funcName, err) } // Success: extract the result line. lines := strings.SplitSeq(strings.TrimSpace(string(out)), "\n") for l := range lines { if strings.Contains(l, funcName) { return strings.TrimSpace(l) } } return strings.TrimSpace(string(out)) } // sweepInSubprocess runs the smoke/abi checks for a single function in a // child process. If the child is killed by a signal (e.g. SIGSEGV from a // function that dereferences its zeroed or fuzzed arguments), it returns a // CRASH report instead of dying — the same isolation fuzzInSubprocess // provides for the fuzz sweep. func sweepInSubprocess(path, funcName string, smoke, abi bool, abiN int) (string, bool) { self, err := os.Executable() if err != nil { return fmt.Sprintf(" smoke/abi: FAIL — cannot find self: %v", err), true } args := []string{"verify"} if smoke { args = append(args, "-smoke") } if abi { args = append(args, "-abi", "-abi-n", strconv.Itoa(abiN)) } args = append(args, path) cmd := exec.Command(self, args...) cmd.Env = append(os.Environ(), "GASM_VERIFY_SWEEP_ONE="+funcName) out, err := cmd.CombinedOutput() if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { ws, ok := exitErr.Sys().(syscall.WaitStatus) if ok && ws.Signaled() { return fmt.Sprintf(" smoke/abi: CRASH (%v — the function faults on zeroed or fuzzed\n arguments; verify it with -call and valid buffers)", ws.Signal()), true } } // Non-zero exit without a signal: the checks themselves failed and // the child already printed the diagnostic lines. return sweepCheckLines(out), true } return sweepCheckLines(out), false } // sweepCheckLines extracts the check-result lines from child output, // dropping the child's own file/function banners (the parent prints those). func sweepCheckLines(out []byte) string { var lines []string for l := range strings.SplitSeq(string(out), "\n") { t := strings.TrimSpace(l) if strings.HasPrefix(t, "smoke:") || strings.HasPrefix(t, "abi:") { lines = append(lines, " "+t) } } return strings.Join(lines, "\n") } // runSweepChecks performs the in-process smoke and ABI checks for one // function — the child half of sweepInSubprocess. func runSweepChecks(k *verify.Kernel, path, name string, fl asm.FuncLayout, smoke, abi bool, abiN int) ([]string, bool) { var msgs []string failed := false if smoke { args := make([]byte, fl.Args) _, err := k.CallFunc(name, args) if err != nil { msgs = append(msgs, fmt.Sprintf(" smoke: FAIL — %v", err)) failed = true } else { msgs = append(msgs, " smoke: OK") } } if abi { if src, err := readSource(path); err == nil { result := k.FuzzFuncCheckedByName(name, src, abiN, int64(abiN)) if result.Mismatches > 0 { msgs = append(msgs, fmt.Sprintf(" abi: %s", result)) failed = true } else { msgs = append(msgs, fmt.Sprintf(" abi: clean (%d varied inputs)", result.Matches)) } } else { args := make([]byte, fl.Args) _, report, err := k.CallFuncChecked(name, args) if err != nil { msgs = append(msgs, fmt.Sprintf(" abi: FAIL — %v", err)) failed = true } else if !report.OK() { msgs = append(msgs, fmt.Sprintf(" abi: %s", report)) failed = true } else { msgs = append(msgs, " abi: clean") } } } return msgs, failed } // cmdVerifyCall implements `gasm verify --call [--buf spec] [--args spec] [--repeat n]`. // It invokes a single function with user-supplied buffers and scalar // arguments 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, scalarSpec 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) scalars, err := verify.ParseScalarArgs(scalarSpec) if err != nil { fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err) return 1 } if err := verify.ApplyScalarArgs(args, layout, scalars); err != nil { fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err) return 1 } 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) } } if len(scalars) > 0 { names := make([]string, 0, len(scalars)) for n := range scalars { names = append(names, n) } sort.Strings(names) fmt.Printf(" scalars:\n") for _, n := range names { fmt.Printf(" %s = %d\n", n, scalars[n]) } } fmt.Printf(" args before: %s\n", hexDump(args)) rc := 0 for i := range repeat { 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 := min(len(buf), max) var sb strings.Builder for i := range n { 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 "" }