2026-07-06 09:49:50 +02:00
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
2026-09-14 18:22:00 +02:00
// Command gasm is the developer frontend for GAsm, Go's Plan 9 assembler.
2026-07-06 09:49:50 +02:00
// 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 (
2026-07-30 12:55:54 +02:00
"bytes"
2026-08-30 21:23:43 +02:00
"encoding/json"
2026-09-19 23:49:19 +02:00
"errors"
2026-07-06 09:49:50 +02:00
"flag"
"fmt"
"io"
2026-07-12 21:24:41 +02:00
"io/fs"
2026-07-06 09:49:50 +02:00
"os"
2026-08-01 18:08:10 +02:00
"os/exec"
2026-07-12 21:24:41 +02:00
"path/filepath"
2026-08-21 01:10:14 +02:00
"runtime"
2026-09-16 22:53:01 +02:00
"runtime/debug"
2026-08-30 21:23:43 +02:00
"slices"
2026-08-05 16:41:00 +02:00
"sort"
2026-08-01 18:08:10 +02:00
"strconv"
2026-07-06 09:49:50 +02:00
"strings"
2026-08-21 01:10:14 +02:00
"sync"
2026-08-01 18:08:10 +02:00
"syscall"
2026-07-06 09:49:50 +02:00
"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"
2026-07-22 09:28:11 +02:00
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
2026-07-06 09:49:50 +02:00
)
2026-09-16 22:53:01 +02:00
// version reports the release the toolchain recorded for this build: the
// tag on a tag, a pseudo-version below one, and (devel) outside version
// control. Nothing is injected; the recorded value cannot go stale.
func version () string {
bi , ok := debug . ReadBuildInfo ()
if ! ok || bi . Main . Version == "" {
return "(devel)"
}
return bi . Main . Version
}
2026-07-06 09:49:50 +02:00
2026-09-19 23:49:19 +02:00
// usageError marks an error the caller's arguments caused, which exits 2
// instead of the 1 a runtime failure gets.
type usageError struct { err error }
func ( e * usageError ) Error () string { return e . err . Error () }
func ( e * usageError ) Unwrap () error { return e . err }
// exitCodeFor maps an error onto the process exit status: 2 for a usage
// error, 1 for anything else.
func exitCodeFor ( err error ) int {
if _ , ok := errors . AsType [ * usageError ]( err ); ok {
return 2
}
return 1
}
2026-07-06 09:49:50 +02:00
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 :]))
2026-09-14 18:47:08 +02:00
case "dis" :
os . Exit ( cmdDis ( os . Args [ 2 :]))
2026-07-22 09:28:11 +02:00
case "verify" :
os . Exit ( cmdVerify ( os . Args [ 2 :]))
2026-08-01 02:34:00 +02:00
case "debug" :
os . Exit ( cmdDebug ( os . Args [ 2 :]))
2026-08-05 14:23:00 +02:00
case "diff" :
os . Exit ( cmdDiff ( os . Args [ 2 :]))
2026-08-05 16:41:00 +02:00
case "profile" :
os . Exit ( cmdProfile ( os . Args [ 2 :]))
2026-08-29 13:52:32 +02:00
case "audit-instructions" :
if err := cmdAuditInstructions ( os . Args [ 2 :]); err != nil {
fmt . Fprintln ( os . Stderr , err )
2026-09-19 23:49:19 +02:00
os . Exit ( exitCodeFor ( err ))
2026-08-29 13:52:32 +02:00
}
case "scaffold" :
if err := cmdScaffold ( os . Args [ 2 :]); err != nil {
fmt . Fprintln ( os . Stderr , err )
2026-09-19 23:49:19 +02:00
os . Exit ( exitCodeFor ( err ))
2026-08-29 13:52:32 +02:00
}
2026-07-06 09:49:50 +02:00
case "lsp" :
os . Exit ( cmdLSP ( os . Args [ 2 :]))
case "version" , "--version" , "-V" :
2026-07-13 19:50:38 +02:00
os . Exit ( cmdVersion ())
case "help" , "--help" , "-h" :
2026-07-06 09:49:50 +02:00
usage ( os . Stdout )
default :
2026-09-14 18:22:00 +02:00
fmt . Fprintf ( os . Stderr , "gasm: unknown command %q; run \"gasm --help\" for usage\n" , os . Args [ 1 ])
2026-07-06 09:49:50 +02:00
os . Exit ( 2 )
}
}
2026-09-16 22:53:01 +02:00
// cmdVersion prints the recorded version.
2026-07-13 19:50:38 +02:00
func cmdVersion () int {
2026-09-16 22:53:01 +02:00
fmt . Printf ( "gasm %s\n" , version ())
2026-07-13 19:50:38 +02:00
return 0
}
2026-09-17 20:33:18 +02:00
// ANSI colour helpers for terminal output.
2026-08-03 19:42:05 +02:00
const (
2026-09-19 23:49:19 +02:00
colourReset = "\033[0m"
colourBold = "\033[1m"
colourCyan = "\033[36m"
colourYellow = "\033[33m"
colourGrey = "\033[90m"
2026-08-03 19:42:05 +02:00
)
2026-09-17 20:33:18 +02:00
// isTTY reports whether the writer is a terminal (for colour output).
2026-08-03 19:42:05 +02:00
func isTTY ( w io . Writer ) bool {
if f , ok := w .( * os . File ); ok {
stat , _ := f . Stat ()
return ( stat . Mode () & os . ModeCharDevice ) != 0
}
return false
}
2026-07-06 09:49:50 +02:00
func usage ( w io . Writer ) {
2026-08-03 19:42:05 +02:00
useColor := isTTY ( w )
2026-09-19 23:49:19 +02:00
bold , cyan , yellow , grey , reset := "" , "" , "" , "" , ""
2026-08-03 19:42:05 +02:00
if useColor {
2026-09-19 23:49:19 +02:00
bold , cyan , yellow , grey , reset = colourBold , colourCyan , colourYellow , colourGrey , colourReset
2026-08-03 19:42:05 +02:00
}
2026-07-13 19:50:38 +02:00
2026-09-16 22:53:01 +02:00
fmt . Fprintf ( w , "%sgasm %s%s: developer tooling for Go's Plan 9 assembler (GAsm)%s\n\n" , bold , version (), reset , reset )
2026-08-03 19:42:05 +02:00
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" )
2026-07-06 09:49:50 +02:00
2026-08-03 19:42:05 +02:00
fmt . Fprintf ( w , "%sUsage:%s\n" , yellow , reset )
fmt . Fprintf ( w , " gasm <command> [arguments]\n" )
fmt . Fprintf ( w , " gasm [flags]\n\n" )
2026-07-13 19:50:38 +02:00
2026-08-03 19:42:05 +02:00
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" },
2026-08-21 00:50:31 +02:00
{ "asm" , "assemble .s files to machine code (amd64, arm64, riscv64, loong64)" },
2026-09-14 18:47:08 +02:00
{ "dis" , "disassemble machine code (raw bytes or an assembled .s file)" },
2026-08-21 00:50:31 +02:00
{ "verify" , "JIT-assemble and run dynamic checks (amd64, arm64, riscv64, loong64)" },
{ "debug" , "interactive source-level debugger (amd64, arm64, riscv64, loong64)" },
2026-08-05 14:23:00 +02:00
{ "diff" , "compare machine code of two .s files" },
2026-08-05 16:41:00 +02:00
{ "profile" , "show basic-block structure of functions" },
2026-08-29 13:52:32 +02:00
{ "audit-instructions" , "diff the encoder against the Go toolchain's name table" },
{ "scaffold" , "generate a differential test skeleton for a kernel file" },
2026-08-03 19:42:05 +02:00
{ "lsp" , "run the language server over stdio" },
{ "version" , "print the version (same as --version)" },
}
for _ , c := range commands {
2026-09-19 23:49:19 +02:00
fmt . Fprintf ( w , " %s%-10s%s %s%s%s\n" , cyan , c . name , reset , grey , c . desc , reset )
2026-08-03 19:42:05 +02:00
}
2026-07-13 19:50:38 +02:00
2026-08-03 19:42:05 +02:00
fmt . Fprintf ( w , "\n%sFlags:%s\n" , yellow , reset )
2026-09-19 23:49:19 +02:00
fmt . Fprintf ( w , " %s-h, --help%s %sshow this help%s\n" , cyan , reset , grey , reset )
fmt . Fprintf ( w , " %s-V, --version%s %sprint the version%s\n" , cyan , reset , grey , reset )
2026-07-13 19:50:38 +02:00
2026-08-03 19:42:05 +02:00
fmt . Fprintf ( w , "\nRun \"gasm <command> -h\" for a command's usage and flags.\n\n" )
2026-07-13 19:50:38 +02:00
2026-08-03 19:42:05 +02:00
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 != "" {
2026-09-19 23:49:19 +02:00
fmt . Fprintf ( w , " %s%s%s %s%s%s\n" , cyan , e . cmd , reset , grey , e . desc , reset )
2026-08-03 19:42:05 +02:00
} else {
fmt . Fprintf ( w , " %s%s%s\n" , cyan , e . cmd , reset )
}
}
2026-07-06 09:49:50 +02:00
}
2026-07-13 19:50:38 +02:00
// 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
}
2026-07-06 09:49:50 +02:00
// readSource returns the contents of path, or stdin when path is "-".
2026-08-30 11:27:54 +02:00
// 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
}
2026-07-06 09:49:50 +02:00
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
}
2026-09-20 14:25:47 +02:00
// includeDirs collects repeatable -I flags: the directories searched for
// #include files during macro expansion and include splicing.
type includeDirs [] string
func ( d * includeDirs ) String () string { return strings . Join ( * d , "," ) }
func ( d * includeDirs ) Set ( v string ) error {
* d = append ( * d , v )
return nil
}
2026-07-06 09:49:50 +02:00
func cmdTokens ( args [] string ) int {
2026-07-13 19:50:38 +02:00
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.
` )
2026-07-06 09:49:50 +02:00
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 {
2026-07-13 19:50:38 +02:00
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.
` )
2026-07-06 09:49:50 +02:00
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 ++
}
}
2026-09-14 18:22:00 +02:00
fmt . Printf ( "%s: OK, %d declarations, %d functions\n" , path , len ( file . Decls ), funcs )
2026-07-06 09:49:50 +02:00
return 0
}
func cmdFmt ( args [] string ) int {
2026-09-14 18:47:08 +02:00
fs := newCommand ( "fmt" , "gasm fmt [-w|-l|-d] [path...]" , `
2026-07-13 19:50:38 +02:00
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.
2026-09-14 18:22:00 +02:00
With no paths, or a directory path, every .s file below it is reformatted
2026-07-13 19:50:38 +02:00
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.
2026-09-14 18:47:08 +02:00
-l and -d rewrite nothing: -l prints the paths whose formatting differs
from gasm's (empty output means everything is formatted, which is what a CI
check wants), -d prints the diffs. They are mutually exclusive.
2026-07-13 19:50:38 +02:00
` )
write := fs . Bool ( "w" , false , "write result to the source file" )
2026-09-14 18:47:08 +02:00
list := fs . Bool ( "l" , false , "list files whose formatting differs from gasm's" )
diffMode := fs . Bool ( "d" , false , "print diffs instead of rewriting files" )
2026-07-13 19:50:38 +02:00
fs . Parse ( args )
2026-09-14 18:47:08 +02:00
if * list && * diffMode {
fmt . Fprintln ( os . Stderr , "gasm fmt: -l and -d are mutually exclusive" )
return 2
}
2026-07-12 21:24:41 +02:00
// 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.
2026-07-13 19:50:38 +02:00
paths := fs . Args ()
2026-07-12 21:24:41 +02:00
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 )
2026-07-06 09:49:50 +02:00
}
rc := 0
2026-07-12 21:24:41 +02:00
for _ , path := range files {
2026-07-06 09:49:50 +02:00
src , err := readSource ( path )
if err != nil {
fmt . Fprintln ( os . Stderr , "gasm:" , err )
rc = 1
continue
}
2026-08-29 17:12:53 +02:00
out := format . Source ( src )
2026-09-14 18:47:08 +02:00
if * list || * diffMode {
if out != src {
if * list {
fmt . Println ( path )
} else {
fmt . Print ( unifiedDiff ( path , strings . Split ( src , "\n" ), strings . Split ( out , "\n" )))
}
}
continue
}
2026-07-12 21:24:41 +02:00
if dirMode || * write {
2026-07-06 09:49:50 +02:00
if out != src {
if err := os . WriteFile ( path , [] byte ( out ), 0 o644 ); err != nil {
fmt . Fprintln ( os . Stderr , "gasm:" , err )
rc = 1
2026-07-12 21:24:41 +02:00
continue
}
if dirMode {
fmt . Println ( path )
2026-07-06 09:49:50 +02:00
}
}
continue
}
fmt . Print ( out )
}
return rc
}
2026-07-12 21:24:41 +02:00
// asmFiles collects the .s files below dir, skipping directories whose name
2026-09-14 18:22:00 +02:00
// starts with "." or "_", as the go tooling does, which keeps .git and
2026-07-12 21:24:41 +02:00
// 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
}
2026-07-06 09:49:50 +02:00
func cmdLint ( args [] string ) int {
2026-07-13 19:50:38 +02:00
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.
` )
2026-07-06 09:49:50 +02:00
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 {}
2026-08-29 15:40:31 +02:00
for code := range strings . SplitSeq ( * disable , "," ) {
2026-07-06 09:49:50 +02:00
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 {
2026-07-13 19:50:38 +02:00
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.
` )
2026-07-06 09:49:50 +02:00
fs . Parse ( args )
srv := lsp . New ( os . Stdin , os . Stdout )
2026-09-16 22:53:01 +02:00
srv . SetVersion ( version ())
2026-07-06 09:49:50 +02:00
if err := srv . Run (); err != nil {
fmt . Fprintln ( os . Stderr , "gasm lsp:" , err )
return 1
}
return 0
}
func cmdAsm ( args [] string ) int {
2026-09-20 22:02:19 +02:00
fs := newCommand ( "asm" , "gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-GOOS os] [-o out] <file>" , `
2026-08-07 22:43:40 +02:00
Assemble FILE without the Go toolchain: every TEXT function is encoded to
machine code and printed as a hex dump. Supported architectures: amd64
2026-08-20 22:55:49 +02:00
(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).
2026-07-16 20:52:20 +02:00
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
2026-08-07 22:20:26 +02:00
section into one self-consistent image; elf emits a relocatable object
2026-09-20 01:40:51 +02:00
(.text/.data sections, a symbol table and one relocation per static-symbol
reference, in the architecture's own form: R_X86_64_PC32 on amd64,
R_AARCH64_*, R_RISCV_* or R_LARCH_* on the others) 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: the object preamble is captured from go tool asm
and the format version from go version).
2026-09-20 19:14:43 +02:00
A file that includes go_asm.h gets that header generated automatically from
the package it lives in (the .go files beside it, type-checked for the
target architecture, the toolchain's own defines), so GOROOT assembly
2026-09-20 22:02:19 +02:00
assembles without a compiler. -GOOS selects the type-checking GOOS for
that header: a GOOS-specific file (sys_darwin_arm64.s) needs its platform's
defines, which a header from the ambient GOOS silently omits. A package
that has no Go files for the target or does not type-check is a hard error
naming the package.
2026-07-13 19:50:38 +02:00
` )
2026-07-16 20:52:20 +02:00
out := fs . String ( "o" , "" , "write the output to this file" )
2026-08-07 22:20:26 +02:00
format := fs . String ( "format" , "raw" , "output format: raw (concatenated image), elf or goobj (Go object)" )
2026-07-17 18:57:04 +02:00
pkg := fs . String ( "p" , "" , "package path for --format goobj (qualifies the exported symbols)" )
2026-09-19 19:20:47 +02:00
archName := fs . String ( "GOARCH" , "" , "target architecture: amd64, arm64, riscv64 or loong64 (overrides the file-name suffix)" )
2026-09-20 22:02:19 +02:00
goosName := fs . String ( "GOOS" , "" , "operating system for go_asm.h generation: a GOOS go/build recognises (default: the host's)" )
2026-09-20 14:25:47 +02:00
var dirs includeDirs
fs . Var ( & dirs , "I" , "directory to search for #include files (may be repeated)" )
2026-07-06 09:49:50 +02:00
fs . Parse ( args )
if fs . NArg () != 1 {
2026-09-20 22:02:19 +02:00
fmt . Fprintln ( os . Stderr , "usage: gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-GOOS os] [-o out] <file>" )
2026-07-06 09:49:50 +02:00
return 2
}
2026-09-19 23:49:19 +02:00
// The format is validated before anything else, so a bogus value exits 2
// with or without -o instead of silently dumping the hex of a raw image.
switch * format {
case "raw" , "elf" , "goobj" :
default :
fmt . Fprintf ( os . Stderr , "gasm asm: unknown format %q (want raw, elf or goobj)\n" , * format )
return 2
}
2026-07-06 09:49:50 +02:00
path := fs . Arg ( 0 )
2026-08-02 00:18:00 +02:00
targetArch := arch . FromFilename ( path )
2026-09-19 19:20:47 +02:00
if * archName != "" {
a , err := auditArch ( * archName )
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm asm: %v\n" , err )
return 2
}
targetArch = a
}
2026-09-20 22:02:19 +02:00
goos := ""
if * goosName != "" {
g , err := resolveGOOS ( * goosName )
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm asm: %v\n" , err )
return 2
}
goos = g
}
2026-07-06 09:49:50 +02:00
src , err := readSource ( path )
if err != nil {
fmt . Fprintln ( os . Stderr , "gasm:" , err )
return 1
2026-09-20 19:14:43 +02:00
}
// A file that includes go_asm.h cannot assemble without the package's
// defines, and without a compiler nothing else has generated them, so
// gasm produces the equivalent itself: automatic, because the compiler
// behaves the same way and a flag would only ever be forgotten. A
// generation failure is fatal and names the package: assembling against
// a missing header would fail later with a bare "undefined" instead.
// A go_asm.h that already resolves (placed by hand, or passed with -I)
// is left alone.
if needsGoAsmHeader ( src ) && ! goAsmHeaderResolved ( filepath . Dir ( path ), dirs ) {
2026-09-20 22:02:19 +02:00
hdrDir , cleanup , err := ensureGoAsmHeader ( path , targetArch , goos , nil )
2026-09-20 19:14:43 +02:00
if err != nil {
fmt . Fprintln ( os . Stderr , "gasm asm:" , err )
return 1
}
defer cleanup ()
dirs = append ( dirs , hdrDir )
2026-07-06 09:49:50 +02:00
}
2026-09-21 21:06:42 +02:00
f , errs := parser . ParseWithOptions ( path , src , parser . Options { Expand : true , IncludeDirs : dirs , Predefines : platformPredefinesFor ( string ( targetArch ), goos )})
2026-07-06 09:49:50 +02:00
for _ , e := range errs {
fmt . Fprintf ( os . Stderr , "%s: %v\n" , path , e )
}
if len ( errs ) > 0 {
return 1
}
2026-09-21 21:35:08 +02:00
img , err := assembleFile ( targetArch , f , goos )
2026-07-09 15:56:03 +02:00
if err != nil {
fmt . Fprintf ( os . Stderr , "%s: %v\n" , path , err )
return 1
}
2026-09-19 19:58:43 +02:00
if len ( img . Funcs ) == 0 && len ( img . Data ) == 0 {
// A file with neither code nor data assembles to nothing, which is
// almost always a wrong architecture rather than an intent.
fmt . Fprintln ( os . Stderr , "gasm asm: no assemblable TEXT functions or GLOBL data found" )
2026-07-09 15:56:03 +02:00
return 1
}
2026-09-19 23:49:19 +02:00
// Without -o the hex dump on stdout is the output; with -o the file is,
// and the dump is skipped, as the -o help text promises.
if * out == "" {
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 ()
2026-07-06 09:49:50 +02:00
}
}
2026-09-19 23:49:19 +02:00
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 ])
2026-07-09 15:56:03 +02:00
}
2026-09-19 23:49:19 +02:00
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 ()
2026-07-09 15:56:03 +02:00
}
}
2026-07-06 09:49:50 +02:00
}
if * out != "" {
2026-07-16 20:52:20 +02:00
var obj [] byte
var err error
var kind string
switch * format {
case "raw" :
if len ( img . Externals ) > 0 {
2026-08-07 22:20:26 +02:00
fmt . Fprintf ( os . Stderr , "gasm asm: external symbol %q needs an object file (use --format elf)\n" , img . Externals [ 0 ])
2026-07-16 20:52:20 +02:00
return 1
}
obj , kind = img . Bytes (), "raw image"
case "elf" :
2026-08-13 11:24:44 +02:00
switch targetArch {
case arch . RISCV :
2026-08-03 08:51:00 +02:00
obj , err = img . ELFRISCVObject ()
2026-08-13 11:24:44 +02:00
case arch . LOONG64 :
obj , err = img . ELFLOONG64Object ()
2026-08-20 13:32:52 +02:00
case arch . ARM64 :
obj , err = img . ELFAARCH64Object ()
2026-08-13 11:24:44 +02:00
default :
2026-08-03 08:51:00 +02:00
obj , err = img . ELFObject ()
}
2026-07-16 20:52:20 +02:00
kind = "ELF object"
2026-07-17 18:57:04 +02:00
case "goobj" :
2026-08-13 11:24:44 +02:00
switch targetArch {
case arch . RISCV :
2026-08-05 09:27:00 +02:00
obj , err = img . GOObjectRISCV ( * pkg , path )
2026-08-13 11:24:44 +02:00
case arch . LOONG64 :
obj , err = img . GOObjectLOONG64 ( * pkg , path )
2026-08-20 13:32:52 +02:00
case arch . ARM64 :
obj , err = img . GOObjectAARCH64 ( * pkg , path )
2026-08-13 11:24:44 +02:00
default :
2026-08-05 09:27:00 +02:00
obj , err = img . GOObject ( * pkg , path )
}
2026-07-17 18:57:04 +02:00
kind = "Go object"
2026-07-16 20:52:20 +02:00
}
if err != nil {
2026-07-06 09:49:50 +02:00
fmt . Fprintln ( os . Stderr , "gasm asm:" , err )
return 1
}
2026-07-16 20:52:20 +02:00
if err := os . WriteFile ( * out , obj , 0 o644 ); err != nil {
fmt . Fprintln ( os . Stderr , "gasm asm:" , err )
return 1
}
fmt . Printf ( "wrote %d bytes to %s (%s)\n" , len ( obj ), * out , kind )
2026-07-06 09:49:50 +02:00
}
return 0
}
2026-07-22 09:28:11 +02:00
2026-08-05 14:23:00 +02:00
// cmdDiff compares the machine code of two assembly files.
func cmdDiff ( args [] string ) int {
2026-09-20 14:25:47 +02:00
set := newCommand ( "diff" , "gasm diff [-GOARCH arch] [-I dir] <file1.s> <file2.s>" , `
2026-08-05 14:23:00 +02:00
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.
2026-08-05 20:20:47 +02:00
Use --map to compare functions whose names differ between the files,
e.g. --map wideCopyAVX2=wideCopyAVX512 pairs the two regardless of suffix.
2026-08-05 14:23:00 +02:00
` )
2026-08-29 17:12:53 +02:00
mapSpec := set . String ( "map" , "" , "comma-separated old=new pairs to match functions with different names" )
2026-09-19 19:20:47 +02:00
archName := set . String ( "GOARCH" , "" , "target architecture for both files: amd64, arm64, riscv64 or loong64" )
2026-09-20 14:25:47 +02:00
var dirs includeDirs
set . Var ( & dirs , "I" , "directory to search for #include files (may be repeated)" )
2026-08-29 17:12:53 +02:00
set . Parse ( args )
if set . NArg () != 2 {
2026-09-20 14:25:47 +02:00
fmt . Fprintln ( os . Stderr , "usage: gasm diff [-GOARCH arch] [-I dir] <file1.s> <file2.s>" )
2026-08-05 14:23:00 +02:00
return 2
}
2026-08-29 17:12:53 +02:00
path1 , path2 := set . Arg ( 0 ), set . Arg ( 1 )
2026-09-19 19:20:47 +02:00
forced := arch . Unknown
if * archName != "" {
a , err := auditArch ( * archName )
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm diff: %v\n" , err )
return 2
}
forced = a
}
2026-08-05 14:23:00 +02:00
2026-08-05 20:20:47 +02:00
// Parse the name mapping (file1 name → file2 name).
nameMap := make ( map [ string ] string )
if * mapSpec != "" {
2026-08-29 15:40:31 +02:00
for pair := range strings . SplitSeq ( * mapSpec , "," ) {
2026-08-05 20:20:47 +02:00
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
}
}
2026-08-05 14:23:00 +02:00
// Assemble both files.
2026-09-20 14:25:47 +02:00
img1 , err := assemblePath ( path1 , forced , dirs )
2026-08-05 14:23:00 +02:00
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm diff: %s: %v\n" , path1 , err )
return 1
}
2026-09-20 14:25:47 +02:00
img2 , err := assemblePath ( path2 , forced , dirs )
2026-08-05 14:23:00 +02:00
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm diff: %s: %v\n" , path2 , err )
return 1
}
2026-08-05 20:20:47 +02:00
// Compare functions by name, honouring the --map overrides.
2026-08-05 14:23:00 +02:00
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 ]
}
2026-08-05 20:20:47 +02:00
// 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 )
2026-08-05 14:23:00 +02:00
diffs := 0
for name , code1 := range funcs1 {
2026-08-05 20:20:47 +02:00
target := name
if mapped , ok := nameMap [ name ]; ok {
target = mapped
}
code2 , ok := funcs2 [ target ]
2026-08-05 14:23:00 +02:00
if ! ok {
fmt . Printf ( "%s: only in %s\n" , name , path1 )
diffs ++
continue
}
2026-08-05 20:20:47 +02:00
matched2 [ target ] = true
label := name
if target != name {
label = name + " → " + target
}
2026-08-05 14:23:00 +02:00
if ! bytes . Equal ( code1 , code2 ) {
2026-08-05 20:20:47 +02:00
fmt . Printf ( "%s: DIFFERS (%d vs %d bytes)\n" , label , len ( code1 ), len ( code2 ))
2026-08-05 14:23:00 +02:00
printByteDiff ( code1 , code2 )
diffs ++
} else {
2026-08-05 20:20:47 +02:00
fmt . Printf ( "%s: identical (%d bytes)\n" , label , len ( code1 ))
2026-08-05 14:23:00 +02:00
}
}
for name := range funcs2 {
2026-08-05 20:20:47 +02:00
if ! matched2 [ name ] {
2026-08-05 14:23:00 +02:00
fmt . Printf ( "%s: only in %s\n" , name , path2 )
diffs ++
}
}
if diffs == 0 {
fmt . Println ( "all functions identical" )
return 0
}
return 1
}
2026-09-21 21:06:42 +02:00
// platformPredefines mirrors the go command's assembler invocation, which
// defines GOOS_<goos> and GOARCH_<arch> as -D macros: GOROOT headers
// (go_tls.h, asm_riscv64.h) select their platform blocks with #ifdef on
// exactly those names, so an assembler without them cannot see the platform
// definitions at all.
func platformPredefines ( goarch , goos string ) map [ string ] string {
return map [ string ] string {
"GOARCH_" + goarch : "1" ,
"GOOS_" + goos : "1" ,
}
}
// platformPredefinesFor resolves the ambient GOOS the way a build would: a
// file whose name carries one (sys_darwin_arm64.s) is compiled for that GOOS
// and nothing else.
func platformPredefinesFor ( goarch string , fileGoos string ) map [ string ] string {
goos := fileGoos
if goos == "" {
goos = runtime . GOOS
}
return platformPredefines ( goarch , goos )
}
2026-08-07 22:43:40 +02:00
// assembleFile assembles a parsed file for the given architecture and returns the image.
2026-09-21 21:35:08 +02:00
func assembleFile ( targetArch arch . Arch , f * ast . File , goos string ) ( * asm . Image , error ) {
2026-08-07 22:43:40 +02:00
switch targetArch {
case arch . AMD64 :
2026-09-21 21:35:08 +02:00
return asm . AssembleFile ( f , asm . WithGOOS ( goos ))
2026-08-07 22:43:40 +02:00
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 )
}
}
2026-09-20 14:25:47 +02:00
// assemblePath reads, preprocesses, parses and assembles a file (used by
// cmdDiff). A non-Unknown forced architecture overrides the file-name
// suffix.
func assemblePath ( path string , forced arch . Arch , dirs includeDirs ) ( * asm . Image , error ) {
2026-08-05 14:23:00 +02:00
src , err := readSource ( path )
if err != nil {
return nil , err
}
2026-09-21 21:06:42 +02:00
target := forced
if target == arch . Unknown {
target = arch . FromFilename ( path )
}
f , errs := parser . ParseWithOptions ( path , src , parser . Options { Expand : true , IncludeDirs : dirs , Predefines : platformPredefinesFor ( string ( target ), "" )})
2026-08-05 14:23:00 +02:00
for _ , e := range errs {
fmt . Fprintf ( os . Stderr , "%s: %v\n" , path , e )
}
if len ( errs ) > 0 {
return nil , fmt . Errorf ( "parse errors" )
}
2026-09-21 21:35:08 +02:00
return assembleFile ( target , f , "" )
2026-08-05 14:23:00 +02:00
}
// printByteDiff shows the first few byte differences between two code blocks.
func printByteDiff ( a , b [] byte ) {
2026-08-29 15:40:31 +02:00
maxLen := min ( len ( b ), len ( a ))
2026-08-05 14:23:00 +02:00
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 ))
}
}
2026-08-05 16:41:00 +02:00
// cmdProfile shows the basic-block structure of functions in an assembly file.
func cmdProfile ( args [] string ) int {
2026-08-29 17:12:53 +02:00
flagSet := newCommand ( "profile" , "gasm profile <file.s>" , `
2026-08-05 16:41:00 +02:00
Show the basic-block structure of functions in an assembly file.
Lists each function's labels, their offsets, and the block boundaries.
2026-09-20 01:40:51 +02:00
This is the static structure; for runtime execution counts use
gasm debug --cover, and for input coverage gasm verify --fuzz.
2026-08-05 16:41:00 +02:00
` )
2026-08-29 17:12:53 +02:00
flagSet . Parse ( args )
if flagSet . NArg () != 1 {
2026-08-05 16:41:00 +02:00
fmt . Fprintln ( os . Stderr , "usage: gasm profile <file.s>" )
return 2
}
2026-08-29 17:12:53 +02:00
path := flagSet . Arg ( 0 )
2026-08-05 16:41:00 +02:00
// 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
}
2026-09-14 18:22:00 +02:00
// cmdVerifyNonJIT handles the verify subcommand for files whose architecture
// the host cannot execute: only the ground-truth comparison and the static
// profile are available there. Relocation sites are masked before the byte
// comparison, as the toolchain leaves them zero for the linker.
func cmdVerifyNonJIT ( path string , targetArch arch . Arch , groundTruth , profile bool ) int {
2026-08-03 08:51:00 +02:00
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
}
2026-09-21 21:35:08 +02:00
img , err := assembleFile ( targetArch , f , "" )
2026-08-03 08:51:00 +02:00
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm verify: %v\n" , err )
return 1
}
if groundTruth {
2026-09-14 18:22:00 +02:00
var gt map [ string ][] byte
switch targetArch {
case arch . RISCV :
gt , err = verify . GroundTruthRISCV ( path )
case arch . LOONG64 :
gt , err = verify . GroundTruthLOONG64 ( path )
case arch . ARM64 :
gt , err = verify . GroundTruthARM64 ( path )
default :
gt , err = verify . GroundTruth ( path )
}
2026-08-03 08:51:00 +02:00
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm verify: ground truth: %v\n" , err )
return 1
}
2026-09-14 18:22:00 +02:00
matched , total , diffs := compareGroundTruth ( img , gt )
if diffs > 0 {
printCodeDiff ( img , gt )
2026-08-03 08:51:00 +02:00
}
fmt . Printf ( "%s: %d/%d matched\n" , path , matched , total )
2026-09-14 18:22:00 +02:00
if matched < total || diffs > 0 {
2026-08-03 08:51:00 +02:00
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
}
2026-09-14 18:22:00 +02:00
// compareGroundTruth compares the image's functions against the go tool asm
// output byte-for-byte, masking relocation sites (disp32 fields the Go linker
// fills at link time). It prints one line per function and returns the
// matched and compared counts plus the number of functions with byte diffs.
func compareGroundTruth ( img * asm . Image , gt map [ string ][] byte ) ( matched , total , diffs int ) {
2026-08-13 11:24:44 +02:00
for _ , fn := range img . Funcs {
2026-09-14 18:22:00 +02:00
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
}
}
2026-09-20 00:38:24 +02:00
// The toolchain pads text symbols to 16-byte boundaries with
// zeros, so a function whose size is not a multiple of 16
// carries trailing zeros in the ground truth that are not part
// of the encoding. Compare up to the shorter side and require
// the remainder of whichever is longer to be zero, so padding
// never masks a real difference.
cmpLen := min ( len ( gasmCmp ), len ( goCmp ))
equal := bytes . Equal ( gasmCmp [: cmpLen ], goCmp [: cmpLen ])
if equal {
for _ , b := range gasmCmp [ cmpLen :] {
if b != 0 {
equal = false
break
}
}
}
if equal {
for _ , b := range goCmp [ cmpLen :] {
if b != 0 {
equal = false
break
}
}
}
if equal {
2026-09-14 18:22:00 +02:00
matched ++
2026-09-20 00:38:24 +02:00
switch {
case len ( fn . Relocs ) > 0 && len ( goCmp ) > cmpLen :
fmt . Printf ( " %s: MATCH (%d bytes, %d relocs masked, %d padding)\n" , fn . Name , fn . Size , len ( fn . Relocs ), len ( goCmp ) - cmpLen )
case len ( fn . Relocs ) > 0 :
2026-09-14 18:22:00 +02:00
fmt . Printf ( " %s: MATCH (%d bytes, %d relocs masked)\n" , fn . Name , fn . Size , len ( fn . Relocs ))
2026-09-20 00:38:24 +02:00
case len ( goCmp ) > cmpLen :
fmt . Printf ( " %s: MATCH (%d bytes, %d padding)\n" , fn . Name , fn . Size , len ( goCmp ) - cmpLen )
default :
2026-09-14 18:22:00 +02:00
fmt . Printf ( " %s: MATCH (%d bytes)\n" , fn . Name , fn . Size )
}
} else {
diffs ++
fmt . Printf ( " %s: MISMATCH (%d vs %d bytes)\n" , fn . Name , fn . Size , len ( goCode ))
}
2026-08-13 11:24:44 +02:00
}
2026-09-14 18:22:00 +02:00
return matched , total , diffs
2026-08-13 11:24:44 +02:00
}
2026-09-14 18:22:00 +02:00
// printCodeDiff shows a 16-byte hex dump per function whose gasm bytes differ
// from the go tool asm output.
func printCodeDiff ( img * asm . Image , gt map [ string ][] byte ) {
2026-08-20 13:32:52 +02:00
for _ , fn := range img . Funcs {
2026-09-14 18:22:00 +02:00
gasmCode := img . Code [ fn . Offset : fn . Offset + fn . Size ]
goCode , ok := gt [ fn . Name ]
if ! ok || bytes . Equal ( gasmCode , goCode ) {
continue
}
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 )
}
2026-08-20 13:32:52 +02:00
}
}
2026-07-22 09:28:11 +02:00
func cmdVerify ( args [] string ) int {
2026-08-29 17:12:53 +02:00
set := newCommand ( "verify" , "gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <file.s>" , `
2026-07-22 09:28:11 +02:00
Assemble FILE (amd64), map it into executable memory and report the available
functions. This confirms the assembled image is self-consistent (no
2026-09-14 18:22:00 +02:00
unresolved external symbols) and executable, the prerequisite for dynamic
2026-07-22 09:28:11 +02:00
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.
2026-07-26 16:13:11 +02:00
2026-08-31 12:13:43 +02:00
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
2026-09-20 00:57:02 +02:00
checks run when the host matches the file's architecture, on all four
architectures.
2026-07-26 16:13:11 +02:00
2026-08-05 20:46:39 +02:00
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.
2026-07-26 16:13:11 +02:00
With -profile, the static basic-block structure is listed for each function.
2026-08-05 20:46:39 +02:00
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.
2026-09-20 01:40:51 +02:00
decoders) that crash on random input but should succeed on valid data. The
function named must be NOSPLIT: a function with a stack frame is refused.
2026-08-30 21:23:43 +02:00
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.
2026-07-22 09:28:11 +02:00
` )
2026-08-29 17:12:53 +02:00
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" )
2026-08-30 21:23:43 +02:00
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" )
2026-08-29 17:12:53 +02:00
set . Parse ( args )
if set . NArg () != 1 {
2026-08-05 20:46:39 +02:00
fmt . Fprintln ( os . Stderr , "usage: gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <file.s>" )
2026-07-22 09:28:11 +02:00
return 2
}
2026-08-29 17:12:53 +02:00
path := set . Arg ( 0 )
2026-08-03 08:51:00 +02:00
targetArch := arch . FromFilename ( path )
2026-08-31 12:13:43 +02:00
// JIT execution runs when the host CPU matches the kernel's
2026-09-20 00:57:02 +02:00
// architecture; every trampoline is validated end to end under
// qemu-user emulation (the loong64 one included, via the raw-address
// leave handoff).
if targetArch != hostArch () {
2026-09-19 23:49:19 +02:00
// No JIT on this host: ground truth and profile remain available for
// every architecture, because cmdVerifyNonJIT assembles and compares
2026-09-20 00:57:02 +02:00
// against the toolchain without executing anything.
2026-08-30 11:27:54 +02:00
switch targetArch {
2026-09-19 23:49:19 +02:00
case arch . AMD64 , arch . RISCV , arch . LOONG64 , arch . ARM64 :
2026-09-14 18:22:00 +02:00
return cmdVerifyNonJIT ( path , targetArch , * groundTruth , * profile )
2026-08-30 11:27:54 +02:00
default :
fmt . Fprintln ( os . Stderr , "gasm verify: unsupported architecture" )
return 1
}
2026-08-03 08:51:00 +02:00
}
2026-07-22 09:28:11 +02:00
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
2026-07-30 12:55:54 +02:00
2026-08-05 20:46:39 +02:00
// Single-function call mode: invoke one function with user-supplied buffers.
if * call != "" {
2026-08-29 14:03:45 +02:00
return cmdVerifyCall ( k , path , * call , * bufSpec , * scalarSpec , * repeat )
2026-08-05 20:46:39 +02:00
}
2026-08-30 21:23:43 +02:00
// 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 )
}
2026-08-30 11:00:40 +02:00
// 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 != "" {
2026-08-01 18:08:10 +02:00
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 )
2026-08-30 11:00:40 +02:00
sig , ok := sigs [ fuzzOne ]
2026-08-01 18:08:10 +02:00
if ! ok {
2026-08-30 11:00:40 +02:00
fmt . Printf ( "%s: no signature\n" , fuzzOne )
2026-08-01 18:08:10 +02:00
return 0
}
2026-08-30 11:00:40 +02:00
goCode , ok := gt [ fuzzOne ]
2026-08-01 18:08:10 +02:00
if ! ok {
2026-08-30 11:00:40 +02:00
fmt . Printf ( "%s: not in go tool asm\n" , fuzzOne )
2026-08-01 18:08:10 +02:00
return 0
}
2026-08-30 21:23:43 +02:00
var onSave func ( verify . CorpusEntry )
if * saveCorpus != "" {
if err := os . MkdirAll ( * saveCorpus , 0 o755 ); 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 , 0 o644 )
}
}
res := k . FuzzFuncHook ( fuzzOne , sig , goCode , * fuzzN , 42 , onSave )
2026-08-01 18:08:10 +02:00
fmt . Printf ( "%s\n" , res )
if ! res . OK () {
return 1
}
return 0
}
2026-08-24 21:01:32 +02:00
// 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.
2026-08-30 11:00:40 +02:00
// 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 )
2026-08-24 21:01:32 +02:00
if err != nil || ! fl . NoSplit {
2026-08-30 11:00:40 +02:00
fmt . Fprintf ( os . Stderr , "gasm verify: %s: %v\n" , sweepOne , err )
2026-08-24 21:01:32 +02:00
return 1
}
2026-08-30 11:00:40 +02:00
msgs , failed := runSweepChecks ( k , path , sweepOne , fl , * smoke , * abi , * abiN )
2026-08-24 21:01:32 +02:00
for _ , m := range msgs {
fmt . Println ( m )
}
if failed {
return 1
}
return 0
}
2026-07-30 12:55:54 +02:00
// 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
}
2026-09-14 18:22:00 +02:00
matched , total , diffs := compareGroundTruth ( k . Image (), gt )
if diffs > 0 {
printCodeDiff ( k . Image (), gt )
rc = 1
2026-07-30 12:55:54 +02:00
}
fmt . Printf ( "ground truth: %d/%d functions byte-identical\n" , matched , total )
2026-09-14 18:22:00 +02:00
if matched < total || diffs > 0 {
2026-07-30 12:55:54 +02:00
rc = 1
}
}
2026-07-31 17:09:50 +02:00
// 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 {
2026-09-14 18:22:00 +02:00
if _ , ok := sigs [ name ]; ! ok {
2026-07-31 17:09:50 +02:00
fmt . Printf ( " %s: SKIP (no // func signature)\n" , name )
continue
}
2026-09-14 18:22:00 +02:00
if _ , ok := gt [ name ]; ! ok {
2026-07-31 17:09:50 +02:00
fmt . Printf ( " %s: SKIP (not in go tool asm output)\n" , name )
continue
}
2026-08-01 18:08:10 +02:00
// Run in a subprocess: if the function crashes on random
// input (partial function), we report it and move on.
2026-08-30 21:23:43 +02:00
var extra [] string
if * saveCorpus != "" {
extra = append ( extra , "-save-corpus" , * saveCorpus )
}
res := fuzzInSubprocess ( path , name , * fuzzN , extra ... )
2026-08-01 18:08:10 +02:00
if res != "" {
fmt . Printf ( " %s\n" , res )
if strings . Contains ( res , "MISMATCH" ) {
rc = 1
}
2026-07-31 17:09:50 +02:00
}
fuzzed ++
}
fmt . Printf ( "fuzz: %d functions tested, %d iterations each\n" , fuzzed , * fuzzN )
}
2026-08-21 01:10:14 +02:00
// Print function info and profile.
2026-07-22 09:28:11 +02:00
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 )
2026-07-26 16:13:11 +02:00
if * profile {
blocks , err := k . Blocks ( name )
if err != nil {
fmt . Printf ( " profile: %v\n" , err )
} else {
fmt . Printf ( " blocks: %d\n" , len ( blocks ))
}
}
2026-08-21 01:10:14 +02:00
}
2026-07-26 16:13:11 +02:00
2026-08-24 21:01:32 +02:00
// 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
2026-09-14 18:22:00 +02:00
// function that dereferences them faults, the crash is reported as a
2026-08-24 21:01:32 +02:00
// CRASH line instead of killing this process (mirrors fuzzInSubprocess).
2026-08-21 01:10:14 +02:00
if * smoke || * abi {
type checkResult struct {
name string
msg string
fail bool
2026-07-22 09:28:11 +02:00
}
2026-08-21 01:10:14 +02:00
var mu sync . Mutex
var results [] checkResult
sem := make ( chan struct {}, runtime . NumCPU ())
var wg sync . WaitGroup
2026-07-26 16:13:11 +02:00
2026-08-21 01:10:14 +02:00
for _ , name := range names {
fl , _ := k . Func ( name )
if ! fl . NoSplit {
continue
}
wg . Add ( 1 )
2026-08-24 21:01:32 +02:00
go func ( name string ) {
2026-08-21 01:10:14 +02:00
defer wg . Done ()
sem <- struct {}{}
defer func () { <- sem }()
2026-08-24 21:01:32 +02:00
msg , fail := sweepInSubprocess ( path , name , * smoke , * abi , * abiN )
2026-08-21 01:10:14 +02:00
mu . Lock ()
2026-08-24 21:01:32 +02:00
results = append ( results , checkResult { name : name , msg : msg , fail : fail })
2026-08-21 01:10:14 +02:00
mu . Unlock ()
2026-08-24 21:01:32 +02:00
}( name )
2026-08-21 01:10:14 +02:00
}
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 {
2026-08-04 22:26:04 +02:00
rc = 1
}
2026-07-26 16:13:11 +02:00
}
}
2026-07-22 09:28:11 +02:00
}
return rc
}
2026-08-01 18:08:10 +02:00
// fuzzInSubprocess runs the fuzz for a single function in a child process.
2026-08-30 21:23:43 +02:00
// 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
}
2026-08-01 18:08:10 +02:00
// 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.
2026-08-30 21:23:43 +02:00
func fuzzInSubprocess ( path , funcName string , n int , extra ... string ) string {
2026-08-01 18:08:10 +02:00
self , err := os . Executable ()
if err != nil {
return fmt . Sprintf ( "%s: cannot find self: %v" , funcName , err )
}
2026-08-30 21:23:43 +02:00
fuzzChildArgs := append ([] string { "verify" , "-n" , strconv . Itoa ( n )}, extra ... )
fuzzChildArgs = append ( fuzzChildArgs , path )
cmd := exec . Command ( self , fuzzChildArgs ... )
2026-08-30 11:00:40 +02:00
cmd . Env = append ( os . Environ (), "GASM_VERIFY_FUZZ_ONE=" + funcName )
2026-08-01 18:08:10 +02:00
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 () {
2026-09-14 18:22:00 +02:00
return fmt . Sprintf ( "%s: CRASH (%v; partial function, use --ground-truth)" , funcName , ws . Signal ())
2026-08-01 18:08:10 +02:00
}
}
// Non-zero exit without a signal: the fuzz reported mismatches.
2026-08-29 15:40:31 +02:00
lines := strings . SplitSeq ( strings . TrimSpace ( string ( out )), "\n" )
for l := range lines {
2026-08-01 18:08:10 +02:00
if strings . Contains ( l , funcName ) {
return strings . TrimSpace ( l )
}
}
return fmt . Sprintf ( "%s: FAIL (exit %v)" , funcName , err )
}
// Success: extract the result line.
2026-08-29 15:40:31 +02:00
lines := strings . SplitSeq ( strings . TrimSpace ( string ( out )), "\n" )
for l := range lines {
2026-08-01 18:08:10 +02:00
if strings . Contains ( l , funcName ) {
return strings . TrimSpace ( l )
}
}
return strings . TrimSpace ( string ( out ))
}
2026-08-05 20:46:39 +02:00
2026-08-24 21:01:32 +02:00
// 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
2026-09-14 18:22:00 +02:00
// CRASH report instead of dying, the same isolation fuzzInSubprocess
2026-08-24 21:01:32 +02:00
// provides for the fuzz sweep.
func sweepInSubprocess ( path , funcName string , smoke , abi bool , abiN int ) ( string , bool ) {
self , err := os . Executable ()
if err != nil {
2026-09-14 18:22:00 +02:00
return fmt . Sprintf ( " smoke/abi: FAIL: cannot find self: %v" , err ), true
2026-08-24 21:01:32 +02:00
}
2026-08-30 11:00:40 +02:00
args := [] string { "verify" }
2026-08-24 21:01:32 +02:00
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 ... )
2026-08-30 11:00:40 +02:00
cmd . Env = append ( os . Environ (), "GASM_VERIFY_SWEEP_ONE=" + funcName )
2026-08-24 21:01:32 +02:00
out , err := cmd . CombinedOutput ()
if err != nil {
if exitErr , ok := err .( * exec . ExitError ); ok {
ws , ok := exitErr . Sys ().( syscall . WaitStatus )
if ok && ws . Signaled () {
2026-09-14 18:22:00 +02:00
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
2026-08-24 21:01:32 +02:00
}
}
// 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
2026-08-29 15:40:31 +02:00
for l := range strings . SplitSeq ( string ( out ), "\n" ) {
2026-08-24 21:01:32 +02:00
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
2026-09-14 18:22:00 +02:00
// function, the child half of sweepInSubprocess.
2026-08-24 21:01:32 +02:00
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 {
2026-09-14 18:22:00 +02:00
msgs = append ( msgs , fmt . Sprintf ( " smoke: FAIL: %v" , err ))
2026-08-24 21:01:32 +02:00
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 {
2026-09-14 18:22:00 +02:00
msgs = append ( msgs , fmt . Sprintf ( " abi: FAIL: %v" , err ))
2026-08-24 21:01:32 +02:00
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
}
2026-08-29 14:03:45 +02:00
// 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 {
2026-08-05 20:46:39 +02:00
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 )
2026-08-29 14:03:45 +02:00
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
}
2026-08-05 20:46:39 +02:00
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 )
}
}
2026-08-29 14:03:45 +02:00
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 ])
}
}
2026-08-05 20:46:39 +02:00
fmt . Printf ( " args before: %s\n" , hexDump ( args ))
rc := 0
2026-08-29 15:25:15 +02:00
for i := range repeat {
2026-08-05 20:46:39 +02:00
out , err := k . CallFunc ( funcName , args )
if err != nil {
2026-09-14 18:22:00 +02:00
fmt . Printf ( " call %d: FAIL: %v\n" , i + 1 , err )
2026-08-05 20:46:39 +02:00
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
2026-08-29 15:40:31 +02:00
n := min ( len ( buf ), max )
2026-08-05 20:46:39 +02:00
var sb strings . Builder
2026-08-29 15:25:15 +02:00
for i := range n {
2026-08-05 20:46:39 +02:00
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 ""
}