2026-09-19 18:03:34 +02:00
# Plan 9 assembly tooling, inside and outside Go
2026-08-01 05:22:00 +02:00
2026-09-19 18:03:34 +02:00
> **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
2026-09-20 01:40:51 +02:00
> is already in active use and is tested on real assembly work. Only
> amd64 is validated on real hardware; the other three architectures run
> under emulation ([Validation status](#validation-status)).
2026-08-01 05:22:00 +02:00
2026-09-19 18:03:34 +02:00
**GAsm** is Go's Plan 9 assembler, and Go ships it without tooling:
2026-09-20 01:40:51 +02:00
there is no formatter, no linter and no debugger for `.s` files, and no
assembler that works without a Go installation. 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.
2026-09-19 18:03:34 +02:00
- **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
2026-09-20 01:40:51 +02:00
on its own and writes raw images or linkable ELF objects with DWARF5
debug sections, with no Go installation in the loop; the Go
2026-09-19 18:03:34 +02:00
toolchain's own GOOBJ format, which `go build` consumes in place of
2026-09-20 01:40:51 +02:00
the toolchain's output, needs the installed toolchain.
2026-09-19 18:03:34 +02:00
## 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.
2026-08-01 05:22:00 +02:00
2026-08-30 10:40:25 +02:00
## Features
2026-08-01 05:22:00 +02:00
2026-08-30 10:40:25 +02:00
- **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,
2026-09-14 23:36:19 +02:00
operating recursively on directories the way `go fmt` does. `-l` lists
files whose formatting differs and `-d` prints a unified diff.
2026-08-30 21:31:05 +02:00
- **Linter.** `gasm lint` runs 18 conservative static checks, among them
2026-09-20 01:40:51 +02:00
`undefined-label` , `abi-argsize` (declared argument area vs the `// func`
signature), `register-clobber` (Go ABI register liveness over the
control-flow graph), `stack-imbalance` , `abi0-register-args` and
`unencodable-instruction` .
2026-08-30 10:40:25 +02:00
- **Standalone assembler.** `gasm asm` encodes all four architectures without
2026-09-20 01:40:51 +02:00
the Go toolchain and writes raw images or linkable ELF objects (with DWARF5
debug sections) with no Go installation needed, or the Go toolchain's own
GOOBJ format, which needs the installed toolchain and which `go build`
2026-09-14 23:36:19 +02:00
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.
2026-08-30 10:40:25 +02:00
- **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
2026-09-20 01:40:51 +02:00
memory inspection, and headless script runs that report instruction and
label coverage.
2026-08-30 10:40:25 +02:00
- **Language server.** `gasm lsp` serves completion, hover, document symbols,
2026-08-30 21:31:05 +02:00
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
2026-09-14 23:36:19 +02:00
links and folding ranges over stdio; definition, references and rename
work across every open document.
2026-08-30 10:40:25 +02:00
- **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.
2026-08-01 05:22:00 +02:00
2026-08-30 10:40:25 +02:00
### Architecture support
2026-08-01 05:22:00 +02:00
2026-09-19 18:03:34 +02:00
Four architectures, the four that matter in practice:
2026-08-30 10:40:25 +02:00
| 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 |
2026-08-01 05:22:00 +02:00
"Common opcodes" are the instructions shared by every architecture (`RET` ,
2026-08-30 10:40:25 +02:00
`JMP` , `NOP` , `CALL` , `TEXT` , `FUNCDATA` , `PCDATA` , ...). AMD64 additionally
2026-08-01 05:22:00 +02:00
carries the traditional conditional-jump spellings (`JZ` , `JNZ` , `JA` , `JC` ,
2026-09-19 18:03:34 +02:00
...) 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.
2026-09-19 20:48:51 +02:00
The same measurement runs over GOROOT's whole assembly corpus:
2026-09-20 06:45:03 +02:00
`gasm audit-instructions --corpus` reports 136 of 433 attemptable files
(31.4 %) assembling for every target architecture today (files named for
other Go ports are counted but never attempted), with the top failure
2026-09-19 20:48:51 +02:00
reasons per architecture; the number moves with every release.
2026-09-20 01:40:51 +02:00
### Validation status
**Only amd64 is validated on real hardware.** The other three
architectures are validated under qemu-user emulation, because the
project owns no arm64, riscv64 or loong64 machine, and emulation is the
only substitute available for the hardware. The distinction matters and
is stated rather than implied: everything below is a claim about what has
actually been executed.
| Layer | amd64 | arm64, riscv64, loong64 |
|---|---|---|
| Encoding: byte-for-byte against `go tool asm` | native hardware | native hardware (the toolchain cross-assembles any GOARCH on any host) |
| Execution: JIT calls, ABI checks, differential fuzzing | native hardware | qemu-user emulation |
| Debugger: ptrace tracing, breakpoints, watchpoints, coverage | native hardware | emulation cannot run ptrace; the layer compiles and its architecture-neutral units run under `go test ./...` , nothing more |
Consequences, stated plainly. An emulator is a model of a CPU, not the
CPU: instruction semantics are implemented in software and can differ
from silicon in ways a test suite does not reveal. A kernel that passes
under qemu-user is therefore not proven correct on real hardware, and a
discrepancy found on real hardware is a defect in gasm, reported like any
other. Encoding parity is the exception: the byte comparison against the
toolchain runs on the host for every architecture, so no emulator stands
between the claim and the evidence. The debugger is the weakest case: on
the three emulated architectures its per-architecture ptrace code has
been compiled and read, never executed. Its architecture-neutral units
run under `go test ./...` , which the race workflow and a manual run
perform; the default `just test` gate does not sweep `./debug/...` .
2026-09-19 18:03:34 +02:00
## 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
2026-09-20 01:40:51 +02:00
and encoders, verified by execution (on real hardware for amd64, under
emulation for the rest, per the validation status above) because the
2026-09-19 18:03:34 +02:00
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.
2026-08-01 05:22:00 +02:00
2026-08-30 10:40:25 +02:00
## Install
2026-08-07 22:20:26 +02:00
2026-08-30 10:40:25 +02:00
Prebuilt binaries for linux/amd64, linux/arm64, linux/riscv64 and
linux/loong64 are on the
[releases page ](https://sourcedock.dev/petrbalvin/gasm-devkit/releases ).
2026-09-17 20:33:18 +02:00
From source (Go 1.27.1):
2026-08-07 22:20:26 +02:00
2026-08-30 10:40:25 +02:00
```sh
go install sourcedock.dev/petrbalvin/gasm-devkit/cmd/gasm@latest
```
2026-08-01 05:22:00 +02:00
2026-09-16 22:53:01 +02:00
Or from a repository checkout:
2026-08-01 05:22:00 +02:00
2026-08-30 10:40:25 +02:00
```sh
2026-09-16 22:53:01 +02:00
just install
2026-08-30 10:40:25 +02:00
```
2026-08-01 05:22:00 +02:00
2026-09-16 22:53:01 +02:00
The installed binary reports the version the toolchain recorded: the tag
on a tagged checkout, a pseudo-version naming the commit below one.
2026-08-01 05:22:00 +02:00
## Quick start
```sh
2026-08-30 10:40:25 +02:00
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
2026-08-01 05:22:00 +02:00
```
2026-08-30 10:40:25 +02:00
## Usage
2026-08-01 05:22:00 +02:00
```sh
2026-08-30 10:40:25 +02:00
gasm fmt # reformat every .s below here, like go fmt
gasm fmt -w kernel_amd64.s # canonicalise one file in place
2026-09-14 23:36:19 +02:00
gasm fmt -l *.s # list files whose formatting differs
gasm fmt -d kernel_amd64.s # print a unified diff instead
2026-08-30 10:40:25 +02:00
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
2026-09-14 23:36:19 +02:00
gasm dis k.s # assemble, then list each function
gasm dis -a amd64 - < dump.bin # disassemble raw bytes from stdin
2026-08-30 10:40:25 +02:00
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
2026-08-29 14:24:52 +02:00
gasm debug --func name --script cmds.txt --timeout 30s k.s # headless run
2026-09-20 01:40:51 +02:00
gasm debug --func name --cover k.s # instruction and label coverage
2026-08-30 10:40:25 +02:00
gasm diff a.s b.s # compare machine code byte-for-byte
2026-08-05 21:15:05 +02:00
gasm diff --map wideCopyAVX2 = wideCopyAVX512 avx2.s avx512.s
2026-08-30 10:40:25 +02:00
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
2026-08-01 05:22:00 +02:00
```
2026-08-30 10:40:25 +02:00
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.
2026-08-01 05:22:00 +02:00
2026-08-30 10:40:25 +02:00
### Editor integration
2026-08-01 05:22:00 +02:00
`gasm lsp` speaks the Language Server Protocol over standard input/output, so
2026-08-30 10:40:25 +02:00
any LSP-capable editor can use it: point your editor's LSP client at the
2026-08-01 05:22:00 +02:00
binary and associate it with `.s` files. Syntax highlighting is delivered as
2026-08-30 10:40:25 +02:00
LSP semantic tokens, so no editor-specific grammar is required. The server
2026-08-01 05:22:00 +02:00
infers the target architecture from the file-name suffix
(`_amd64.s` / `_arm64.s` / `_riscv64.s` / `_loong64.s` ).
2026-08-30 10:40:25 +02:00
## Development
```sh
2026-09-16 22:53:01 +02:00
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
2026-08-30 10:40:25 +02:00
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
2026-09-19 21:18:43 +02:00
- man pages: `just install-man` installs gasm(1) and one page per command
2026-09-20 01:40:51 +02:00
except `version` , which is documented inside gasm(1) instead, into
~/.local/share/man (MANDIR overrides); `just uninstall-man` removes
2026-09-19 21:18:43 +02:00
them
- [docs/ARCHITECTURE.md ](docs/ARCHITECTURE.md ): components and data flow
2026-08-30 10:40:25 +02:00
- [docs/DEVELOPMENT.md ](docs/DEVELOPMENT.md ): development setup and recipes
- [CHANGELOG.md ](CHANGELOG.md ): release history
## Licence
2026-09-16 23:12:31 +02:00
BSD-3-Clause; see [LICENSE ](LICENSE ).
2026-08-01 05:22:00 +02:00
2026-08-20 15:01:57 +02:00
Copyright © 2026 [Petr Balvín ](https://petrbalvin.org )