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
|
|
|
|
|
|
|
|
|
|
// 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 (
|
|
|
|
|
"flag"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
2026-07-12 21:24:41 +02:00
|
|
|
"io/fs"
|
2026-07-06 09:49:50 +02:00
|
|
|
"os"
|
2026-07-12 21:24:41 +02:00
|
|
|
"path/filepath"
|
2026-07-06 09:49:50 +02:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"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
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// version is the release version, stamped at build time via
|
|
|
|
|
// -ldflags "-X main.version=…" (defaulting to the current release).
|
2026-07-23 10:12:42 +02:00
|
|
|
var version = "0.18.0"
|
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-07-22 09:28:11 +02:00
|
|
|
case "verify":
|
|
|
|
|
os.Exit(cmdVerify(os.Args[2:]))
|
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-07-13 19:50:38 +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-07-13 19:50:38 +02:00
|
|
|
// cmdVersion prints the release version.
|
|
|
|
|
func cmdVersion() int {
|
|
|
|
|
fmt.Printf("gasm %s\n", version)
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 09:49:50 +02:00
|
|
|
func usage(w io.Writer) {
|
2026-07-13 19:50:38 +02:00
|
|
|
fmt.Fprintf(w, `gasm %s — developer tooling for Go's Plan 9 assembler (GAsm)
|
|
|
|
|
|
|
|
|
|
gasm bundles a lexer, parser, formatter, linter, standalone assembler and
|
|
|
|
|
language server for Plan 9 assembly into one self-contained binary.
|
2026-07-06 09:49:50 +02:00
|
|
|
|
|
|
|
|
Usage:
|
2026-07-13 19:50:38 +02:00
|
|
|
gasm <command> [arguments]
|
|
|
|
|
gasm [flags]
|
|
|
|
|
|
|
|
|
|
Commands:
|
|
|
|
|
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)
|
2026-07-22 09:28:11 +02:00
|
|
|
verify JIT-assemble and run dynamic checks (amd64)
|
2026-07-13 19:50:38 +02:00
|
|
|
lsp run the language server over stdio
|
|
|
|
|
version print the version (same as --version)
|
|
|
|
|
|
|
|
|
|
Flags:
|
|
|
|
|
-h, --help show this help
|
|
|
|
|
-V, --version print the version
|
|
|
|
|
|
|
|
|
|
Run "gasm <command> -h" for a command's usage and flags.
|
|
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
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
|
2026-07-16 20:52:20 +02:00
|
|
|
gasm asm --format elf -o k.o kern_amd64.s
|
2026-07-17 18:57:04 +02:00
|
|
|
gasm asm --format goobj -p pkg/path -o k.o kern_amd64.s
|
2026-07-06 09:49:50 +02:00
|
|
|
`, version)
|
|
|
|
|
}
|
|
|
|
|
|
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 "-".
|
|
|
|
|
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 {
|
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++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("%s: OK — %d declarations, %d functions\n", path, len(file.Decls), funcs)
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func cmdFmt(args []string) int {
|
2026-07-13 19:50:38 +02:00
|
|
|
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)
|
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
|
|
|
|
|
}
|
|
|
|
|
out := format.Source(path, src)
|
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), 0o644); 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
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
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{}
|
|
|
|
|
for _, code := range strings.Split(*disable, ",") {
|
|
|
|
|
if code = strings.TrimSpace(code); code != "" {
|
|
|
|
|
disabled[code] = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
hadError := false
|
|
|
|
|
for _, path := range fs.Args() {
|
|
|
|
|
src, err := readSource(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintln(os.Stderr, "gasm:", err)
|
|
|
|
|
hadError = true
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
file, errs := parser.Parse(path, src)
|
|
|
|
|
for _, e := range errs {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
|
|
|
|
|
hadError = true
|
|
|
|
|
}
|
|
|
|
|
diags := lint.File(file, lint.Config{Arch: arch.FromFilename(path), Disable: disabled})
|
|
|
|
|
for _, d := range diags {
|
|
|
|
|
fmt.Printf("%s:%d:%d: %s: %s [%s]\n", path, d.Pos.Line, d.Pos.Column, d.Severity, d.Message, d.Code)
|
|
|
|
|
if d.Severity == lint.Error {
|
|
|
|
|
hadError = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if hadError {
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func cmdLSP(args []string) int {
|
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)
|
|
|
|
|
if err := srv.Run(); err != nil {
|
|
|
|
|
fmt.Fprintln(os.Stderr, "gasm lsp:", err)
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func cmdAsm(args []string) int {
|
2026-07-17 18:57:04 +02:00
|
|
|
fs := newCommand("asm", "gasm asm [--format raw|elf|macho|goobj] [-p pkg] [-o out] <file>", `
|
2026-07-13 19:50:38 +02:00
|
|
|
Assemble FILE (amd64) without the Go toolchain: every TEXT function is
|
|
|
|
|
encoded to machine code — scalar, VEX/AVX2 and EVEX/AVX-512 instructions,
|
|
|
|
|
FP/SP frame mapping, local labels and file-local static symbols (GLOBL/DATA)
|
2026-07-16 20:52:20 +02:00
|
|
|
resolved RIP-relative — and printed as a hex dump.
|
|
|
|
|
|
|
|
|
|
With -o the output is written to a file instead. The --format flag selects
|
|
|
|
|
what is written: raw (the default) concatenates the functions and the data
|
|
|
|
|
section into one self-consistent image; elf and macho emit a relocatable
|
|
|
|
|
object (.text/.data sections, a symbol table and one PC32 relocation per
|
2026-07-17 18:57:04 +02:00
|
|
|
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).
|
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-07-17 18:57:04 +02:00
|
|
|
format := fs.String("format", "raw", "output format: raw (concatenated image), elf, macho or goobj (Go object)")
|
|
|
|
|
pkg := fs.String("p", "", "package path for --format goobj (qualifies the exported symbols)")
|
2026-07-06 09:49:50 +02:00
|
|
|
fs.Parse(args)
|
|
|
|
|
if fs.NArg() != 1 {
|
2026-07-17 18:57:04 +02:00
|
|
|
fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|macho|goobj] [-p pkg] [-o out] <file>")
|
2026-07-06 09:49:50 +02:00
|
|
|
return 2
|
|
|
|
|
}
|
|
|
|
|
path := fs.Arg(0)
|
|
|
|
|
if arch.FromFilename(path) != arch.AMD64 {
|
|
|
|
|
fmt.Fprintln(os.Stderr, "gasm asm: only amd64 is supported in this Phase 2 increment")
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
src, err := readSource(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintln(os.Stderr, "gasm:", err)
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
f, errs := parser.Parse(path, src)
|
|
|
|
|
for _, e := range errs {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
|
|
|
|
|
}
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 15:56:03 +02:00
|
|
|
img, err := asm.AssembleFile(f)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "%s: %v\n", path, err)
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
if len(img.Funcs) == 0 {
|
|
|
|
|
fmt.Fprintln(os.Stderr, "gasm asm: no assemblable TEXT functions found")
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
for _, fn := range img.Funcs {
|
|
|
|
|
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
|
|
|
|
fmt.Printf("%s: %d bytes\n", fn.Name, fn.Size)
|
2026-07-06 09:49:50 +02:00
|
|
|
for i := 0; i < len(code); i += 16 {
|
|
|
|
|
end := i + 16
|
|
|
|
|
if end > len(code) {
|
|
|
|
|
end = len(code)
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf(" %04x:", i)
|
|
|
|
|
for _, b := range code[i:end] {
|
|
|
|
|
fmt.Printf(" %02x", b)
|
|
|
|
|
}
|
|
|
|
|
fmt.Println()
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-09 15:56:03 +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])
|
|
|
|
|
}
|
|
|
|
|
for i := 0; i < len(img.Data); i += 16 {
|
|
|
|
|
end := i + 16
|
|
|
|
|
if end > len(img.Data) {
|
|
|
|
|
end = len(img.Data)
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf(" %04x:", len(img.Code)+i)
|
|
|
|
|
for _, b := range img.Data[i:end] {
|
|
|
|
|
fmt.Printf(" %02x", b)
|
|
|
|
|
}
|
|
|
|
|
fmt.Println()
|
|
|
|
|
}
|
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 {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "gasm asm: external symbol %q needs an object file (use --format elf or --format macho)\n", img.Externals[0])
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
obj, kind = img.Bytes(), "raw image"
|
|
|
|
|
case "elf":
|
|
|
|
|
obj, err = img.ELFObject()
|
|
|
|
|
kind = "ELF object"
|
|
|
|
|
case "macho":
|
|
|
|
|
obj, err = img.MachOObject()
|
|
|
|
|
kind = "Mach-O object"
|
2026-07-17 18:57:04 +02:00
|
|
|
case "goobj":
|
|
|
|
|
obj, err = img.GOObject(*pkg, path)
|
|
|
|
|
kind = "Go object"
|
2026-07-16 20:52:20 +02:00
|
|
|
default:
|
2026-07-17 18:57:04 +02:00
|
|
|
fmt.Fprintf(os.Stderr, "gasm asm: unknown format %q (want raw, elf, macho or goobj)\n", *format)
|
2026-07-16 20:52:20 +02:00
|
|
|
return 2
|
|
|
|
|
}
|
|
|
|
|
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, 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)
|
2026-07-06 09:49:50 +02:00
|
|
|
}
|
|
|
|
|
return 0
|
|
|
|
|
}
|
2026-07-22 09:28:11 +02:00
|
|
|
|
|
|
|
|
func cmdVerify(args []string) int {
|
|
|
|
|
fs := newCommand("verify", "gasm verify <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.
|
|
|
|
|
`)
|
|
|
|
|
smoke := fs.Bool("smoke", false, "call each NOSPLIT function with zeroed args")
|
|
|
|
|
fs.Parse(args)
|
|
|
|
|
if fs.NArg() != 1 {
|
|
|
|
|
fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] <file.s>")
|
|
|
|
|
return 2
|
|
|
|
|
}
|
|
|
|
|
path := fs.Arg(0)
|
|
|
|
|
if arch.FromFilename(path) != arch.AMD64 {
|
|
|
|
|
fmt.Fprintln(os.Stderr, "gasm verify: only amd64 is supported")
|
|
|
|
|
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
|
|
|
|
|
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 *smoke && fl.NoSplit {
|
|
|
|
|
args := make([]byte, fl.Args)
|
|
|
|
|
_, err := k.CallFunc(name, args)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Printf(" smoke: FAIL — %v\n", err)
|
|
|
|
|
rc = 1
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Printf(" smoke: OK\n")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return rc
|
|
|
|
|
}
|