257 lines
12 KiB
Markdown
257 lines
12 KiB
Markdown
# Plan 9 assembly tooling, inside and outside Go
|
|
|
|
> **Warning: this is an experiment.** gasm-devkit is under active
|
|
> development and is not stable. The version is 0.x.x: commands, flags,
|
|
> output formats and behaviour can change without warning at any time.
|
|
> A 1.0.0 release is light years away. Nothing in this document is a
|
|
> stability promise. For all of that, this is not a paper project: gasm
|
|
> is already in active use and is tested on real assembly work.
|
|
|
|
**GAsm** is Go's Plan 9 assembler, and Go ships it without tooling:
|
|
there is no formatter, no linter, no static analyser, no standalone
|
|
assembler and no debugger for `.s` files. Developers write assembly
|
|
blind, validate it by benchmark, and debug it by print statement.
|
|
gasm-devkit is the missing toolkit: a single, self-contained binary,
|
|
`gasm`, that serves both purposes.
|
|
|
|
- **Help develop Plan 9 assembly.** Formatting, linting, disassembly,
|
|
dynamic verification, a source-level debugger and a language server,
|
|
for `.s` files in Go programs.
|
|
- **Use Plan 9 assembly outside the Go toolchain.** `gasm asm` encodes
|
|
on its own, with no Go installation in the loop, and writes raw
|
|
images, linkable ELF objects with DWARF5 debug sections, or the Go
|
|
toolchain's own GOOBJ format, which `go build` consumes in place of
|
|
the toolchain's output.
|
|
|
|
## Why Plan 9 assembly
|
|
|
|
Plan 9 assembly is the quiet triumph of the field. One syntax across
|
|
every architecture Go builds for: the same source-first operand order,
|
|
the same four pseudo-registers, the same frame convention, whether the
|
|
target is x86, ARM, RISC-V or LoongArch. Learn it once and you can
|
|
read a kernel on any of them.
|
|
|
|
Compare the alternatives. Intel syntax and AT&T syntax disagree on the
|
|
one question every instruction answers, which operand is the source
|
|
and which is the destination, so half the world writes it one way,
|
|
half the other, and every assembly programmer carries both in their
|
|
head forever. GNU as settles the argument with directives that switch
|
|
dialects mid-file (`.intel_syntax noprefix`), a percent sign on every
|
|
register and a dollar on every immediate: punctuation that carries
|
|
nothing the operand order did not already say. And the x86 family
|
|
fragments again underneath: NASM is not MASM is not GAS, each with its
|
|
own directive zoo and macro language, so every project picks a dialect
|
|
and every reader learns a different one by accident.
|
|
|
|
Plan 9 assembly has none of it. Registers are bare names. Memory is
|
|
one notation, `offset(base)`, extended by an index and a scale when
|
|
the instruction needs it. Arguments arrive named and offset-checked:
|
|
`x+0(FP)` is the argument x, on every architecture, and `go vet`
|
|
polices the offsets against the Go prototype.
|
|
|
|
```text
|
|
AT&T (GNU as): movq %rax, -16(%rbp)
|
|
Plan 9 (Go): MOVQ AX, total-16(SP)
|
|
```
|
|
|
|
The same lines, but only one of them tells you what the number is for.
|
|
The syntax is uppercase, regular and boring, which is the highest
|
|
compliment a language for machine code can earn. gasm-devkit exists
|
|
to give that syntax the tooling it deserves.
|
|
|
|
## Features
|
|
|
|
- **Front end.** A hand-written lexer and an error-tolerant parser produce a
|
|
typed AST with source positions; `gasm tokens` and `gasm parse` expose them
|
|
directly.
|
|
- **Formatter.** `gasm fmt` canonicalises indentation, operand spacing,
|
|
per-function mnemonic alignment and blank-line layout: `gofmt` for assembly,
|
|
operating recursively on directories the way `go fmt` does. `-l` lists
|
|
files whose formatting differs and `-d` prints a unified diff.
|
|
- **Linter.** `gasm lint` runs 18 conservative static checks, among them
|
|
`undefined-label`, `abi-argsize` (declared frame vs the `// func` signature),
|
|
`register-clobber` (Go ABI register liveness over the control-flow graph),
|
|
`stack-imbalance`, `abi0-register-args` and `unencodable-instruction`.
|
|
- **Standalone assembler.** `gasm asm` encodes all four architectures without
|
|
the Go toolchain and writes raw images, linkable ELF objects (with DWARF5
|
|
debug sections) or the Go toolchain's own GOOBJ format, which `go build`
|
|
consumes in place of the toolchain's output. Framed functions get the
|
|
stack-split guard and the morestack block, byte-identical to the
|
|
toolchain's, so split functions link too.
|
|
- **Disassembler.** `gasm dis` lists a `.s` file's functions at their real
|
|
offsets after assembling, or disassembles raw bytes from a file or stdin.
|
|
- **Dynamic verification.** `gasm verify` JIT-loads assembled functions into
|
|
executable memory: smoke calls, ABI checks (sentinel registers, red-zone
|
|
canary), differential fuzzing against the `go tool asm` build, and
|
|
byte-for-byte ground-truth comparison of the machine code.
|
|
- **Debugger.** `gasm debug` is a source-level ptrace debugger with
|
|
breakpoints (optionally conditional), hardware watchpoints, register and
|
|
memory inspection, and headless script runs with label-level coverage.
|
|
- **Language server.** `gasm lsp` serves completion, hover, document symbols,
|
|
push and pull diagnostics, semantic-token highlighting, go-to-definition,
|
|
find references, rename, formatting, inlay hints, code actions, signature
|
|
help, document highlights, workspace symbol search, #include document
|
|
links and folding ranges over stdio; definition, references and rename
|
|
work across every open document.
|
|
- **Comparators and audits.** `gasm diff` compares the machine code of two
|
|
assembly files byte-for-byte, `gasm profile` shows basic-block structure,
|
|
`gasm audit-instructions` diffs the encoder against the installed toolchain,
|
|
and `gasm scaffold` generates a differential test skeleton for a kernel.
|
|
|
|
### Architecture support
|
|
|
|
Four architectures, the four that matter in practice:
|
|
|
|
| Architecture | GOARCH | File suffix | Instructions recognised |
|
|
|--------------|-------------|--------------|---------------------------------------------|
|
|
| AMD64 | `amd64` | `_amd64.s` | 1600 + common opcodes + traditional aliases |
|
|
| ARM64 | `arm64` | `_arm64.s` | 538 + common opcodes |
|
|
| RISC-V | `riscv64` | `_riscv64.s` | 961 + common opcodes |
|
|
| LoongArch | `loong64` | `_loong64.s` | 799 + common opcodes |
|
|
|
|
"Common opcodes" are the instructions shared by every architecture (`RET`,
|
|
`JMP`, `NOP`, `CALL`, `TEXT`, `FUNCDATA`, `PCDATA`, ...). AMD64 additionally
|
|
carries the traditional conditional-jump spellings (`JZ`, `JNZ`, `JA`, `JC`,
|
|
...) that the assembler accepts as aliases. The tables are generated from
|
|
the Go toolchain's own assembler source (`just gen` refreshes them), so
|
|
every mnemonic the real assembler accepts is recognised; what the encoder
|
|
can emit today is narrower, and a recognised but unencodable instruction is
|
|
reported as an explicit error, never as a wrong byte.
|
|
|
|
The same measurement runs over GOROOT's whole assembly corpus:
|
|
`gasm audit-instructions --corpus` reports 127 of 627 files (20.3 %)
|
|
assembling for every target architecture today, with the top failure
|
|
reasons per architecture; the number moves with every release.
|
|
|
|
## Direction
|
|
|
|
The plan, in the order it is being worked:
|
|
|
|
- **Extended instruction support.** Two layers. First, encoding
|
|
coverage for every mnemonic the Go toolchain itself accepts, closed in
|
|
order of how often real code needs each instruction;
|
|
`gasm audit-instructions` measures the gap. Second, the larger work:
|
|
an extended instruction set the toolchain does not know at all. The
|
|
toolchain-derived tables stay generated and untouched; only the
|
|
extended instructions are hand-maintained, with their own spellings
|
|
and encoders, verified by execution on real hardware because the
|
|
toolchain offers no ground truth to compare against. The gaps exist
|
|
on every architecture, amd64 included.
|
|
- **Full GOOBJ and ELF compilation.** The destination is a complete,
|
|
standalone compilation path: linkable ELF objects for consumers outside
|
|
Go, and GOOBJ objects that `go build` links directly. Through GOOBJ, a
|
|
Go program will be able to use machine instructions that the Go
|
|
toolchain itself does not support; through ELF, Plan 9 assembly becomes
|
|
usable outside Go entirely.
|
|
- **Platforms: Linux and FreeBSD.** Linux is supported today on all four
|
|
architectures and is where the binary builds. FreeBSD follows: the
|
|
JIT's executable-memory mapping and the ptrace debugger layer are the
|
|
two pieces of porting work. Other unix systems may follow those two.
|
|
- **Four architectures, no more.** amd64, arm64, riscv64 and loong64.
|
|
No others are planned.
|
|
|
|
## Install
|
|
|
|
Prebuilt binaries for linux/amd64, linux/arm64, linux/riscv64 and
|
|
linux/loong64 are on the
|
|
[releases page](https://sourcedock.dev/petrbalvin/gasm-devkit/releases).
|
|
From source (Go 1.27.1):
|
|
|
|
```sh
|
|
go install sourcedock.dev/petrbalvin/gasm-devkit/cmd/gasm@latest
|
|
```
|
|
|
|
Or from a repository checkout:
|
|
|
|
```sh
|
|
just install
|
|
```
|
|
|
|
The installed binary reports the version the toolchain recorded: the tag
|
|
on a tagged checkout, a pseudo-version naming the commit below one.
|
|
|
|
## Quick start
|
|
|
|
```sh
|
|
cat > hello_amd64.s <<'EOF'
|
|
#include "textflag.h"
|
|
|
|
// func add(a, b int) int
|
|
TEXT ·add(SB), NOSPLIT, $0-24
|
|
MOVQ a+0(FP), AX
|
|
ADDQ b+8(FP), AX
|
|
MOVQ AX, ret+16(FP)
|
|
RET
|
|
EOF
|
|
|
|
gasm lint hello_amd64.s # static checks
|
|
gasm asm -o hello.bin hello_amd64.s # assemble to a raw image
|
|
gasm verify --call add --args a=2,b=3 hello_amd64.s # JIT-call it with arguments
|
|
```
|
|
|
|
## Usage
|
|
|
|
```sh
|
|
gasm fmt # reformat every .s below here, like go fmt
|
|
gasm fmt -w kernel_amd64.s # canonicalise one file in place
|
|
gasm fmt -l *.s # list files whose formatting differs
|
|
gasm fmt -d kernel_amd64.s # print a unified diff instead
|
|
gasm lint *.s # static checks
|
|
gasm asm --format elf -o k.o k.s # assemble to a linkable ELF object
|
|
gasm asm --format goobj -p pkg/path -o k.o k.s # Go object, consumed by go build
|
|
gasm dis k.s # assemble, then list each function
|
|
gasm dis -a amd64 - < dump.bin # disassemble raw bytes from stdin
|
|
gasm verify --ground-truth k.s # byte-for-byte vs go tool asm
|
|
gasm verify --fuzz k.s # differential fuzz vs the go tool asm build
|
|
gasm debug --func name k.s # interactive debugger
|
|
gasm debug --func name --script cmds.txt --timeout 30s k.s # headless run
|
|
gasm debug --func name --cover k.s # which labels did execution reach?
|
|
gasm diff a.s b.s # compare machine code byte-for-byte
|
|
gasm diff --map wideCopyAVX2=wideCopyAVX512 avx2.s avx512.s
|
|
gasm profile k.s # show basic-block structure
|
|
gasm audit-instructions # encoder vs go tool asm name diff
|
|
gasm scaffold differential k.s # generate a differential test skeleton
|
|
```
|
|
|
|
Run `gasm --help` for the command overview and `gasm <command> -h` for a
|
|
command's flags. [docs/CLI.md](docs/CLI.md) is the full reference.
|
|
|
|
### Editor integration
|
|
|
|
`gasm lsp` speaks the Language Server Protocol over standard input/output, so
|
|
any LSP-capable editor can use it: point your editor's LSP client at the
|
|
binary and associate it with `.s` files. Syntax highlighting is delivered as
|
|
LSP semantic tokens, so no editor-specific grammar is required. The server
|
|
infers the target architecture from the file-name suffix
|
|
(`_amd64.s` / `_arm64.s` / `_riscv64.s` / `_loong64.s`).
|
|
|
|
## Development
|
|
|
|
```sh
|
|
just build # compile, zero errors and zero warnings
|
|
just test # the suite, no cache, the 80 % coverage floor
|
|
just gates # build, fmt-check, vet, test, race: the definition of done
|
|
just fmt # gofmt the tree
|
|
just gen # regenerate the instruction tables from the Go toolchain
|
|
```
|
|
|
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow and
|
|
[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for setup details and every
|
|
recipe.
|
|
|
|
## Documentation
|
|
|
|
- [docs/CLI.md](docs/CLI.md): full command reference
|
|
- man pages: `just install-man` installs gasm(1) and one page per command
|
|
into ~/.local/share/man (MANDIR overrides); `just uninstall-man` removes
|
|
them
|
|
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
|
|
- [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md): development setup and recipes
|
|
- [CHANGELOG.md](CHANGELOG.md): release history
|
|
|
|
## Licence
|
|
|
|
BSD-3-Clause; see [LICENSE](LICENSE).
|
|
|
|
Copyright © 2026 [Petr Balvín](https://petrbalvin.org)
|