Release / build (amd64, linux) (push) Successful in 43s
Release / build (arm64, linux) (push) Successful in 42s
Release / build (loong64, linux) (push) Successful in 43s
Release / build (riscv64, linux) (push) Successful in 43s
Test / vet (push) Successful in 49s
Release / release (push) Successful in 18s
Test / test (push) Successful in 2m40s
Test / build (push) Successful in 42s
Assisted-by: GLM 5.3 Flash
1731 lines
51 KiB
Go
1731 lines
51 KiB
Go
// 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"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/format"
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/lexer"
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/lint"
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/lsp"
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
|
)
|
|
|
|
// version is the release version, stamped at build time via
|
|
// -ldflags "-X main.version=…" (defaulting to the current release).
|
|
var version = "0.32.0"
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
usage(os.Stderr)
|
|
os.Exit(2)
|
|
}
|
|
switch os.Args[1] {
|
|
case "tokens":
|
|
os.Exit(cmdTokens(os.Args[2:]))
|
|
case "parse":
|
|
os.Exit(cmdParse(os.Args[2:]))
|
|
case "fmt":
|
|
os.Exit(cmdFmt(os.Args[2:]))
|
|
case "lint":
|
|
os.Exit(cmdLint(os.Args[2:]))
|
|
case "asm":
|
|
os.Exit(cmdAsm(os.Args[2:]))
|
|
case "verify":
|
|
os.Exit(cmdVerify(os.Args[2:]))
|
|
case "debug":
|
|
os.Exit(cmdDebug(os.Args[2:]))
|
|
case "diff":
|
|
os.Exit(cmdDiff(os.Args[2:]))
|
|
case "profile":
|
|
os.Exit(cmdProfile(os.Args[2:]))
|
|
case "audit-instructions":
|
|
if err := cmdAuditInstructions(os.Args[2:]); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
case "scaffold":
|
|
if err := cmdScaffold(os.Args[2:]); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
case "lsp":
|
|
os.Exit(cmdLSP(os.Args[2:]))
|
|
case "version", "--version", "-V":
|
|
os.Exit(cmdVersion())
|
|
case "help", "--help", "-h":
|
|
usage(os.Stdout)
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "gasm: unknown command %q — run \"gasm --help\" for usage\n", os.Args[1])
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
// cmdVersion prints the release version.
|
|
func cmdVersion() int {
|
|
fmt.Printf("gasm %s\n", version)
|
|
return 0
|
|
}
|
|
|
|
// ANSI color helpers for terminal output.
|
|
const (
|
|
colorReset = "\033[0m"
|
|
colorBold = "\033[1m"
|
|
colorCyan = "\033[36m"
|
|
colorYellow = "\033[33m"
|
|
colorGray = "\033[90m"
|
|
)
|
|
|
|
// isTTY reports whether the writer is a terminal (for color output).
|
|
func isTTY(w io.Writer) bool {
|
|
if f, ok := w.(*os.File); ok {
|
|
stat, _ := f.Stat()
|
|
return (stat.Mode() & os.ModeCharDevice) != 0
|
|
}
|
|
return false
|
|
}
|
|
|
|
func usage(w io.Writer) {
|
|
useColor := isTTY(w)
|
|
bold, cyan, yellow, gray, reset := "", "", "", "", ""
|
|
if useColor {
|
|
bold, cyan, yellow, gray, reset = colorBold, colorCyan, colorYellow, colorGray, colorReset
|
|
}
|
|
|
|
fmt.Fprintf(w, "%sgasm %s%s — developer tooling for Go's Plan 9 assembler (GAsm)%s\n\n", bold, version, reset, reset)
|
|
fmt.Fprintf(w, "gasm bundles a lexer, parser, formatter, linter, standalone assembler and\n")
|
|
fmt.Fprintf(w, "language server for Plan 9 assembly into one self-contained binary.\n\n")
|
|
|
|
fmt.Fprintf(w, "%sUsage:%s\n", yellow, reset)
|
|
fmt.Fprintf(w, " gasm <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"},
|
|
{"audit-instructions", "diff the encoder against the Go toolchain's name table"},
|
|
{"scaffold", "generate a differential test skeleton for a kernel file"},
|
|
{"lsp", "run the language server over stdio"},
|
|
{"version", "print the version (same as --version)"},
|
|
}
|
|
for _, c := range commands {
|
|
fmt.Fprintf(w, " %s%-10s%s %s%s%s\n", cyan, c.name, reset, gray, c.desc, reset)
|
|
}
|
|
|
|
fmt.Fprintf(w, "\n%sFlags:%s\n", yellow, reset)
|
|
fmt.Fprintf(w, " %s-h, --help%s %sshow this help%s\n", cyan, reset, gray, reset)
|
|
fmt.Fprintf(w, " %s-V, --version%s %sprint the version%s\n", cyan, reset, gray, reset)
|
|
|
|
fmt.Fprintf(w, "\nRun \"gasm <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 "-".
|
|
// hostArch maps the running GOARCH onto the arch package's identifiers.
|
|
// It returns arch.Unknown on hosts the toolkit cannot JIT for.
|
|
func hostArch() arch.Arch {
|
|
switch runtime.GOARCH {
|
|
case "amd64":
|
|
return arch.AMD64
|
|
case "arm64":
|
|
return arch.ARM64
|
|
case "riscv64":
|
|
return arch.RISCV
|
|
case "loong64":
|
|
return arch.LOONG64
|
|
}
|
|
return arch.Unknown
|
|
}
|
|
|
|
func readSource(path string) (string, error) {
|
|
if path == "-" {
|
|
b, err := io.ReadAll(os.Stdin)
|
|
return string(b), err
|
|
}
|
|
b, err := os.ReadFile(path)
|
|
return string(b), err
|
|
}
|
|
|
|
func cmdTokens(args []string) int {
|
|
fs := newCommand("tokens", "gasm tokens <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(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.SplitSeq(*disable, ",") {
|
|
if code = strings.TrimSpace(code); code != "" {
|
|
disabled[code] = true
|
|
}
|
|
}
|
|
hadError := false
|
|
for _, path := range fs.Args() {
|
|
src, err := readSource(path)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "gasm:", err)
|
|
hadError = true
|
|
continue
|
|
}
|
|
file, errs := parser.Parse(path, src)
|
|
for _, e := range errs {
|
|
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
|
|
hadError = true
|
|
}
|
|
diags := lint.File(file, lint.Config{Arch: arch.FromFilename(path), Disable: disabled})
|
|
for _, d := range diags {
|
|
fmt.Printf("%s:%d:%d: %s: %s [%s]\n", path, d.Pos.Line, d.Pos.Column, d.Severity, d.Message, d.Code)
|
|
if d.Severity == lint.Error {
|
|
hadError = true
|
|
}
|
|
}
|
|
}
|
|
if hadError {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func cmdLSP(args []string) int {
|
|
fs := newCommand("lsp", "gasm lsp", `
|
|
Run the language server over standard input/output: JSON-RPC 2.0 with
|
|
Content-Length framing. Point an LSP-capable editor at the binary and
|
|
associate it with .s files; the target architecture is inferred from the file
|
|
suffix (_amd64.s, _arm64.s, _riscv64.s, _loong64.s). Provides completion,
|
|
hover, document symbols, diagnostics and semantic-token highlighting.
|
|
`)
|
|
fs.Parse(args)
|
|
srv := lsp.New(os.Stdin, os.Stdout)
|
|
if err := srv.Run(); err != nil {
|
|
fmt.Fprintln(os.Stderr, "gasm lsp:", err)
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func cmdAsm(args []string) int {
|
|
fs := newCommand("asm", "gasm asm [--format raw|elf|goobj] [-p pkg] [-o out] <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(targetArch, f)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s: %v\n", path, err)
|
|
return 1
|
|
}
|
|
if len(img.Funcs) == 0 {
|
|
fmt.Fprintln(os.Stderr, "gasm asm: no assemblable TEXT functions found")
|
|
return 1
|
|
}
|
|
for _, fn := range img.Funcs {
|
|
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
|
fmt.Printf("%s: %d bytes\n", fn.Name, fn.Size)
|
|
for i := 0; i < len(code); i += 16 {
|
|
end := min(i+16, len(code))
|
|
fmt.Printf(" %04x:", i)
|
|
for _, b := range code[i:end] {
|
|
fmt.Printf(" %02x", b)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
}
|
|
if len(img.Data) > 0 {
|
|
fmt.Printf("data: %d bytes at 0x%x\n", len(img.Data), len(img.Code))
|
|
for _, d := range f.Decls {
|
|
g, ok := d.(*ast.Globl)
|
|
if !ok || g.Name == nil || g.Name.Pseudo != "SB" {
|
|
continue
|
|
}
|
|
size := 0
|
|
if g.Size != nil && g.Size.Imm.HasVal {
|
|
size = int(g.Size.Imm.Val)
|
|
}
|
|
fmt.Printf(" %s: %d bytes at 0x%x\n", g.Name.Name, size, img.Symbols[g.Name.Name])
|
|
}
|
|
for i := 0; i < len(img.Data); i += 16 {
|
|
end := min(i+16, len(img.Data))
|
|
fmt.Printf(" %04x:", len(img.Code)+i)
|
|
for _, b := range img.Data[i:end] {
|
|
fmt.Printf(" %02x", b)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
}
|
|
if *out != "" {
|
|
var obj []byte
|
|
var err error
|
|
var kind string
|
|
switch *format {
|
|
case "raw":
|
|
if len(img.Externals) > 0 {
|
|
fmt.Fprintf(os.Stderr, "gasm asm: external symbol %q needs an object file (use --format elf)\n", img.Externals[0])
|
|
return 1
|
|
}
|
|
obj, kind = img.Bytes(), "raw image"
|
|
case "elf":
|
|
switch targetArch {
|
|
case arch.RISCV:
|
|
obj, err = img.ELFRISCVObject()
|
|
case arch.LOONG64:
|
|
obj, err = img.ELFLOONG64Object()
|
|
case arch.ARM64:
|
|
obj, err = img.ELFAARCH64Object()
|
|
default:
|
|
obj, err = img.ELFObject()
|
|
}
|
|
kind = "ELF object"
|
|
case "goobj":
|
|
switch targetArch {
|
|
case arch.RISCV:
|
|
obj, err = img.GOObjectRISCV(*pkg, path)
|
|
case arch.LOONG64:
|
|
obj, err = img.GOObjectLOONG64(*pkg, path)
|
|
case arch.ARM64:
|
|
obj, err = img.GOObjectAARCH64(*pkg, path)
|
|
default:
|
|
obj, err = img.GOObject(*pkg, path)
|
|
}
|
|
kind = "Go object"
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "gasm asm: unknown format %q (want raw, elf or goobj)\n", *format)
|
|
return 2
|
|
}
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "gasm asm:", err)
|
|
return 1
|
|
}
|
|
if err := os.WriteFile(*out, obj, 0o644); err != nil {
|
|
fmt.Fprintln(os.Stderr, "gasm asm:", err)
|
|
return 1
|
|
}
|
|
fmt.Printf("wrote %d bytes to %s (%s)\n", len(obj), *out, kind)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// cmdDiff compares the machine code of two assembly files.
|
|
func cmdDiff(args []string) int {
|
|
set := newCommand("diff", "gasm diff <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 := set.String("map", "", "comma-separated old=new pairs to match functions with different names")
|
|
set.Parse(args)
|
|
if set.NArg() != 2 {
|
|
fmt.Fprintln(os.Stderr, "usage: gasm diff <file1.s> <file2.s>")
|
|
return 2
|
|
}
|
|
path1, path2 := set.Arg(0), set.Arg(1)
|
|
|
|
// Parse the name mapping (file1 name → file2 name).
|
|
nameMap := make(map[string]string)
|
|
if *mapSpec != "" {
|
|
for pair := range strings.SplitSeq(*mapSpec, ",") {
|
|
old, new, ok := strings.Cut(pair, "=")
|
|
if !ok || old == "" || new == "" {
|
|
fmt.Fprintf(os.Stderr, "gasm diff: invalid --map pair %q (expected old=new)\n", pair)
|
|
return 2
|
|
}
|
|
nameMap[old] = new
|
|
}
|
|
}
|
|
|
|
// Assemble both files.
|
|
img1, err := assemblePath(path1)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm diff: %s: %v\n", path1, err)
|
|
return 1
|
|
}
|
|
img2, err := assemblePath(path2)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm diff: %s: %v\n", path2, err)
|
|
return 1
|
|
}
|
|
|
|
// Compare functions by name, honouring the --map overrides.
|
|
funcs1 := make(map[string][]byte)
|
|
for _, fn := range img1.Funcs {
|
|
funcs1[fn.Name] = img1.Code[fn.Offset : fn.Offset+fn.Size]
|
|
}
|
|
funcs2 := make(map[string][]byte)
|
|
for _, fn := range img2.Funcs {
|
|
funcs2[fn.Name] = img2.Code[fn.Offset : fn.Offset+fn.Size]
|
|
}
|
|
|
|
// Track which file2 functions were consumed (by direct match or via --map)
|
|
// so the "only in file2" pass skips them.
|
|
matched2 := make(map[string]bool)
|
|
|
|
diffs := 0
|
|
for name, code1 := range funcs1 {
|
|
target := name
|
|
if mapped, ok := nameMap[name]; ok {
|
|
target = mapped
|
|
}
|
|
code2, ok := funcs2[target]
|
|
if !ok {
|
|
fmt.Printf("%s: only in %s\n", name, path1)
|
|
diffs++
|
|
continue
|
|
}
|
|
matched2[target] = true
|
|
label := name
|
|
if target != name {
|
|
label = name + " → " + target
|
|
}
|
|
if !bytes.Equal(code1, code2) {
|
|
fmt.Printf("%s: DIFFERS (%d vs %d bytes)\n", label, len(code1), len(code2))
|
|
printByteDiff(code1, code2)
|
|
diffs++
|
|
} else {
|
|
fmt.Printf("%s: identical (%d bytes)\n", label, len(code1))
|
|
}
|
|
}
|
|
for name := range funcs2 {
|
|
if !matched2[name] {
|
|
fmt.Printf("%s: only in %s\n", name, path2)
|
|
diffs++
|
|
}
|
|
}
|
|
|
|
if diffs == 0 {
|
|
fmt.Println("all functions identical")
|
|
return 0
|
|
}
|
|
return 1
|
|
}
|
|
|
|
// assembleFile assembles a parsed file for the given architecture and returns the image.
|
|
func assembleFile(targetArch arch.Arch, f *ast.File) (*asm.Image, error) {
|
|
switch targetArch {
|
|
case arch.AMD64:
|
|
return asm.AssembleFile(f)
|
|
case arch.RISCV:
|
|
return asm.AssembleFileRISCV(f)
|
|
case arch.ARM64:
|
|
return asm.AssembleFileARM64(f)
|
|
case arch.LOONG64:
|
|
return asm.AssembleFileLOONG64(f)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported architecture %q", targetArch)
|
|
}
|
|
}
|
|
|
|
// assemblePath reads, parses and assembles a file (used by cmdDiff).
|
|
func assemblePath(path string) (*asm.Image, error) {
|
|
src, err := readSource(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f, errs := parser.Parse(path, src)
|
|
for _, e := range errs {
|
|
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
|
|
}
|
|
if len(errs) > 0 {
|
|
return nil, fmt.Errorf("parse errors")
|
|
}
|
|
return assembleFile(arch.FromFilename(path), f)
|
|
}
|
|
|
|
// printByteDiff shows the first few byte differences between two code blocks.
|
|
func printByteDiff(a, b []byte) {
|
|
maxLen := min(len(b), len(a))
|
|
shown := 0
|
|
for i := 0; i < maxLen && shown < 8; i++ {
|
|
if a[i] != b[i] {
|
|
fmt.Printf(" offset %#04x: %02x vs %02x\n", i, a[i], b[i])
|
|
shown++
|
|
}
|
|
}
|
|
if len(a) != len(b) {
|
|
fmt.Printf(" length: %d vs %d\n", len(a), len(b))
|
|
}
|
|
}
|
|
|
|
// cmdProfile shows the basic-block structure of functions in an assembly file.
|
|
func cmdProfile(args []string) int {
|
|
flagSet := newCommand("profile", "gasm profile <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.
|
|
`)
|
|
flagSet.Parse(args)
|
|
if flagSet.NArg() != 1 {
|
|
fmt.Fprintln(os.Stderr, "usage: gasm profile <file.s>")
|
|
return 2
|
|
}
|
|
path := flagSet.Arg(0)
|
|
|
|
// Load the file to get function metadata.
|
|
k, err := verify.Load(path)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm profile: %v\n", err)
|
|
return 1
|
|
}
|
|
defer k.Close()
|
|
|
|
for _, name := range k.FuncNames() {
|
|
fl, err := k.Func(name)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
fmt.Printf("%s: %d bytes, args=%d, frame=%d", name, fl.Size, fl.Args, fl.Frame)
|
|
if fl.NoSplit {
|
|
fmt.Printf(" NOSPLIT")
|
|
}
|
|
fmt.Println()
|
|
|
|
// Show labels and their offsets.
|
|
if len(fl.Labels) > 0 {
|
|
fmt.Println(" labels:")
|
|
// Sort labels by offset.
|
|
type labelOff struct {
|
|
name string
|
|
off int
|
|
}
|
|
var labels []labelOff
|
|
for name, off := range fl.Labels {
|
|
labels = append(labels, labelOff{name, off})
|
|
}
|
|
sort.Slice(labels, func(i, j int) bool { return labels[i].off < labels[j].off })
|
|
for _, l := range labels {
|
|
fmt.Printf(" %-20s +%#04x\n", l.name, l.off)
|
|
}
|
|
}
|
|
|
|
// Show basic blocks.
|
|
blocks, err := k.Blocks(name)
|
|
if err == nil && len(blocks) > 0 {
|
|
fmt.Printf(" basic blocks: %d\n", len(blocks))
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// cmdVerifyRISCV handles the verify subcommand for RISC-V files.
|
|
// JIT requires RISC-V hardware; only ground-truth and profile are available.
|
|
func cmdVerifyRISCV(path string, groundTruth, profile bool) int {
|
|
src, err := readSource(path)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
return 1
|
|
}
|
|
f, errs := parser.Parse(path, src)
|
|
for _, e := range errs {
|
|
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
|
|
}
|
|
if len(errs) > 0 {
|
|
return 1
|
|
}
|
|
img, err := asm.AssembleFileRISCV(f)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
return 1
|
|
}
|
|
|
|
if groundTruth {
|
|
gt, err := verify.GroundTruthRISCV(path)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err)
|
|
return 1
|
|
}
|
|
matched, total := 0, 0
|
|
for _, fn := range img.Funcs {
|
|
gasmCode := img.Code[fn.Offset : fn.Offset+fn.Size]
|
|
goCode, ok := gt[fn.Name]
|
|
if !ok {
|
|
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", fn.Name)
|
|
continue
|
|
}
|
|
total++
|
|
gasmCmp := make([]byte, len(gasmCode))
|
|
goCmp := make([]byte, len(goCode))
|
|
copy(gasmCmp, gasmCode)
|
|
copy(goCmp, goCode)
|
|
for _, r := range fn.Relocs {
|
|
for j := r.Off; j < r.Off+4 && j < len(gasmCmp); j++ {
|
|
gasmCmp[j] = 0
|
|
}
|
|
for j := r.Off; j < r.Off+4 && j < len(goCmp); j++ {
|
|
goCmp[j] = 0
|
|
}
|
|
}
|
|
if bytes.Equal(gasmCmp, goCmp) {
|
|
matched++
|
|
if len(fn.Relocs) > 0 {
|
|
fmt.Printf(" %s: MATCH (%d bytes, %d relocs masked)\n", fn.Name, fn.Size, len(fn.Relocs))
|
|
} else {
|
|
fmt.Printf(" %s: MATCH (%d bytes)\n", fn.Name, fn.Size)
|
|
}
|
|
} else {
|
|
fmt.Printf(" %s: MISMATCH (%d vs %d bytes)\n", fn.Name, fn.Size, len(goCode))
|
|
for i := 0; i < len(gasmCode) || i < len(goCode); i += 16 {
|
|
var gb, gs string
|
|
for j := i; j < i+16 && j < len(gasmCode); j++ {
|
|
gb += fmt.Sprintf(" %02x", gasmCode[j])
|
|
}
|
|
for j := i; j < i+16 && j < len(goCode); j++ {
|
|
gs += fmt.Sprintf(" %02x", goCode[j])
|
|
}
|
|
fmt.Printf(" %04x: gasm:%s\n", i, gb)
|
|
fmt.Printf(" %04x: gt: %s\n", i, gs)
|
|
}
|
|
}
|
|
}
|
|
fmt.Printf("%s: %d/%d matched\n", path, matched, total)
|
|
if matched < total {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
if profile {
|
|
for _, fn := range img.Funcs {
|
|
fmt.Printf("%s: %d bytes, labels: %v\n", fn.Name, fn.Size, fn.Labels)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
fmt.Printf("%s: %d functions assembled\n", path, len(img.Funcs))
|
|
for _, fn := range img.Funcs {
|
|
fmt.Printf(" %s: %d bytes\n", fn.Name, fn.Size)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// cmdVerifyLOONG64 verifies a loong64 source file against `go tool asm`
|
|
// (GOARCH=loong64) — the ground-truth oracle — since gasm cannot JIT-load
|
|
// LoongArch code on an amd64 host. Relocation sites are masked before the
|
|
// byte comparison, as the toolchain leaves them zero for the linker.
|
|
func cmdVerifyLOONG64(path string, groundTruth, profile bool) int {
|
|
src, err := readSource(path)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
return 1
|
|
}
|
|
f, errs := parser.Parse(path, src)
|
|
for _, e := range errs {
|
|
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
|
|
}
|
|
if len(errs) > 0 {
|
|
return 1
|
|
}
|
|
img, err := asm.AssembleFileLOONG64(f)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
return 1
|
|
}
|
|
|
|
if groundTruth {
|
|
gt, err := verify.GroundTruthLOONG64(path)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err)
|
|
return 1
|
|
}
|
|
matched, total := 0, 0
|
|
for _, fn := range img.Funcs {
|
|
gasmCode := img.Code[fn.Offset : fn.Offset+fn.Size]
|
|
goCode, ok := gt[fn.Name]
|
|
if !ok {
|
|
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", fn.Name)
|
|
continue
|
|
}
|
|
total++
|
|
gasmCmp := make([]byte, len(gasmCode))
|
|
goCmp := make([]byte, len(goCode))
|
|
copy(gasmCmp, gasmCode)
|
|
copy(goCmp, goCode)
|
|
for _, r := range fn.Relocs {
|
|
for j := r.Off; j < r.Off+4 && j < len(gasmCmp); j++ {
|
|
gasmCmp[j] = 0
|
|
}
|
|
for j := r.Off; j < r.Off+4 && j < len(goCmp); j++ {
|
|
goCmp[j] = 0
|
|
}
|
|
}
|
|
if bytes.Equal(gasmCmp, goCmp) {
|
|
matched++
|
|
if len(fn.Relocs) > 0 {
|
|
fmt.Printf(" %s: MATCH (%d bytes, %d relocs masked)\n", fn.Name, fn.Size, len(fn.Relocs))
|
|
} else {
|
|
fmt.Printf(" %s: MATCH (%d bytes)\n", fn.Name, fn.Size)
|
|
}
|
|
} else {
|
|
fmt.Printf(" %s: MISMATCH (%d vs %d bytes)\n", fn.Name, fn.Size, len(goCode))
|
|
for i := 0; i < len(gasmCode) || i < len(goCode); i += 16 {
|
|
var gb, gs string
|
|
for j := i; j < i+16 && j < len(gasmCode); j++ {
|
|
gb += fmt.Sprintf(" %02x", gasmCode[j])
|
|
}
|
|
for j := i; j < i+16 && j < len(goCode); j++ {
|
|
gs += fmt.Sprintf(" %02x", goCode[j])
|
|
}
|
|
fmt.Printf(" %04x: gasm:%s\n", i, gb)
|
|
fmt.Printf(" %04x: gt: %s\n", i, gs)
|
|
}
|
|
}
|
|
}
|
|
fmt.Printf("%s: %d/%d matched\n", path, matched, total)
|
|
if matched < total {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
if profile {
|
|
for _, fn := range img.Funcs {
|
|
fmt.Printf("%s: %d bytes, labels: %v\n", fn.Name, fn.Size, fn.Labels)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
fmt.Printf("%s: %d functions assembled\n", path, len(img.Funcs))
|
|
for _, fn := range img.Funcs {
|
|
fmt.Printf(" %s: %d bytes\n", fn.Name, fn.Size)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func cmdVerifyARM64(path string, groundTruth, profile bool) int {
|
|
src, err := readSource(path)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
return 1
|
|
}
|
|
f, errs := parser.Parse(path, src)
|
|
for _, e := range errs {
|
|
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
|
|
}
|
|
if len(errs) > 0 {
|
|
return 1
|
|
}
|
|
img, err := asm.AssembleFileARM64(f)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
return 1
|
|
}
|
|
|
|
if groundTruth {
|
|
gt, err := verify.GroundTruthARM64(path)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err)
|
|
return 1
|
|
}
|
|
matched, total := 0, 0
|
|
for _, fn := range img.Funcs {
|
|
gasmCode := img.Code[fn.Offset : fn.Offset+fn.Size]
|
|
goCode, ok := gt[fn.Name]
|
|
if !ok {
|
|
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", fn.Name)
|
|
continue
|
|
}
|
|
total++
|
|
gasmCmp := make([]byte, len(gasmCode))
|
|
goCmp := make([]byte, len(goCode))
|
|
copy(gasmCmp, gasmCode)
|
|
copy(goCmp, goCode)
|
|
for _, r := range fn.Relocs {
|
|
for j := r.Off; j < r.Off+4 && j < len(gasmCmp); j++ {
|
|
gasmCmp[j] = 0
|
|
}
|
|
for j := r.Off; j < r.Off+4 && j < len(goCmp); j++ {
|
|
goCmp[j] = 0
|
|
}
|
|
}
|
|
if bytes.Equal(gasmCmp, goCmp) {
|
|
matched++
|
|
if len(fn.Relocs) > 0 {
|
|
fmt.Printf(" %s: MATCH (%d bytes, %d relocs masked)\n", fn.Name, fn.Size, len(fn.Relocs))
|
|
} else {
|
|
fmt.Printf(" %s: MATCH (%d bytes)\n", fn.Name, fn.Size)
|
|
}
|
|
} else {
|
|
fmt.Printf(" %s: MISMATCH (%d vs %d bytes)\n", fn.Name, fn.Size, len(goCode))
|
|
for i := 0; i < len(gasmCode) || i < len(goCode); i += 16 {
|
|
var gb, gs string
|
|
for j := i; j < i+16 && j < len(gasmCode); j++ {
|
|
gb += fmt.Sprintf(" %02x", gasmCode[j])
|
|
}
|
|
for j := i; j < i+16 && j < len(goCode); j++ {
|
|
gs += fmt.Sprintf(" %02x", goCode[j])
|
|
}
|
|
fmt.Printf(" %04x: gasm:%s\n", i, gb)
|
|
fmt.Printf(" %04x: gt: %s\n", i, gs)
|
|
}
|
|
}
|
|
}
|
|
fmt.Printf("%s: %d/%d matched\n", path, matched, total)
|
|
if matched < total {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
if profile {
|
|
for _, fn := range img.Funcs {
|
|
fmt.Printf("%s: %d bytes, labels: %v\n", fn.Name, fn.Size, fn.Labels)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
fmt.Printf("%s: %d functions assembled\n", path, len(img.Funcs))
|
|
for _, fn := range img.Funcs {
|
|
fmt.Printf(" %s: %d bytes\n", fn.Name, fn.Size)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func cmdVerify(args []string) int {
|
|
set := newCommand("verify", "gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <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 registers
|
|
the Go ABI fixes across calls (the frame pointer and the goroutine
|
|
pointer) plus a canary below SP; violations are reported. JIT-based
|
|
checks run when the host matches the file's architecture (all but
|
|
loong64, which is ground-truth only for now).
|
|
|
|
With -fuzz, each function with a // func signature is differentially fuzzed
|
|
against the go-tool-asm version in a subprocess (so a crash on a partial
|
|
function is reported, not fatal).
|
|
|
|
With -ground-truth, the assembled machine code is compared byte-for-byte
|
|
against go tool asm (relocation sites masked), reporting any encoding drift.
|
|
|
|
With -profile, the static basic-block structure is listed for each function.
|
|
|
|
With -call, a single function is invoked with user-supplied buffers (-buf)
|
|
instead of the smoke/abi/fuzz sweeps. Useful for partial functions (e.g.
|
|
decoders) that crash on random input but should succeed on valid data.
|
|
|
|
With -save-corpus (and -fuzz), every input that crashes or mismatches is
|
|
written to the directory as replayable JSON. -replay re-runs saved
|
|
entries against the kernel, one child process per entry, so an input that
|
|
crashed the original run crashes only the child: the report says whether
|
|
each entry reproduces.
|
|
`)
|
|
smoke := set.Bool("smoke", false, "call each NOSPLIT function with zeroed args")
|
|
abi := set.Bool("abi", false, "run ABI-checking calls (sentinel registers + red zone)")
|
|
abiN := set.Int("abi-n", 100, "number of ABI check iterations with varied inputs")
|
|
profile := set.Bool("profile", false, "list basic-block structure per function")
|
|
groundTruth := set.Bool("ground-truth", false, "compare machine code byte-for-byte against go tool asm")
|
|
fuzz := set.Bool("fuzz", false, "differential fuzz: JIT both gasm and go-tool-asm versions, compare outputs")
|
|
fuzzN := set.Int("n", 1000, "number of fuzz iterations per function")
|
|
call := set.String("call", "", "call a single function with -buf instead of the sweeps")
|
|
bufSpec := set.String("buf", "", "buffer spec for -call: name:size:pattern[,name:size:pattern] (zero, ones, seq, or hex)")
|
|
scalarSpec := set.String("args", "", "scalar args for -call: name=value[,name=value] (decimal or 0x hex)")
|
|
repeat := set.Int("repeat", 1, "number of times to repeat a -call invocation")
|
|
saveCorpus := set.String("save-corpus", "", "with -fuzz: write each failing input to this directory as replayable JSON")
|
|
replay := set.String("replay", "", "replay saved corpus entries (JSON files in this directory) against the kernel")
|
|
set.Parse(args)
|
|
if set.NArg() != 1 {
|
|
fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <file.s>")
|
|
return 2
|
|
}
|
|
path := set.Arg(0)
|
|
targetArch := arch.FromFilename(path)
|
|
// JIT execution runs when the host CPU matches the kernel's
|
|
// architecture, except loong64: its trampoline is implemented but not
|
|
// yet validated against real hardware (the Go runtime cannot start
|
|
// under the available loong64 emulators), so those kernels take the
|
|
// toolchain-comparison path.
|
|
if targetArch != hostArch() || targetArch == arch.LOONG64 {
|
|
switch targetArch {
|
|
case arch.RISCV:
|
|
// RISC-V: ground-truth only (no JIT on non-RISC-V hosts).
|
|
return cmdVerifyRISCV(path, *groundTruth, *profile)
|
|
case arch.LOONG64:
|
|
// LoongArch: ground-truth only (trampoline not yet
|
|
// hardware-validated).
|
|
return cmdVerifyLOONG64(path, *groundTruth, *profile)
|
|
case arch.ARM64:
|
|
// AArch64: ground-truth only (no JIT on non-ARM64 hosts).
|
|
return cmdVerifyARM64(path, *groundTruth, *profile)
|
|
case arch.AMD64:
|
|
fmt.Fprintln(os.Stderr, "gasm verify: JIT-based checks need an amd64 host; use --ground-truth here")
|
|
return 1
|
|
default:
|
|
fmt.Fprintln(os.Stderr, "gasm verify: unsupported architecture")
|
|
return 1
|
|
}
|
|
}
|
|
|
|
k, err := verify.Load(path)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
return 1
|
|
}
|
|
defer k.Close()
|
|
|
|
names := k.FuncNames()
|
|
fmt.Printf("%s: %d functions JIT-loaded\n", path, len(names))
|
|
rc := 0
|
|
|
|
// Single-function call mode: invoke one function with user-supplied buffers.
|
|
if *call != "" {
|
|
return cmdVerifyCall(k, path, *call, *bufSpec, *scalarSpec, *repeat)
|
|
}
|
|
|
|
// Corpus replay: re-run every saved entry in its own child process, so
|
|
// an input that crashed the original run crashes only the child.
|
|
if rp := os.Getenv("GASM_VERIFY_REPLAY_ONE"); rp != "" {
|
|
return cmdReplayOne(k, rp)
|
|
}
|
|
if *replay != "" {
|
|
return cmdVerifyReplay(path, *replay)
|
|
}
|
|
|
|
// Subprocess mode: fuzz a single function and exit. The parent selects
|
|
// the function through the environment, so no internal flag leaks into
|
|
// the -h output.
|
|
fuzzOne := os.Getenv("GASM_VERIFY_FUZZ_ONE")
|
|
if fuzzOne != "" {
|
|
gt, err := verify.GroundTruth(path)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
return 1
|
|
}
|
|
src, err := readSource(path)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
return 1
|
|
}
|
|
sigs := verify.ExtractSignatures(src)
|
|
sig, ok := sigs[fuzzOne]
|
|
if !ok {
|
|
fmt.Printf("%s: no signature\n", fuzzOne)
|
|
return 0
|
|
}
|
|
goCode, ok := gt[fuzzOne]
|
|
if !ok {
|
|
fmt.Printf("%s: not in go tool asm\n", fuzzOne)
|
|
return 0
|
|
}
|
|
var onSave func(verify.CorpusEntry)
|
|
if *saveCorpus != "" {
|
|
if err := os.MkdirAll(*saveCorpus, 0o755); err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
return 1
|
|
}
|
|
saved := 0
|
|
onSave = func(e verify.CorpusEntry) {
|
|
file := filepath.Join(*saveCorpus, fmt.Sprintf("%s@%d.json", sanitize(e.Func), saved))
|
|
saved++
|
|
data, err := json.MarshalIndent(e, "", " ")
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = os.WriteFile(file, data, 0o644)
|
|
}
|
|
}
|
|
res := k.FuzzFuncHook(fuzzOne, sig, goCode, *fuzzN, 42, onSave)
|
|
fmt.Printf("%s\n", res)
|
|
if !res.OK() {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Subprocess mode: run the smoke/abi checks for a single function and
|
|
// exit with the accumulated status. The parent interprets a clean exit
|
|
// as success, a non-zero exit as failure and death-by-signal as a crash.
|
|
// Like the fuzz mode, the parent selects the function through the
|
|
// environment instead of an internal flag.
|
|
if sweepOne := os.Getenv("GASM_VERIFY_SWEEP_ONE"); sweepOne != "" {
|
|
fl, err := k.Func(sweepOne)
|
|
if err != nil || !fl.NoSplit {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %s: %v\n", sweepOne, err)
|
|
return 1
|
|
}
|
|
msgs, failed := runSweepChecks(k, path, sweepOne, fl, *smoke, *abi, *abiN)
|
|
for _, m := range msgs {
|
|
fmt.Println(m)
|
|
}
|
|
if failed {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Ground-truth comparison: assemble with go tool asm and compare bytes.
|
|
if *groundTruth {
|
|
gt, err := verify.GroundTruth(path)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err)
|
|
return 1
|
|
}
|
|
matched, total := 0, 0
|
|
for _, name := range names {
|
|
fl, _ := k.Func(name)
|
|
gasmCode := k.Image().Code[fl.Offset : fl.Offset+fl.Size]
|
|
goCode, ok := gt[name]
|
|
if !ok {
|
|
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", name)
|
|
continue
|
|
}
|
|
total++
|
|
// Compare, masking relocation sites (disp32 fields that the
|
|
// Go linker fills at link time — gasm resolves them internally).
|
|
gasmCmp := make([]byte, len(gasmCode))
|
|
goCmp := make([]byte, len(goCode))
|
|
copy(gasmCmp, gasmCode)
|
|
copy(goCmp, goCode)
|
|
for _, r := range fl.Relocs {
|
|
for j := r.Off; j < r.Off+4 && j < len(gasmCmp); j++ {
|
|
gasmCmp[j] = 0
|
|
}
|
|
for j := r.Off; j < r.Off+4 && j < len(goCmp); j++ {
|
|
goCmp[j] = 0
|
|
}
|
|
}
|
|
if bytes.Equal(gasmCmp, goCmp) {
|
|
matched++
|
|
if len(fl.Relocs) > 0 {
|
|
fmt.Printf(" %s: MATCH (%d bytes, %d relocs masked)\n", name, fl.Size, len(fl.Relocs))
|
|
} else {
|
|
fmt.Printf(" %s: MATCH (%d bytes)\n", name, fl.Size)
|
|
}
|
|
} else {
|
|
fmt.Printf(" %s: MISMATCH (gasm %d bytes, go %d bytes)\n", name, fl.Size, len(goCode))
|
|
for i := 0; i < len(gasmCmp) && i < len(goCmp); i++ {
|
|
if gasmCmp[i] != goCmp[i] {
|
|
fmt.Printf(" first diff at byte %d: gasm=%02x go=%02x\n", i, gasmCmp[i], goCmp[i])
|
|
break
|
|
}
|
|
}
|
|
rc = 1
|
|
}
|
|
}
|
|
fmt.Printf("ground truth: %d/%d functions byte-identical\n", matched, total)
|
|
if matched < total {
|
|
rc = 1
|
|
}
|
|
}
|
|
|
|
// Differential fuzz: JIT both gasm and go-tool-asm, compare outputs.
|
|
// Each function runs in a subprocess so a crash (partial functions like
|
|
// decoders that fault on malformed input) doesn't kill the whole run.
|
|
if *fuzz {
|
|
gt, err := verify.GroundTruth(path)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: fuzz: %v\n", err)
|
|
return 1
|
|
}
|
|
src, err := readSource(path)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
return 1
|
|
}
|
|
sigs := verify.ExtractSignatures(src)
|
|
fuzzed := 0
|
|
for _, name := range names {
|
|
sig, ok := sigs[name]
|
|
if !ok {
|
|
fmt.Printf(" %s: SKIP (no // func signature)\n", name)
|
|
continue
|
|
}
|
|
goCode, ok := gt[name]
|
|
if !ok {
|
|
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", name)
|
|
continue
|
|
}
|
|
// Run in a subprocess: if the function crashes on random
|
|
// input (partial function), we report it and move on.
|
|
var extra []string
|
|
if *saveCorpus != "" {
|
|
extra = append(extra, "-save-corpus", *saveCorpus)
|
|
}
|
|
res := fuzzInSubprocess(path, name, *fuzzN, extra...)
|
|
if res != "" {
|
|
fmt.Printf(" %s\n", res)
|
|
if strings.Contains(res, "MISMATCH") {
|
|
rc = 1
|
|
}
|
|
}
|
|
_ = sig
|
|
_ = goCode
|
|
fuzzed++
|
|
}
|
|
fmt.Printf("fuzz: %d functions tested, %d iterations each\n", fuzzed, *fuzzN)
|
|
}
|
|
// Print function info and profile.
|
|
for _, name := range names {
|
|
fl, _ := k.Func(name)
|
|
flags := ""
|
|
if fl.NoSplit {
|
|
flags = " NOSPLIT"
|
|
}
|
|
fmt.Printf(" %s: %d bytes, args=%d, frame=%d%s\n", name, fl.Size, fl.Args, fl.Frame, flags)
|
|
if *profile {
|
|
blocks, err := k.Blocks(name)
|
|
if err != nil {
|
|
fmt.Printf(" profile: %v\n", err)
|
|
} else {
|
|
fmt.Printf(" blocks: %d\n", len(blocks))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run smoke and ABI checks in parallel, each function in its own child
|
|
// process: the JIT'd code runs with zeroed or fuzzed arguments, and a
|
|
// function that dereferences them faults — the crash is reported as a
|
|
// CRASH line instead of killing this process (mirrors fuzzInSubprocess).
|
|
if *smoke || *abi {
|
|
type checkResult struct {
|
|
name string
|
|
msg string
|
|
fail bool
|
|
}
|
|
var mu sync.Mutex
|
|
var results []checkResult
|
|
sem := make(chan struct{}, runtime.NumCPU())
|
|
var wg sync.WaitGroup
|
|
|
|
for _, name := range names {
|
|
fl, _ := k.Func(name)
|
|
if !fl.NoSplit {
|
|
continue
|
|
}
|
|
wg.Add(1)
|
|
go func(name string) {
|
|
defer wg.Done()
|
|
sem <- struct{}{}
|
|
defer func() { <-sem }()
|
|
|
|
msg, fail := sweepInSubprocess(path, name, *smoke, *abi, *abiN)
|
|
mu.Lock()
|
|
results = append(results, checkResult{name: name, msg: msg, fail: fail})
|
|
mu.Unlock()
|
|
}(name)
|
|
}
|
|
wg.Wait()
|
|
|
|
// Print results in source order.
|
|
resultMap := make(map[string]checkResult, len(results))
|
|
for _, r := range results {
|
|
resultMap[r.name] = r
|
|
}
|
|
for _, name := range names {
|
|
if r, ok := resultMap[name]; ok {
|
|
fmt.Println(r.msg)
|
|
if r.fail {
|
|
rc = 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return rc
|
|
}
|
|
|
|
// fuzzInSubprocess runs the fuzz for a single function in a child process.
|
|
// cmdVerifyReplay replays every saved corpus entry against the kernel, one
|
|
// child process per entry so an input that crashed the original run crashes
|
|
// only the child. Exits non-zero when any entry crashes or fails.
|
|
func cmdVerifyReplay(path, dir string) int {
|
|
self, err := os.Executable()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: cannot find self: %v\n", err)
|
|
return 1
|
|
}
|
|
files, err := filepath.Glob(filepath.Join(dir, "*.json"))
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
return 1
|
|
}
|
|
if len(files) == 0 {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: no corpus entries in %s\n", dir)
|
|
return 1
|
|
}
|
|
slices.Sort(files)
|
|
rc := 0
|
|
for _, f := range files {
|
|
cmd := exec.Command(self, "verify", path)
|
|
cmd.Env = append(os.Environ(), "GASM_VERIFY_REPLAY_ONE="+f)
|
|
out, err := cmd.CombinedOutput()
|
|
name := filepath.Base(f)
|
|
switch {
|
|
case err == nil:
|
|
fmt.Printf(" %s: OK\n", name)
|
|
case replayCrashed(err):
|
|
rc = 1
|
|
fmt.Printf(" %s: CRASH (reproduced)\n", name)
|
|
default:
|
|
rc = 1
|
|
detail := strings.TrimSpace(string(out))
|
|
if detail == "" {
|
|
detail = err.Error()
|
|
}
|
|
fmt.Printf(" %s: FAIL (%s)\n", name, detail)
|
|
}
|
|
}
|
|
return rc
|
|
}
|
|
|
|
// replayCrashed reports whether a replay child died from a signal, which
|
|
// means the saved input reproduced its original crash.
|
|
func replayCrashed(err error) bool {
|
|
exitErr, ok := err.(*exec.ExitError)
|
|
if !ok {
|
|
return false
|
|
}
|
|
ws, ok := exitErr.Sys().(syscall.WaitStatus)
|
|
return ok && ws.Signaled()
|
|
}
|
|
|
|
// cmdReplayOne is the child half of corpus replay: rebuild one entry and
|
|
// call it, reporting the outcome on stdout.
|
|
func cmdReplayOne(k *verify.Kernel, file string) int {
|
|
data, err := os.ReadFile(file)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
return 1
|
|
}
|
|
var e verify.CorpusEntry
|
|
if err := json.Unmarshal(data, &e); err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %s: %v\n", file, err)
|
|
return 1
|
|
}
|
|
if _, err := k.ReplayEntry(e.Func, e); err != nil {
|
|
fmt.Printf("%s: %v\n", e.Func, err)
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// If the child is killed by a signal (e.g. SIGSEGV from a partial function
|
|
// faulting on random input), it returns a CRASH report instead of dying.
|
|
func fuzzInSubprocess(path, funcName string, n int, extra ...string) string {
|
|
self, err := os.Executable()
|
|
if err != nil {
|
|
return fmt.Sprintf("%s: cannot find self: %v", funcName, err)
|
|
}
|
|
fuzzChildArgs := append([]string{"verify", "-n", strconv.Itoa(n)}, extra...)
|
|
fuzzChildArgs = append(fuzzChildArgs, path)
|
|
cmd := exec.Command(self, fuzzChildArgs...)
|
|
cmd.Env = append(os.Environ(), "GASM_VERIFY_FUZZ_ONE="+funcName)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
// Check if the child was killed by a signal.
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
ws := exitErr.Sys().(syscall.WaitStatus)
|
|
if ws.Signaled() {
|
|
return fmt.Sprintf("%s: CRASH (%v — partial function, use --ground-truth)", funcName, ws.Signal())
|
|
}
|
|
}
|
|
// Non-zero exit without a signal: the fuzz reported mismatches.
|
|
lines := strings.SplitSeq(strings.TrimSpace(string(out)), "\n")
|
|
for l := range lines {
|
|
if strings.Contains(l, funcName) {
|
|
return strings.TrimSpace(l)
|
|
}
|
|
}
|
|
return fmt.Sprintf("%s: FAIL (exit %v)", funcName, err)
|
|
}
|
|
// Success: extract the result line.
|
|
lines := strings.SplitSeq(strings.TrimSpace(string(out)), "\n")
|
|
for l := range lines {
|
|
if strings.Contains(l, funcName) {
|
|
return strings.TrimSpace(l)
|
|
}
|
|
}
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
|
|
// sweepInSubprocess runs the smoke/abi checks for a single function in a
|
|
// child process. If the child is killed by a signal (e.g. SIGSEGV from a
|
|
// function that dereferences its zeroed or fuzzed arguments), it returns a
|
|
// CRASH report instead of dying — the same isolation fuzzInSubprocess
|
|
// provides for the fuzz sweep.
|
|
func sweepInSubprocess(path, funcName string, smoke, abi bool, abiN int) (string, bool) {
|
|
self, err := os.Executable()
|
|
if err != nil {
|
|
return fmt.Sprintf(" smoke/abi: FAIL — cannot find self: %v", err), true
|
|
}
|
|
args := []string{"verify"}
|
|
if smoke {
|
|
args = append(args, "-smoke")
|
|
}
|
|
if abi {
|
|
args = append(args, "-abi", "-abi-n", strconv.Itoa(abiN))
|
|
}
|
|
args = append(args, path)
|
|
cmd := exec.Command(self, args...)
|
|
cmd.Env = append(os.Environ(), "GASM_VERIFY_SWEEP_ONE="+funcName)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
ws, ok := exitErr.Sys().(syscall.WaitStatus)
|
|
if ok && ws.Signaled() {
|
|
return fmt.Sprintf(" smoke/abi: CRASH (%v — the function faults on zeroed or fuzzed\n arguments; verify it with -call and valid buffers)", ws.Signal()), true
|
|
}
|
|
}
|
|
// Non-zero exit without a signal: the checks themselves failed and
|
|
// the child already printed the diagnostic lines.
|
|
return sweepCheckLines(out), true
|
|
}
|
|
return sweepCheckLines(out), false
|
|
}
|
|
|
|
// sweepCheckLines extracts the check-result lines from child output,
|
|
// dropping the child's own file/function banners (the parent prints those).
|
|
func sweepCheckLines(out []byte) string {
|
|
var lines []string
|
|
for l := range strings.SplitSeq(string(out), "\n") {
|
|
t := strings.TrimSpace(l)
|
|
if strings.HasPrefix(t, "smoke:") || strings.HasPrefix(t, "abi:") {
|
|
lines = append(lines, " "+t)
|
|
}
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
// runSweepChecks performs the in-process smoke and ABI checks for one
|
|
// function — the child half of sweepInSubprocess.
|
|
func runSweepChecks(k *verify.Kernel, path, name string, fl asm.FuncLayout, smoke, abi bool, abiN int) ([]string, bool) {
|
|
var msgs []string
|
|
failed := false
|
|
|
|
if smoke {
|
|
args := make([]byte, fl.Args)
|
|
_, err := k.CallFunc(name, args)
|
|
if err != nil {
|
|
msgs = append(msgs, fmt.Sprintf(" smoke: FAIL — %v", err))
|
|
failed = true
|
|
} else {
|
|
msgs = append(msgs, " smoke: OK")
|
|
}
|
|
}
|
|
|
|
if abi {
|
|
if src, err := readSource(path); err == nil {
|
|
result := k.FuzzFuncCheckedByName(name, src, abiN, int64(abiN))
|
|
if result.Mismatches > 0 {
|
|
msgs = append(msgs, fmt.Sprintf(" abi: %s", result))
|
|
failed = true
|
|
} else {
|
|
msgs = append(msgs, fmt.Sprintf(" abi: clean (%d varied inputs)", result.Matches))
|
|
}
|
|
} else {
|
|
args := make([]byte, fl.Args)
|
|
_, report, err := k.CallFuncChecked(name, args)
|
|
if err != nil {
|
|
msgs = append(msgs, fmt.Sprintf(" abi: FAIL — %v", err))
|
|
failed = true
|
|
} else if !report.OK() {
|
|
msgs = append(msgs, fmt.Sprintf(" abi: %s", report))
|
|
failed = true
|
|
} else {
|
|
msgs = append(msgs, " abi: clean")
|
|
}
|
|
}
|
|
}
|
|
return msgs, failed
|
|
}
|
|
|
|
// cmdVerifyCall implements `gasm verify --call <func> [--buf spec] [--args spec] [--repeat n]`.
|
|
// It invokes a single function with user-supplied buffers and scalar
|
|
// arguments and prints the arg block before and after the call, so the user
|
|
// can inspect return values and any output written to the buffers.
|
|
func cmdVerifyCall(k *verify.Kernel, path, funcName, bufSpec, scalarSpec string, repeat int) int {
|
|
fl, err := k.Func(funcName)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
return 1
|
|
}
|
|
if !fl.NoSplit {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %s is not NOSPLIT (frame=%d); --call supports NOSPLIT functions only\n", funcName, fl.Frame)
|
|
return 1
|
|
}
|
|
|
|
// Parse the // func signature to lay out the argument block.
|
|
src, err := readSource(path)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
return 1
|
|
}
|
|
sig, ok := verify.ExtractFuncSig(src, funcName)
|
|
if !ok {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: no // func signature found for %s\n", funcName)
|
|
return 1
|
|
}
|
|
layout := verify.ArgLayout(sig)
|
|
|
|
// Allocate the requested buffers (if any) and build the arg block.
|
|
specs, err := verify.ParseBufSpec(bufSpec)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
return 1
|
|
}
|
|
var pool verify.BufPool
|
|
if err := pool.Alloc(specs); err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
return 1
|
|
}
|
|
defer pool.Close()
|
|
args := pool.BuildArgs(layout, fl.Args)
|
|
|
|
scalars, err := verify.ParseScalarArgs(scalarSpec)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
return 1
|
|
}
|
|
if err := verify.ApplyScalarArgs(args, layout, scalars); err != nil {
|
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
return 1
|
|
}
|
|
|
|
fmt.Printf("%s: %d bytes, args=%d\n", funcName, fl.Size, fl.Args)
|
|
fmt.Printf(" signature: func %s(%s) %s\n", sig.Name, formatParams(sig.Params), formatResults(sig.Results))
|
|
if len(specs) > 0 {
|
|
fmt.Printf(" buffers:\n")
|
|
for _, s := range specs {
|
|
fmt.Printf(" %s: %d bytes, pattern=%s\n", s.Name, s.Size, s.Pattern)
|
|
}
|
|
}
|
|
if len(scalars) > 0 {
|
|
names := make([]string, 0, len(scalars))
|
|
for n := range scalars {
|
|
names = append(names, n)
|
|
}
|
|
sort.Strings(names)
|
|
fmt.Printf(" scalars:\n")
|
|
for _, n := range names {
|
|
fmt.Printf(" %s = %d\n", n, scalars[n])
|
|
}
|
|
}
|
|
fmt.Printf(" args before: %s\n", hexDump(args))
|
|
|
|
rc := 0
|
|
for i := range repeat {
|
|
out, err := k.CallFunc(funcName, args)
|
|
if err != nil {
|
|
fmt.Printf(" call %d: FAIL — %v\n", i+1, err)
|
|
rc = 1
|
|
continue
|
|
}
|
|
if repeat == 1 {
|
|
fmt.Printf(" args after: %s\n", hexDump(out))
|
|
} else if i == repeat-1 {
|
|
fmt.Printf(" args after %d calls: %s\n", repeat, hexDump(out))
|
|
}
|
|
fmt.Printf(" call %d: OK\n", i+1)
|
|
}
|
|
return rc
|
|
}
|
|
|
|
// formatParams renders a parameter list as "a []byte, b []byte".
|
|
func formatParams(ps []verify.Param) string {
|
|
var parts []string
|
|
for _, p := range ps {
|
|
if p.Name != "" {
|
|
parts = append(parts, p.Name+" "+p.Typ)
|
|
} else {
|
|
parts = append(parts, p.Typ)
|
|
}
|
|
}
|
|
return strings.Join(parts, ", ")
|
|
}
|
|
|
|
// formatResults renders a result list as "(n int, code int)" or "int".
|
|
func formatResults(rs []verify.Param) string {
|
|
if len(rs) == 0 {
|
|
return ""
|
|
}
|
|
if len(rs) == 1 && rs[0].Name == "" {
|
|
return rs[0].Typ
|
|
}
|
|
return "(" + formatParams(rs) + ")"
|
|
}
|
|
|
|
// hexDump returns a one-line hex dump of buf, truncated to 64 bytes.
|
|
func hexDump(buf []byte) string {
|
|
const max = 64
|
|
n := min(len(buf), max)
|
|
var sb strings.Builder
|
|
for i := range n {
|
|
if i > 0 {
|
|
sb.WriteByte(' ')
|
|
}
|
|
fmt.Fprintf(&sb, "%02x", buf[i])
|
|
}
|
|
return fmt.Sprintf("%s%s (%d bytes)", sb.String(), truncMark(len(buf), max), len(buf))
|
|
}
|
|
|
|
// truncMark returns "…" when the buffer is longer than max, else "".
|
|
func truncMark(n, max int) string {
|
|
if n > max {
|
|
return "…"
|
|
}
|
|
return ""
|
|
}
|