// 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" "sort" "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.31.1" 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 "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"}, {"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|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(path, 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 := 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)\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 { fs := 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 := fs.String("map", "", "comma-separated old=new pairs to match functions with different names") fs.Parse(args) if fs.NArg() != 2 { fmt.Fprintln(os.Stderr, "usage: gasm diff ") return 2 } path1, path2 := fs.Arg(0), fs.Arg(1) // Parse the name mapping (file1 name → file2 name). nameMap := make(map[string]string) if *mapSpec != "" { for _, pair := range strings.Split(*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(path string, 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(path, arch.FromFilename(path), f) } // printByteDiff shows the first few byte differences between two code blocks. func printByteDiff(a, b []byte) { maxLen := len(a) if len(b) < maxLen { maxLen = len(b) } 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 { fs := 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. `) fs.Parse(args) if fs.NArg() != 1 { fmt.Fprintln(os.Stderr, "usage: gasm profile ") return 2 } path := fs.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 { fs := 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. `) 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)") abiN := fs.Int("abi-n", 100, "number of ABI check iterations with varied inputs") 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) 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] [-fuzz] [-ground-truth] [-profile] [-call] ") return 2 } path := fs.Arg(0) targetArch := arch.FromFilename(path) switch targetArch { case arch.AMD64: // JIT-based verification below. 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) 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, *repeat) } // 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 { // Try varied-input ABI fuzzing first. if src, err := readSource(path); err == nil { result := k.FuzzFuncCheckedByName(name, src, *abiN, int64(*abiN)) if result.Mismatches > 0 { fmt.Printf(" abi: %s\n", result) rc = 1 } else { fmt.Printf(" abi: clean (%d varied inputs)\n", result.Matches) } } else { // Fallback: single zeroed-arg call. 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)) } // cmdVerifyCall implements `gasm verify --call [--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 "" }