// 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" "flag" "fmt" "io" "io/fs" "os" "os/exec" "path/filepath" "strconv" "strings" "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.28.0" 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 "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, riscv64)"}, {"verify", "JIT-assemble and run dynamic checks (amd64, riscv64)"}, {"debug", "interactive source-level debugger (amd64)"}, {"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 "-". 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(path, 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.Split(*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|macho|goobj] [-p pkg] [-o out] ", ` Assemble FILE (amd64 or riscv64) without the Go toolchain: every TEXT function is encoded to machine code — scalar, VEX/AVX2 and EVEX/AVX-512 instructions, FP/SP frame mapping, local labels and file-local static symbols (GLOBL/DATA) resolved RIP-relative — and printed as a hex dump. 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 and macho emit 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, macho 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|macho|goobj] [-p pkg] [-o out] ") return 2 } path := fs.Arg(0) targetArch := arch.FromFilename(path) if targetArch != arch.AMD64 && targetArch != arch.RISCV { fmt.Fprintln(os.Stderr, "gasm asm: only amd64 and riscv64 are supported") return 1 } 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 } var img *asm.Image if targetArch == arch.RISCV { img, err = asm.AssembleFileRISCV(f) } else { img, err = asm.AssembleFile(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 := i + 16 if end > len(code) { end = 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 := i + 16 if end > len(img.Data) { end = 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 or --format macho)\n", img.Externals[0]) return 1 } obj, kind = img.Bytes(), "raw image" case "elf": if targetArch == arch.RISCV { obj, err = img.ELFRISCVObject() } else { obj, err = img.ELFObject() } kind = "ELF object" case "macho": obj, err = img.MachOObject() kind = "Mach-O object" case "goobj": obj, err = img.GOObject(*pkg, path) kind = "Go object" default: fmt.Fprintf(os.Stderr, "gasm asm: unknown format %q (want raw, elf, macho 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 } // 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 } func cmdVerify(args []string) int { fs := newCommand("verify", "gasm verify [-smoke] [-abi] [-profile] ", ` 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 -profile, the static basic-block structure is listed for each function. `) 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)") profile := fs.Bool("profile", false, "list basic-block structure per function") groundTruth := fs.Bool("ground-truth", false, "compare machine code byte-for-byte against go tool asm") 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) fs.Parse(args) if fs.NArg() != 1 { fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-profile] ") return 2 } path := fs.Arg(0) targetArch := arch.FromFilename(path) if targetArch != arch.AMD64 && targetArch != arch.RISCV { fmt.Fprintln(os.Stderr, "gasm verify: only amd64 and riscv64 are supported") return 1 } // RISC-V: ground-truth only (no JIT on non-RISC-V hosts). if targetArch == arch.RISCV { return cmdVerifyRISCV(path, *groundTruth, *profile) } 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 // Subprocess mode: fuzz a single function and exit. 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 } res := k.FuzzFunc(*fuzzOne, sig, goCode, *fuzzN, 42) fmt.Printf("%s\n", res) if !res.OK() { 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. res := fuzzInSubprocess(path, name, *fuzzN) 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) } 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)) } } if *smoke && fl.NoSplit { args := make([]byte, fl.Args) _, err := k.CallFunc(name, args) if err != nil { fmt.Printf(" smoke: FAIL — %v\n", err) rc = 1 } else { fmt.Printf(" smoke: OK\n") } } if *abi && fl.NoSplit { args := make([]byte, fl.Args) _, report, err := k.CallFuncChecked(name, args) if err != nil { fmt.Printf(" abi: FAIL — %v\n", err) rc = 1 } else if !report.OK() { fmt.Printf(" abi: %s\n", report) rc = 1 } else { fmt.Printf(" abi: clean\n") } } } return rc } // fuzzInSubprocess runs the fuzz for a single function in a child process. // 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) string { self, err := os.Executable() if err != nil { return fmt.Sprintf("%s: cannot find self: %v", funcName, err) } cmd := exec.Command(self, "verify", "--fuzz-one="+funcName, "-n", strconv.Itoa(n), path) 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.Split(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.Split(strings.TrimSpace(string(out)), "\n") for _, l := range lines { if strings.Contains(l, funcName) { return strings.TrimSpace(l) } } return strings.TrimSpace(string(out)) }