Files
gasm-sdk/cmd/gasm/main.go
T

1436 lines
41 KiB
Go
Raw Normal View History

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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).
2026-08-20 22:30:40 +02:00
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 <command> [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 <command> -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 <file>", `
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 <file>")
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 <file>", `
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 <file>")
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 <file...>", `
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 <file...>")
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] <file>", `
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] <file>")
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:
2026-08-05 09:27:00 +02:00
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:
2026-08-05 09:27:00 +02:00
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 <file1.s> <file2.s>", `
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 <file1.s> <file2.s>")
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 <file.s>", `
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 <file.s>")
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] <file.s>", `
Assemble FILE (amd64), map it into executable memory and report the available
functions. This confirms the assembled image is self-consistent (no
unresolved external symbols) and executable — the prerequisite for dynamic
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] <file.s>")
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 <func> [--buf spec] [--repeat n]`.
// It invokes a single function with user-supplied buffers and prints the arg
// block before and after the call, so the user can inspect return values and
// any output written to the buffers.
func cmdVerifyCall(k *verify.Kernel, path, funcName, bufSpec string, repeat int) int {
fl, err := k.Func(funcName)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1
}
if !fl.NoSplit {
fmt.Fprintf(os.Stderr, "gasm verify: %s is not NOSPLIT (frame=%d); --call supports NOSPLIT functions only\n", funcName, fl.Frame)
return 1
}
// Parse the // func signature to lay out the argument block.
src, err := readSource(path)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1
}
sig, ok := verify.ExtractFuncSig(src, funcName)
if !ok {
fmt.Fprintf(os.Stderr, "gasm verify: no // func signature found for %s\n", funcName)
return 1
}
layout := verify.ArgLayout(sig)
// Allocate the requested buffers (if any) and build the arg block.
specs, err := verify.ParseBufSpec(bufSpec)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1
}
var pool verify.BufPool
if err := pool.Alloc(specs); err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1
}
defer pool.Close()
args := pool.BuildArgs(layout, fl.Args)
fmt.Printf("%s: %d bytes, args=%d\n", funcName, fl.Size, fl.Args)
fmt.Printf(" signature: func %s(%s) %s\n", sig.Name, formatParams(sig.Params), formatResults(sig.Results))
if len(specs) > 0 {
fmt.Printf(" buffers:\n")
for _, s := range specs {
fmt.Printf(" %s: %d bytes, pattern=%s\n", s.Name, s.Size, s.Pattern)
}
}
fmt.Printf(" args before: %s\n", hexDump(args))
rc := 0
for i := 0; i < repeat; i++ {
out, err := k.CallFunc(funcName, args)
if err != nil {
fmt.Printf(" call %d: FAIL — %v\n", i+1, err)
rc = 1
continue
}
if repeat == 1 {
fmt.Printf(" args after: %s\n", hexDump(out))
} else if i == repeat-1 {
fmt.Printf(" args after %d calls: %s\n", repeat, hexDump(out))
}
fmt.Printf(" call %d: OK\n", i+1)
}
return rc
}
// formatParams renders a parameter list as "a []byte, b []byte".
func formatParams(ps []verify.Param) string {
var parts []string
for _, p := range ps {
if p.Name != "" {
parts = append(parts, p.Name+" "+p.Typ)
} else {
parts = append(parts, p.Typ)
}
}
return strings.Join(parts, ", ")
}
// formatResults renders a result list as "(n int, code int)" or "int".
func formatResults(rs []verify.Param) string {
if len(rs) == 0 {
return ""
}
if len(rs) == 1 && rs[0].Name == "" {
return rs[0].Typ
}
return "(" + formatParams(rs) + ")"
}
// hexDump returns a one-line hex dump of buf, truncated to 64 bytes.
func hexDump(buf []byte) string {
const max = 64
n := len(buf)
if n > max {
n = max
}
var sb strings.Builder
for i := 0; i < n; i++ {
if i > 0 {
sb.WriteByte(' ')
}
fmt.Fprintf(&sb, "%02x", buf[i])
}
return fmt.Sprintf("%s%s (%d bytes)", sb.String(), truncMark(len(buf), max), len(buf))
}
// truncMark returns "…" when the buffer is longer than max, else "".
func truncMark(n, max int) string {
if n > max {
return "…"
}
return ""
}