# 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. ## 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 -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/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow - [docs/CLI.md](docs/CLI.md): full command reference - [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)