2026-07-06 09:49:50 +02:00
|
|
|
|
# Architecture
|
|
|
|
|
|
|
|
|
|
|
|
How gasm-devkit is put together and why.
|
|
|
|
|
|
|
2026-08-01 05:22:00 +02:00
|
|
|
|
Repository: [sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrbalvin/gasm-devkit)
|
|
|
|
|
|
|
2026-09-17 20:33:18 +02:00
|
|
|
|
## Overview
|
|
|
|
|
|
|
|
|
|
|
|
Three design goals shape everything below.
|
2026-07-06 09:49:50 +02:00
|
|
|
|
|
|
|
|
|
|
1. **A real AST, not a grammar hack.** The linter, analyser, assembler and
|
2026-08-30 10:40:25 +02:00
|
|
|
|
language server all need to *reason* about assembly, not just colour it.
|
2026-07-06 09:49:50 +02:00
|
|
|
|
So the centre of the toolkit is a hand-written lexer and a parser that
|
|
|
|
|
|
produce a typed AST with source positions on every node.
|
|
|
|
|
|
2. **Architecture as data, not code.** Per-architecture differences (amd64,
|
|
|
|
|
|
arm64, riscv64, loong64) live in register and instruction *tables* (`arch`),
|
|
|
|
|
|
never in `if arch == …` branches scattered through the logic. The
|
|
|
|
|
|
instruction tables are generated from the Go toolchain's own assembler
|
|
|
|
|
|
source (`just gen`), so adding or refreshing an architecture is a data
|
|
|
|
|
|
operation, not a coding one.
|
|
|
|
|
|
3. **Open integration surface.** Everything the toolkit can do is reachable
|
|
|
|
|
|
through two vendor-neutral interfaces: a CLI and an LSP server. No editor
|
|
|
|
|
|
owns the toolkit; the toolkit is offered to editors on standard terms.
|
|
|
|
|
|
|
2026-09-17 20:33:18 +02:00
|
|
|
|
The components, and how data moves between them:
|
2026-07-06 09:49:50 +02:00
|
|
|
|
|
|
|
|
|
|
```mermaid
|
2026-09-17 20:33:18 +02:00
|
|
|
|
flowchart TD
|
2026-07-06 09:49:50 +02:00
|
|
|
|
SRC["source .s"] --> LEX["lexer<br/>token stream"]
|
|
|
|
|
|
LEX --> PAR["parser<br/>AST + diagnostics"]
|
2026-08-30 10:40:25 +02:00
|
|
|
|
LEX --> FMT["format<br/>re-space tokens"]
|
2026-07-06 09:49:50 +02:00
|
|
|
|
PAR --> LINT["lint<br/>static checks"]
|
|
|
|
|
|
PAR --> LSP["lsp server"]
|
|
|
|
|
|
LEX --> LSP
|
|
|
|
|
|
ARCH["arch tables<br/>amd64 / arm64 / riscv64 / loong64"] --> LINT
|
|
|
|
|
|
ARCH --> LSP
|
|
|
|
|
|
LINT --> LSP
|
|
|
|
|
|
FMT --> CLI["gasm CLI"]
|
|
|
|
|
|
LINT --> CLI
|
|
|
|
|
|
PAR --> CLI
|
|
|
|
|
|
LEX --> CLI
|
|
|
|
|
|
LSP --> EDITOR["any LSP editor"]
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
The lexer is the shared foundation: the parser builds the AST from it, the
|
|
|
|
|
|
formatter re-spaces its tokens directly, and the language server uses it for
|
2026-09-19 23:49:27 +02:00
|
|
|
|
semantic highlighting. The packages follow a dependency chain: static analysis
|
|
|
|
|
|
builds only on the AST, the standalone assembler emits object code, and both
|
|
|
|
|
|
the dynamic analysis and the debugger consume the execution substrate the
|
|
|
|
|
|
assembler provides.
|
2026-07-06 09:49:50 +02:00
|
|
|
|
|
2026-09-17 20:33:18 +02:00
|
|
|
|
## Packages
|
|
|
|
|
|
|
|
|
|
|
|
| Package | Responsibility |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| `token` | token kinds and positions |
|
|
|
|
|
|
| `lexer` | hand-written scanner; permissive, and it never panics |
|
|
|
|
|
|
| `ast` | the typed syntax tree: declarations, lines, operands |
|
|
|
|
|
|
| `parser` | line-oriented parser producing the AST and its diagnostics |
|
|
|
|
|
|
| `arch` | register and instruction tables for the four architectures |
|
|
|
|
|
|
| `lint` | static checks over the AST |
|
|
|
|
|
|
| `format` | canonical formatter over the token stream |
|
|
|
|
|
|
| `lsp` | the language server |
|
|
|
|
|
|
| `asm` | standalone assembler: encoders, image layout, object emitters |
|
2026-09-19 23:49:27 +02:00
|
|
|
|
| `disasm` | disassembly backend over golang.org/x/arch |
|
2026-09-17 20:33:18 +02:00
|
|
|
|
| `verify` | JIT execution, ABI checks, differential fuzzing |
|
|
|
|
|
|
| `debug` | interactive ptrace debugger |
|
|
|
|
|
|
| `cmd/gasm` | the CLI |
|
|
|
|
|
|
| `_gen` | rebuilds the `arch` tables from the Go toolchain source |
|
|
|
|
|
|
|
|
|
|
|
|
The boundaries matter as much as the responsibilities: `ast` records syntax
|
|
|
|
|
|
only, and whether a name is a register or a label is left to `arch`, so the
|
|
|
|
|
|
parser stays architecture-agnostic. `asm` and `verify` are the only packages
|
|
|
|
|
|
that touch machine code and executable memory, and `cmd/gasm` owns no logic
|
|
|
|
|
|
beyond flags and output.
|
2026-07-06 09:49:50 +02:00
|
|
|
|
|
|
|
|
|
|
### `token` and `lexer`
|
|
|
|
|
|
|
|
|
|
|
|
The scanner is hand-written and permissive: it never panics and maps anything
|
|
|
|
|
|
it cannot classify to an `Illegal` token, so every downstream tool still works
|
|
|
|
|
|
on malformed input. Newlines are significant tokens, because Plan 9 assembly
|
|
|
|
|
|
is line-oriented and the parser relies on line structure.
|
|
|
|
|
|
|
|
|
|
|
|
The middle dot (`·`, U+00B7) is treated as an identifier character so that
|
|
|
|
|
|
`·funcName(SB)` lexes as one symbol. Multi-character operators (`<<`, `>>`,
|
|
|
|
|
|
`->`) are recognised so arm64 shift operands scan correctly. A backslash
|
|
|
|
|
|
immediately before a newline is a C-preprocessor line continuation (used by
|
|
|
|
|
|
`#define` macros in the runtime `.s` files); the lexer splices the lines
|
|
|
|
|
|
together so a multi-line macro becomes one logical line the parser treats as an
|
|
|
|
|
|
opaque preprocessor directive.
|
|
|
|
|
|
|
|
|
|
|
|
### `ast` and `parser`
|
|
|
|
|
|
|
|
|
|
|
|
The parser is **line-oriented**, matching how the Plan 9 assembler reads a
|
|
|
|
|
|
file: it groups tokens into lines, classifies each line (directive, label,
|
|
|
|
|
|
instruction, comment, preprocessor) and dispatches. A malformed line is
|
|
|
|
|
|
reported and skipped; it never aborts the file.
|
|
|
|
|
|
|
|
|
|
|
|
Operands are parsed into a faithful, flat representation. The amd64
|
2026-08-30 10:40:25 +02:00
|
|
|
|
addressing modes (`reg`, `$imm`, `(base)`, `off(base)`, `(base)(index*scale)`,
|
|
|
|
|
|
`name+off(FP)`, `name<>(SB)`) are all captured structurally, and the original
|
2026-07-06 09:49:50 +02:00
|
|
|
|
token text is retained for fidelity.
|
|
|
|
|
|
|
|
|
|
|
|
A deliberate boundary: the AST records **syntax only**. Whether a bare
|
|
|
|
|
|
identifier is a register or a label is an *architecture* question, so it is
|
|
|
|
|
|
left to `arch` and resolved in the lint/lsp layers. This keeps the parser
|
|
|
|
|
|
arch-agnostic and its output deterministic.
|
|
|
|
|
|
|
|
|
|
|
|
### `arch`
|
|
|
|
|
|
|
2026-08-30 10:40:25 +02:00
|
|
|
|
Register files are generated programmatically (the regular `R8`-`R15`,
|
|
|
|
|
|
`X0`-`X15`, `Y0`-`Y15`, `Z0`-`Z31`, `K0`-`K7` ranges) plus the irregularly
|
2026-07-06 09:49:50 +02:00
|
|
|
|
named registers listed explicitly. Instruction names are **generated from the
|
|
|
|
|
|
Go toolchain's own assembler source** (`cmd/internal/obj/<arch>/anames.go`,
|
|
|
|
|
|
plus the common opcodes and the per-architecture front-end aliases such as the
|
|
|
|
|
|
arm64 `B`/`BL` branches and the `.P`/`.W` load-store addressing suffixes) by
|
|
|
|
|
|
`just gen`, so the tables always match what the real assembler accepts. Each
|
|
|
|
|
|
mnemonic maps to a summary and an optional operand-count range; counts are
|
|
|
|
|
|
recorded only where unambiguous (`-1` disables the operand-count lint for that
|
|
|
|
|
|
instruction) so the linter stays silent rather than guess. For architectures
|
|
|
|
|
|
with highly variable operand forms (arm64, riscv64, loong64) only a few
|
|
|
|
|
|
fixed-arity instructions (`RET`, `NOP`, `JMP`, `CALL`) carry counts at all.
|
|
|
|
|
|
|
|
|
|
|
|
### `lint`
|
|
|
|
|
|
|
2026-08-30 10:40:25 +02:00
|
|
|
|
Rules are conservative by design: silence beats a false positive. The rules
|
2026-07-06 09:49:50 +02:00
|
|
|
|
are `unknown-instruction`, `operand-count`, `undefined-label`,
|
2026-08-21 00:50:31 +02:00
|
|
|
|
`duplicate-label`, `missing-ret`, `missing-textflag-include`, `abi-argsize`,
|
|
|
|
|
|
`unreachable-code`, `register-clobber`, `funcdata-pcdata`, `unused-label`,
|
2026-08-30 10:40:25 +02:00
|
|
|
|
`invalid-textflag`, `stack-imbalance`, `register-width-mismatch`,
|
2026-08-30 21:31:05 +02:00
|
|
|
|
`abi0-register-args`, `nonportable-register-name`,
|
|
|
|
|
|
`unencodable-instruction` and `reserved-register-write`. Every diagnostic carries a stable
|
2026-08-21 00:50:31 +02:00
|
|
|
|
code so callers can disable rules individually, and arch-specific rules switch
|
|
|
|
|
|
off entirely when the target architecture cannot be inferred from the file name.
|
2026-07-06 09:49:50 +02:00
|
|
|
|
|
|
|
|
|
|
Two things keep the rules honest on real-world code:
|
|
|
|
|
|
|
|
|
|
|
|
- **Pseudo-ops and macros are not instructions.** `unknown-instruction` knows
|
|
|
|
|
|
the assembler pseudo-ops (`BYTE`, `WORD`, `FUNCDATA`, `PCDATA`, …) and
|
2026-08-30 10:40:25 +02:00
|
|
|
|
recognises macro invocations: an in-file `#define` name, or any identifier
|
2026-07-06 09:49:50 +02:00
|
|
|
|
containing an underscore (no Plan 9 mnemonic ever does).
|
|
|
|
|
|
- **Macro-heavy files get the label/RET heuristics turned off.** Without a
|
|
|
|
|
|
preprocessor, labels a macro defines are invisible, so `undefined-label` and
|
|
|
|
|
|
`missing-ret` are suppressed for files that use macros (an in-file `#define`
|
|
|
|
|
|
or a `#include` of anything other than `textflag.h`). `missing-ret` also
|
|
|
|
|
|
treats a trailing unconditional jump and `UNDEF` as valid terminators.
|
|
|
|
|
|
|
|
|
|
|
|
The result is validated by `TestGoRuntimeCorpus`, which parses and lints every
|
|
|
|
|
|
`src/runtime/*.s` file the toolchain ships for all four architectures and
|
|
|
|
|
|
asserts zero parse errors and zero error-severity diagnostics.
|
|
|
|
|
|
|
|
|
|
|
|
Two deeper analyses sit on top of the AST:
|
|
|
|
|
|
|
|
|
|
|
|
- **`abi-argsize`.** Hand-written kernels document their signature in a
|
|
|
|
|
|
`// func …` comment above the `TEXT`. The linter parses that signature with
|
|
|
|
|
|
the standard library's Go parser, lays out the parameters and results under
|
|
|
|
|
|
Go's ABI0 stack rules (results begin on a word boundary after the
|
|
|
|
|
|
parameters), and checks the total against the argument size declared in the
|
|
|
|
|
|
`TEXT` directive. It only runs for stack-argument functions (a non-zero
|
|
|
|
|
|
declared arg area that is actually addressed through `FP`), and aborts
|
2026-08-30 10:40:25 +02:00
|
|
|
|
silently on a type whose size it cannot determine, so it never guesses.
|
2026-07-06 09:49:50 +02:00
|
|
|
|
- **`unreachable-code`.** Code after a `RET` and before the next label is
|
|
|
|
|
|
dead. The check is suppressed for any function whose reachability cannot be
|
|
|
|
|
|
decided statically: those using PC-relative jumps (`JMP 2(PC)`),
|
2026-09-19 19:17:07 +02:00
|
|
|
|
register-indirect branches (`JALR`/`JR`/`JIRL`/`BR`/`BLR`, or a `JMP`/`CALL`
|
|
|
|
|
|
through a register or memory operand), or living in a
|
2026-08-30 10:40:25 +02:00
|
|
|
|
file with `#ifdef` conditionals. `UNDEF` is deliberately not a terminator:
|
2026-07-06 09:49:50 +02:00
|
|
|
|
code after it is occasionally intentional metadata.
|
|
|
|
|
|
- **`register-clobber` (register liveness).** The linter builds the function's
|
|
|
|
|
|
control-flow graph (basic blocks split at labels and after branches, with
|
|
|
|
|
|
fall-through and jump-target edges), computes a conservative per-instruction
|
|
|
|
|
|
register def/use, and runs the standard backward liveness iteration to a fixed
|
2026-07-11 17:36:52 +02:00
|
|
|
|
point. On top of that it flags writes to the registers the **Go ABI** fixes
|
2026-08-30 10:40:25 +02:00
|
|
|
|
across calls that are never saved and restored, calibrated from
|
2026-07-11 17:36:52 +02:00
|
|
|
|
`cmd/compile/abi-internal.md`, *not* the platform ABI: Go's stack-based ABI0
|
2026-08-30 10:40:25 +02:00
|
|
|
|
has no System V style callee-saved registers (amd64 `BX`, `R12`-`R15` and
|
2026-07-11 17:36:52 +02:00
|
|
|
|
the like are caller-saved or permanent scratch, and hand-written kernels may
|
|
|
|
|
|
clobber them freely). The audited set is the frame pointer and the
|
2026-08-30 10:40:25 +02:00
|
|
|
|
goroutine pointer per architecture (amd64 `BP`/`R14`, arm64 `R18`/`R28`/
|
2026-07-11 17:36:52 +02:00
|
|
|
|
`R29`, riscv64 `X27`, loong64 `R22`); the goroutine pointer is reported only
|
2026-08-30 10:40:25 +02:00
|
|
|
|
when the function can reach the runtime (it is not `NOSPLIT` or makes a
|
|
|
|
|
|
call), since the ABI0 transition machinery restores it on those paths, and
|
2026-07-11 17:36:52 +02:00
|
|
|
|
NOSPLIT call-free leaves may use it (the runtime's own assembly does). It
|
|
|
|
|
|
runs only on macro-free files, where
|
2026-07-06 09:49:50 +02:00
|
|
|
|
no opaque macro can perform the save/restore.
|
|
|
|
|
|
- **`funcdata-pcdata`.** `FUNCDATA $idx, sym(SB)` and `PCDATA $idx, $val` are
|
|
|
|
|
|
checked for well-formed operands (arity, immediate index and value, symbol
|
|
|
|
|
|
reference) and a literal index is range-checked; a named index constant such
|
|
|
|
|
|
as `$PCDATA_StackMapIndex` is accepted without a range check.
|
|
|
|
|
|
|
2026-08-30 10:40:25 +02:00
|
|
|
|
### `format`
|
2026-07-06 09:49:50 +02:00
|
|
|
|
|
|
|
|
|
|
The formatter works on the **token stream, not the AST**, so it preserves
|
2026-08-30 10:40:25 +02:00
|
|
|
|
every line, comments and blanks included. It normalises indentation, operand
|
2026-07-12 21:24:41 +02:00
|
|
|
|
spacing, per-function mnemonic alignment and blank-line layout: a new block
|
|
|
|
|
|
(a label, `TEXT` or `GLOBL`) is preceded by exactly one blank line (comments
|
|
|
|
|
|
leading a block stay with it), runs of blanks collapse to one, and a `RET`
|
|
|
|
|
|
terminates the body so the next function's doc comment stays at column 0. It
|
|
|
|
|
|
is idempotent and its output always round-trips through the parser. With a
|
2026-08-30 10:40:25 +02:00
|
|
|
|
directory argument, or none, it reformats every `.s` file below it in
|
2026-07-12 21:24:41 +02:00
|
|
|
|
place and lists the files changed, the way `go fmt` does (`.` and `_`
|
|
|
|
|
|
directories are skipped).
|
2026-07-06 09:49:50 +02:00
|
|
|
|
|
|
|
|
|
|
### `lsp`
|
|
|
|
|
|
|
|
|
|
|
|
The server speaks JSON-RPC 2.0 with `Content-Length` framing over any
|
|
|
|
|
|
`io.Reader`/`io.Writer` (normally stdin/stdout). It maintains an in-memory
|
|
|
|
|
|
document store, republishes diagnostics on every change, and provides:
|
|
|
|
|
|
|
2026-08-30 10:40:25 +02:00
|
|
|
|
- **completion**: instructions, registers, pseudo-registers, textflag macros
|
2026-07-06 09:49:50 +02:00
|
|
|
|
and local labels;
|
2026-08-30 10:40:25 +02:00
|
|
|
|
- **hover**: instruction summaries and register descriptions from `arch`;
|
|
|
|
|
|
- **document symbols**: `TEXT` functions with their labels, plus `GLOBL`/`DATA`;
|
|
|
|
|
|
- **semantic tokens**: syntax highlighting delivered as LSP semantic tokens,
|
2026-07-06 09:49:50 +02:00
|
|
|
|
classified with the lexer plus `arch` (instructions, registers by class,
|
2026-08-30 10:40:25 +02:00
|
|
|
|
pseudo-registers, labels, immediates, comments, directives, textflag macros);
|
|
|
|
|
|
- **navigation**: go-to-definition from a label reference to its definition,
|
|
|
|
|
|
find references, document highlights of every use of the symbol under the
|
|
|
|
|
|
cursor, rename, and workspace symbol search over the open documents;
|
|
|
|
|
|
- **assists**: document formatting through the `format` package, inlay hints
|
|
|
|
|
|
(the frame size after the TEXT argument area), signature help (the callee's
|
|
|
|
|
|
`// func` signature while the cursor is on a `CALL`), and code actions
|
|
|
|
|
|
offering quick fixes for the `missing-ret` and `unused-label` diagnostics.
|
2026-08-30 21:31:05 +02:00
|
|
|
|
- **document information**: pull diagnostics (`textDocument/diagnostic`),
|
|
|
|
|
|
#include document links (resolved against the document directory, then
|
|
|
|
|
|
`$GOROOT/pkg/include`) and folding ranges (one collapsible region per
|
|
|
|
|
|
TEXT function body).
|
2026-07-06 09:49:50 +02:00
|
|
|
|
|
|
|
|
|
|
Semantic tokens are the key to editor-agnostic highlighting: the editor renders
|
|
|
|
|
|
them from the standard LSP legend, so no editor-specific grammar is needed.
|
|
|
|
|
|
|
|
|
|
|
|
### `asm`
|
|
|
|
|
|
|
2026-09-19 23:49:27 +02:00
|
|
|
|
The standalone assembler. Its core is an amd64 instruction encoder:
|
2026-07-06 09:49:50 +02:00
|
|
|
|
a REX/ModR-M/SIB/displacement/immediate engine plus the scalar instruction set,
|
|
|
|
|
|
with the Plan 9 operand order (source first) mapped onto the x86 encoding.
|
2026-08-30 10:40:25 +02:00
|
|
|
|
Every encoding is validated by decoding it again with `golang.org/x/arch`, the
|
2026-09-17 20:33:18 +02:00
|
|
|
|
one module dependency, which also backs the `gasm dis` listings.
|
2026-07-06 09:49:50 +02:00
|
|
|
|
|
2026-09-19 23:49:27 +02:00
|
|
|
|
A **RISC-V encoder** (RV64IMAFDC + RVC compression) encodes the full
|
2026-08-05 20:08:56 +02:00
|
|
|
|
integer, atomic, float/double, FMA and CSR instruction sets with the MOV
|
|
|
|
|
|
pseudo-instruction and SB/global symbol references (AUIPC pairs with
|
|
|
|
|
|
R_RISCV_PCREL_HI20/LO12 relocations). The encoder compresses eligible
|
|
|
|
|
|
instructions to 16-bit RVC forms and is validated byte-for-byte against
|
|
|
|
|
|
`GOARCH=riscv64 go tool asm`.
|
|
|
|
|
|
|
2026-09-19 23:49:27 +02:00
|
|
|
|
A **LoongArch encoder** (LoongArch64) encodes the integer and
|
2026-08-13 11:24:44 +02:00
|
|
|
|
floating-point instruction sets with the dual-form arithmetic mnemonics (3R
|
|
|
|
|
|
vs 2RI12), the 16/21-bit branch families, the MOV pseudo-instruction and its
|
|
|
|
|
|
constant materialisation (the dcon classification driving lu12i.w/ori/lu32i.d/
|
|
|
|
|
|
lu52i.d expansions), the FP/SP frame mapping (autosize = align8(frame+8),
|
|
|
|
|
|
prologue storing the link register before and after the SP decrement) and
|
|
|
|
|
|
SB/global symbol references (pcalau12i pairs with R_LOONG64_ADDR_HI/LO
|
|
|
|
|
|
relocations). Like the RISC-V encoder it is validated byte-for-byte against
|
|
|
|
|
|
`GOARCH=loong64 go tool asm`, and its GOOBJ output is proven end-to-end by
|
|
|
|
|
|
substituting it into a cross-compiled `go build` and linking with `cmd/link`.
|
|
|
|
|
|
|
2026-09-19 23:49:27 +02:00
|
|
|
|
An **AArch64 encoder** (arm64) encodes the integer instruction set
|
2026-08-20 13:32:52 +02:00
|
|
|
|
with the data-processing (shifted register and immediate forms), load/store
|
|
|
|
|
|
(scaled unsigned immediate and unscaled9-bit immediate), conditional and
|
|
|
|
|
|
unconditional branches, the MOV pseudo-instruction and its constant
|
|
|
|
|
|
materialisation (MOVZ/MOVN/MOVK for wide immediates, ORR with logical bitmask
|
|
|
|
|
|
encoding for values like `$1`), the FP/SP frame mapping (autosize =
|
|
|
|
|
|
align16(frame+8), prologue using pre-index store for small frames and
|
|
|
|
|
|
STP+SUB for large frames) and SB/global symbol references (ADRP+ADD pairs with
|
|
|
|
|
|
R_ADDRARM64 relocations). Like the other encoders it is validated
|
|
|
|
|
|
byte-for-byte against `GOARCH=arm64 go tool asm`.
|
|
|
|
|
|
|
2026-09-14 23:36:19 +02:00
|
|
|
|
On top of the per-architecture encoders, every framed function carries the
|
|
|
|
|
|
**stack-split guard**: the prologue check against `g.stackguard0` (small,
|
|
|
|
|
|
medium and large frame classes, the medium and large classes materialising
|
|
|
|
|
|
their offset through the architecture's temporary register and the large
|
|
|
|
|
|
class adding the SP-underflow branch) and the trailing morestack block
|
|
|
|
|
|
(save the link register, `CALL runtime.morestack_noctxt`, jump back to the
|
|
|
|
|
|
function entry). The auto-NOSPLIT rule, the frame classes, the large-frame
|
|
|
|
|
|
prologue and epilogue forms and the tail calls match the toolchain's
|
|
|
|
|
|
`stacksplit` and `preprocess` output byte for byte; a parity suite
|
|
|
|
|
|
assembles kernel files with gasm and the installed `go tool asm` and diffs
|
|
|
|
|
|
the bytes on all four architectures.
|
|
|
|
|
|
|
2026-07-06 09:49:50 +02:00
|
|
|
|
On top of the encoder, `Assemble` walks a parsed `TEXT` body, converts each
|
2026-07-08 12:51:35 +02:00
|
|
|
|
operand to an encoder operand, and lays the instructions out so local labels
|
|
|
|
|
|
resolve to relative jump offsets: jumps start in the short (rel8) form and
|
|
|
|
|
|
expand to rel32 when the settled displacement does not fit, iterating to a
|
|
|
|
|
|
fixed point, and jump-to-jump chains are folded (a conditional jump to a label
|
|
|
|
|
|
whose only instruction is an unconditional jump is redirected to the ultimate
|
|
|
|
|
|
target) exactly as the Go toolchain's linker does before it encodes branches.
|
|
|
|
|
|
The `FP`/`SP` pseudo-
|
2026-08-30 10:40:25 +02:00
|
|
|
|
registers are translated onto the hardware stack pointer: `x+N(FP)` becomes
|
2026-07-06 09:49:50 +02:00
|
|
|
|
`(N+8)(SP)` for a zero-frame function and `(N+frame+16)(SP)` once a frame
|
2026-08-30 10:40:25 +02:00
|
|
|
|
pointer is set up, with the matching Go prologue/epilogue generated, so the
|
2026-07-06 09:49:50 +02:00
|
|
|
|
output is byte-identical to the Go assembler for these cases. SIMD is handled
|
2026-08-30 10:40:25 +02:00
|
|
|
|
by a VEX (AVX/AVX2) encoder (the two- and three-byte VEX prefixes with XMM/YMM
|
|
|
|
|
|
registers) across eight operand forms: the three-operand NDS form, the
|
2026-07-08 12:51:35 +02:00
|
|
|
|
two-operand reg/rm form, the immediate-shift form (plus the variable-count
|
|
|
|
|
|
shifts, which share the NDS shape with the count in an XMM register or
|
|
|
|
|
|
memory), the immediate shuffle form (`VPSHUFD`, `VPERMQ`), the
|
|
|
|
|
|
three-operand-plus-immediate form (`VSHUFPD`,
|
2026-07-07 13:57:53 +02:00
|
|
|
|
`VPERM2I128`, `VINSERTI128`), the lane-extract form (`VEXTRACTI128`,
|
|
|
|
|
|
`VEXTRACTF128`, where the YMM source occupies the reg field and the XMM or
|
|
|
|
|
|
memory destination r/m), the direction-sensitive moves (`VMOVDQU`, `VMOVUPD`,
|
2026-08-30 10:40:25 +02:00
|
|
|
|
`VMOVD`, `VMOVQ`, `VMOVSD`), the floating-point and FMA arithmetic: the
|
2026-07-15 17:13:28 +02:00
|
|
|
|
packed double operations (`VADDPD`/`VSUBPD`/`VMULPD`/`VDIVPD`/`VMINPD`/
|
|
|
|
|
|
`VMAXPD`), the unpacks (`VUNPCKHPD`/`VUNPCKLPD`), the scalar SD and SS
|
|
|
|
|
|
operations, `VMOVDDUP`, `VXORPD`, the width-changing conversions
|
|
|
|
|
|
(`VCVTDQ2PS`, `VCVTPS2PD`, `VCVTDQ2PD`, and the `VCVTPD2DQX`/`Y` and
|
|
|
|
|
|
`VCVTTPD2DQX`/`Y` spellings, whose length follows the wider source) and
|
2026-08-30 10:40:25 +02:00
|
|
|
|
`VFMADD231PD`, and the no-operand `VZEROUPPER`, together with `VPERMD` and
|
2026-07-08 12:51:35 +02:00
|
|
|
|
the scalar families (`CMOVcc`, `SETcc`, `LZCNT`/`TZCNT`, the extending moves,
|
2026-08-30 10:40:25 +02:00
|
|
|
|
`CVTSx2SD`, `IMUL3`) and the EVEX (AVX-512) prefix, the four-byte prefix with
|
|
|
|
|
|
5-bit register fields (Z0-Z31, X/Y 16-31, with the mod=11 quirk that carries
|
|
|
|
|
|
rm[4] in X̄), opmask registers (K0-K7 as operands, mask destinations and
|
|
|
|
|
|
explicit merging/zeroing masks, written the way Go writes them, as a K
|
2026-07-14 21:03:26 +02:00
|
|
|
|
operand among the operands plus a `.Z` mnemonic suffix), and the compressed
|
2026-08-30 10:40:25 +02:00
|
|
|
|
disp8×N displacement, whose multiplier follows the memory operand's size,
|
2026-07-14 21:03:26 +02:00
|
|
|
|
covering every instruction the go-flac and go-lz4 AVX2/AVX-512 kernels use,
|
2026-07-18 15:47:59 +02:00
|
|
|
|
plus the common AVX-512 F/BW integer set, the floating-point and conversion
|
2026-08-30 10:40:25 +02:00
|
|
|
|
set (the packed double and single arithmetic, the scalar SD/SS forms
|
|
|
|
|
|
(whose EVEX encodings serve masked and zeroing use), `VMOVDDUP`, the
|
2026-07-18 15:47:59 +02:00
|
|
|
|
replicating moves, and the width-changing conversions, including the
|
|
|
|
|
|
`VCVTPD2DQ`/`VCVTTPD2DQ` family whose length follows the wider source
|
|
|
|
|
|
operand), and the wider AVX-512 set: ternary logic, lane shuffles, inserts
|
|
|
|
|
|
and extracts, compares with an opmask destination, the permutes, the
|
|
|
|
|
|
expand/compress family, the broadcasts, the opmask-register instructions
|
|
|
|
|
|
(KAND/KOR/KXNOR/KADD/KUNPCK/KNOT/KSHIFTL/KORTEST and KMOVQ), the aligned
|
2026-07-19 15:58:48 +02:00
|
|
|
|
moves and the remaining extending/narrowing moves, the floating-point
|
|
|
|
|
|
helper and conversion tail (VRCP14*, VRSQRT14*, VGETEXP*, VGETMANT*,
|
|
|
|
|
|
VSCALEF*, VRNDSCALE*, VREDUCE*, VFIXUPIMM*, VRANGE*, VFPCLASS* with an
|
2026-08-30 10:40:25 +02:00
|
|
|
|
opmask destination, and the VCVT* conversions, signed, unsigned and
|
2026-07-20 16:05:08 +02:00
|
|
|
|
truncating, including the length-suffixed X/Y spellings and the
|
2026-07-21 12:59:25 +02:00
|
|
|
|
mask/vector conversions VPMOVM2*/VPMOV*2M, and the scalar conversions
|
|
|
|
|
|
between vector and general-purpose registers (VCVT{,T}S{D,S}2SI{,Q} and
|
2026-08-30 10:40:25 +02:00
|
|
|
|
the unsigned forms, VCVTSI2*/VCVTUSI2*), and gather/scatter with VSIB addressing, both the
|
2026-07-19 15:58:48 +02:00
|
|
|
|
VEX spelling with a vector mask register and the EVEX spelling with an
|
|
|
|
|
|
explicit K mask, where the EVEX length follows the VSIB index register,
|
|
|
|
|
|
not the data register. The EVEX mnemonic
|
2026-08-30 10:40:25 +02:00
|
|
|
|
suffixes (rounding modes (.RN_SAE/.RD_SAE/.RU_SAE/.RZ_SAE),
|
|
|
|
|
|
suppress-all-exceptions (.SAE) and memory broadcast (.BCST)) set the EVEX
|
2026-07-18 15:47:59 +02:00
|
|
|
|
b bit and the L'L rounding-control field (broadcast keeps the vector length
|
|
|
|
|
|
and scales disp8 by the element size), and combine with the .Z zeroing
|
|
|
|
|
|
suffix. Every encoding is validated two ways: by
|
2026-07-10 13:20:49 +02:00
|
|
|
|
round-trip decoding through `golang.org/x/arch`, and byte-for-byte against
|
2026-08-30 10:40:25 +02:00
|
|
|
|
the machine code the real Go assembler emits, a comparison that holds for
|
2026-07-10 13:20:49 +02:00
|
|
|
|
whole functions: all 27 functions of both kernels assemble to exactly the Go
|
|
|
|
|
|
toolchain's bytes, the lone exception being the displacements of the
|
|
|
|
|
|
static-constant loads, which the Go linker fills at link time.
|
2026-07-09 15:56:03 +02:00
|
|
|
|
|
|
|
|
|
|
File-level assembly (`AssembleFile`) goes beyond single functions: it
|
|
|
|
|
|
materialises the file's static symbols (`GLOBL`/`DATA`) in a data section
|
|
|
|
|
|
behind the code and resolves references to them (`mask<>(SB)`) to
|
|
|
|
|
|
RIP-relative loads whose displacements point inside the resulting image, so
|
2026-07-16 20:52:20 +02:00
|
|
|
|
the bytes are self-consistent at any base address. References to symbols no
|
|
|
|
|
|
`GLOBL` defines are kept as relocations on the function layout, and the
|
|
|
|
|
|
object-file emitters turn the whole image into a linkable object: the ELF
|
2026-08-07 22:20:26 +02:00
|
|
|
|
writer (`gasm asm --format elf`) lays the code and data out as `.text`/`.data`
|
|
|
|
|
|
sections, exports a symbol per
|
2026-08-30 10:40:25 +02:00
|
|
|
|
`TEXT` and `GLOBL` (the `<>` ones local, the rest global), emits one
|
|
|
|
|
|
PC-relative relocation per static-symbol reference, undefined external
|
|
|
|
|
|
symbols included, and appends the DWARF5 debug sections
|
|
|
|
|
|
(`.debug_abbrev`, `.debug_info`, `.debug_line`, `.debug_line_str`, and a
|
|
|
|
|
|
`.debug_frame` CFI section on amd64) so `addr2line` and GDB/LLDB can debug
|
|
|
|
|
|
the output. The GOOBJ
|
2026-07-17 18:57:04 +02:00
|
|
|
|
emitter (`gasm asm --format goobj`) writes the format the Go linker consumes
|
|
|
|
|
|
directly: the functions as non-package symbols (the way `cmd/asm` records
|
|
|
|
|
|
assembly symbols), the `GLOBL` data, one `FuncInfo` per function and the
|
2026-08-30 10:40:25 +02:00
|
|
|
|
pc-value tables (`pcsp` built from the prologue and epilogue stack
|
|
|
|
|
|
boundaries, plus flat `pcfile`, `pcline` and `pcinline` tables), so a
|
2026-07-17 18:57:04 +02:00
|
|
|
|
gasm-assembled object drops into a `go build` in place of the toolchain's.
|
|
|
|
|
|
The object preamble (the version-and-experiment header the linker compares
|
|
|
|
|
|
verbatim) is captured from the installed `go tool asm`, so the output is
|
2026-08-13 12:07:29 +02:00
|
|
|
|
always consistent with the toolchain that links it. RISC-V and LoongArch
|
|
|
|
|
|
GOOBJ emission share this emitter: the loong64 marker with
|
|
|
|
|
|
R_LOONG64_ADDR_HI/LO relocation types, and the riscv64 marker with a single
|
2026-08-13 18:27:12 +02:00
|
|
|
|
R_RISCV_PCREL_ITYPE/STYPE relocation per AUIPC pair (plus `R_RISCV_JAL` for
|
2026-08-30 10:40:25 +02:00
|
|
|
|
`CALL sym(SB)`), the model `cmd/asm`
|
|
|
|
|
|
writes, not the ELF HI20/LO12 pair, and both link into a real `go build` for
|
2026-08-13 12:07:29 +02:00
|
|
|
|
their `GOARCH`. Per function, the emitter also writes the two DWARF
|
2026-08-30 10:40:25 +02:00
|
|
|
|
symbols the linker's DWARF pass reads verbatim: the subprogram DIE
|
2026-08-13 11:24:44 +02:00
|
|
|
|
(`SDWARFFCN`) and the `.debug_line` state-machine program (`SDWARFLINES`),
|
|
|
|
|
|
both built the way `cmd/asm` builds them (the DIE carries the
|
|
|
|
|
|
R_DWTXTADDR_U4 address reference; the line program one row per source-line
|
2026-08-30 10:40:25 +02:00
|
|
|
|
change, in the same special-opcode encoding), and the pc-value deltas are
|
2026-08-13 11:24:44 +02:00
|
|
|
|
in the architecture's MinLC units, as the runtime's `pcvalue` expects.
|
2026-08-30 10:40:25 +02:00
|
|
|
|
Cross-package external references resolve on all four architectures: when
|
|
|
|
|
|
`img.Externals` is non-empty, the GOOBJ emitter locates the referenced
|
|
|
|
|
|
package's `.a` archive via `go list -json -export`, reads the GOOBJ symbol
|
|
|
|
|
|
definitions the linker reads, and wires the resolved package and symbol
|
|
|
|
|
|
indices into the emission, so a gasm-assembled object links against the
|
|
|
|
|
|
compiled packages it references.
|
2026-07-06 09:49:50 +02:00
|
|
|
|
|
2026-07-22 09:28:11 +02:00
|
|
|
|
### `verify`
|
|
|
|
|
|
|
2026-09-19 23:49:27 +02:00
|
|
|
|
The dynamic-analysis substrate. It JIT-loads assembled images into
|
2026-07-22 09:28:11 +02:00
|
|
|
|
executable memory and invokes them directly, enabling differential testing,
|
|
|
|
|
|
runtime ABI checks and coverage profiling.
|
|
|
|
|
|
|
|
|
|
|
|
The execution model is pure Go (stdlib only). `Map` copies machine code into
|
|
|
|
|
|
an anonymous `syscall.Mmap` mapping and enforces W^X (write the bytes, then
|
|
|
|
|
|
`mprotect` to read-execute). `Call` prepares a stack whose first word is the
|
|
|
|
|
|
address of an assembly trampoline (`leaveJIT`), lays the ABI0 argument
|
|
|
|
|
|
block after it, switches to that stack via `enterJIT` (which saves the Go
|
|
|
|
|
|
stack pointer in a package global and jumps to the target), and recovers
|
|
|
|
|
|
control when the function RETs into `leaveJIT` (which restores the Go stack
|
|
|
|
|
|
and returns). A 64-byte pad below the return address accommodates the
|
|
|
|
|
|
ABIInternal wrapper that the Go runtime interposes on assembly functions.
|
2026-08-30 10:40:25 +02:00
|
|
|
|
Every supported architecture carries its own hand-written trampoline pair
|
|
|
|
|
|
(`trampoline_amd64.s`, `trampoline_arm64.s`, `trampoline_riscv64.s`,
|
2026-08-30 22:27:20 +02:00
|
|
|
|
`trampoline_loong64.s`). The ABI-checked variant `CallChecked` exists for
|
|
|
|
|
|
every architecture too: `enterJITChecked` plants sentinels in the registers
|
|
|
|
|
|
the Go ABI fixes across calls (amd64 `BP`/`R14`, arm64 `R29`/`R28`, riscv64
|
|
|
|
|
|
`X27`, loong64 `R22`; the latter two keep no hardware frame pointer) and the
|
|
|
|
|
|
raw return trampoline `leaveJITCheckedRaw` verifies them, restoring the
|
2026-09-20 00:57:02 +02:00
|
|
|
|
saved registers before Go code resumes. All three non-amd64 trampolines
|
|
|
|
|
|
are validated end to end under qemu-user emulation, the loong64 one
|
|
|
|
|
|
through its raw-address leave handoff.
|
2026-09-17 20:33:18 +02:00
|
|
|
|
`gasm verify` runs the JIT checks when the host
|
2026-08-31 12:13:43 +02:00
|
|
|
|
matches the kernel's architecture and the toolchain comparisons
|
|
|
|
|
|
elsewhere.
|
2026-07-22 09:28:11 +02:00
|
|
|
|
|
|
|
|
|
|
`Load` / `LoadSource` / `LoadAST` parse, assemble and map a `.s` file in one
|
|
|
|
|
|
step, returning a `Kernel` whose `CallFunc` method marshals the argument block
|
|
|
|
|
|
by name. The image must be self-contained (no external relocations); the
|
|
|
|
|
|
assembler’s `Image.Bytes()` provides the code-and-data concatenation.
|
|
|
|
|
|
|
|
|
|
|
|
The `gasm verify` CLI subcommand exposes this: it loads a file, reports the
|
|
|
|
|
|
available functions and (with `-smoke`) calls each NOSPLIT function with zeroed
|
2026-08-30 10:40:25 +02:00
|
|
|
|
arguments to confirm the trampoline round-trips. The `-smoke` and `-abi`
|
|
|
|
|
|
sweeps run in parallel and each inside a child process, so a function that
|
|
|
|
|
|
faults is reported without ending the sweep. `gasm verify --fuzz` combines
|
2026-08-05 20:08:56 +02:00
|
|
|
|
ABI checks (sentinel registers, canary, stack bounds) with differential fuzz
|
|
|
|
|
|
testing, comparing the JIT-assembled kernel against the portable Go reference
|
|
|
|
|
|
bit-for-bit while verifying the ABI contract on every iteration. When a fuzz
|
|
|
|
|
|
iteration crashes or mismatches, `FuzzResult.CrashInput` stores the exact input
|
2026-08-05 21:15:05 +02:00
|
|
|
|
for reproducibility. `gasm verify --call <func> --buf name:size:pattern`
|
|
|
|
|
|
invokes a single function with user-supplied buffers (patterns: zero, ones,
|
2026-08-30 10:40:25 +02:00
|
|
|
|
seq, or hex), printing the ABI0 argument block before and after the call,
|
2026-08-05 21:15:05 +02:00
|
|
|
|
useful for partial functions (e.g. decoders) that crash on random input but
|
2026-08-30 10:40:25 +02:00
|
|
|
|
should succeed on valid data; `--args name=value,...` supplies scalar
|
2026-08-30 21:31:05 +02:00
|
|
|
|
arguments (decimal or `0x` hex) alongside the buffers. `--save-corpus`
|
|
|
|
|
|
records every input that crashes or mismatches as replayable JSON, and
|
|
|
|
|
|
`--replay` re-runs saved entries in isolated child processes, reporting
|
|
|
|
|
|
whether each reproduces. `gasm verify
|
2026-08-30 10:40:25 +02:00
|
|
|
|
--ground-truth` compares the
|
2026-08-05 21:15:05 +02:00
|
|
|
|
assembled machine code byte-for-byte against `go tool asm` (relocation sites
|
|
|
|
|
|
masked), reporting any encoding drift.
|
2026-07-22 09:28:11 +02:00
|
|
|
|
|
2026-08-01 05:22:00 +02:00
|
|
|
|
### `debug`
|
|
|
|
|
|
|
2026-08-21 00:50:31 +02:00
|
|
|
|
The interactive debugger (all four architectures). It launches the target
|
2026-08-01 05:22:00 +02:00
|
|
|
|
function in a child process that maps the JIT code, calls
|
|
|
|
|
|
`PTRACE_TRACEME`, and stops; the parent attaches via ptrace and controls
|
|
|
|
|
|
execution. Breakpoints are patched as INT3 bytes through `/proc/pid/mem`
|
|
|
|
|
|
(PTRACE_PEEKTEXT is unreliable with Go's multi-threaded runtime).
|
|
|
|
|
|
The child pins its goroutine to the OS thread with `runtime.LockOSThread`
|
|
|
|
|
|
so the traced thread is the one executing JIT code. The REPL provides
|
2026-08-05 20:08:56 +02:00
|
|
|
|
single-step, register inspection (GPR + YMM/XMM via `PTRACE_GETFPREGS`),
|
|
|
|
|
|
label resolution, named buffer allocation with pattern filling
|
2026-08-30 10:40:25 +02:00
|
|
|
|
(`--buf name:size:pattern`: zero, ones, seq, or hex), and breakpoint
|
|
|
|
|
|
management. Breakpoints accept conditions
|
|
|
|
|
|
(`break <label> if <reg> <op> <val>`, including register-against-register
|
|
|
|
|
|
comparisons), and hardware watchpoints work on all four architectures.
|
|
|
|
|
|
For non-interactive use, `--script` runs REPL commands from a file (or
|
|
|
|
|
|
stdin) and exits, `--timeout` kills the debuggee when a run hangs (the
|
|
|
|
|
|
watchdog is armed before the ptrace attach, so a sandboxed debuggee cannot
|
|
|
|
|
|
block it), and `--cover` runs to completion with a breakpoint on every
|
2026-09-19 23:49:27 +02:00
|
|
|
|
instruction and reports which instructions executed and how often.
|
2026-08-01 05:22:00 +02:00
|
|
|
|
|
2026-09-17 20:33:18 +02:00
|
|
|
|
### Extending the toolkit
|
2026-07-06 09:49:50 +02:00
|
|
|
|
|
|
|
|
|
|
- **New architecture:** add an entry to the generator in `_gen`, run
|
|
|
|
|
|
`just gen`, and add a `buildXXX()` register file plus a case in `ForArch`.
|
|
|
|
|
|
- **New lint rule:** add a function in `lint` and a rule-code constant.
|
|
|
|
|
|
- **New LSP feature:** add a method case in `dispatch` and a handler.
|
|
|
|
|
|
|
2026-09-17 20:33:18 +02:00
|
|
|
|
## Data flow
|
|
|
|
|
|
|
|
|
|
|
|
The main operation, assembling one file:
|
|
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
sequenceDiagram
|
|
|
|
|
|
participant User
|
|
|
|
|
|
participant CLI as gasm CLI
|
|
|
|
|
|
participant Parser as parser
|
|
|
|
|
|
participant Asm as asm
|
|
|
|
|
|
participant Go as go toolchain
|
|
|
|
|
|
User->>CLI: gasm asm --format goobj -p pkg -o k.o k_amd64.s
|
|
|
|
|
|
CLI->>Parser: Parse(path, src)
|
|
|
|
|
|
Parser-->>CLI: AST, diagnostics
|
|
|
|
|
|
CLI->>Asm: AssembleFile(AST)
|
|
|
|
|
|
Asm->>Asm: encode operands, settle label offsets, lay out data
|
|
|
|
|
|
Asm-->>CLI: Image, code and data and relocations
|
|
|
|
|
|
CLI->>Asm: GOObject(pkg, path)
|
|
|
|
|
|
Asm->>Go: go list -json -export, externals only
|
|
|
|
|
|
Go-->>Asm: package and symbol indices
|
|
|
|
|
|
Asm-->>CLI: Go object bytes
|
|
|
|
|
|
CLI-->>User: wrote N bytes to k.o
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Errors are produced where the parse or the encoding fails and become values at
|
|
|
|
|
|
the CLI boundary: the parser returns a diagnostic list and never aborts a file,
|
|
|
|
|
|
`AssembleFile` returns an error, and `cmd/gasm` prints what it has to stderr
|
|
|
|
|
|
and returns a non-zero exit code. The formatter and the linter take the same
|
|
|
|
|
|
AST by a different route: `gasm fmt` re-spaces the token stream and `gasm lint`
|
|
|
|
|
|
walks the parsed file, so neither depends on an encoding.
|
|
|
|
|
|
|
|
|
|
|
|
## State and lifetime
|
|
|
|
|
|
|
|
|
|
|
|
- The analysis packages (`lexer`, `parser`, `format`, `lint`, `arch`) hold only
|
|
|
|
|
|
read-only lookup tables and no mutable state: every call allocates its own
|
|
|
|
|
|
tokens and AST, and any number of goroutines may read the `arch` tables.
|
|
|
|
|
|
- A `verify.Kernel` owns one executable mapping, which `Close` releases. The
|
|
|
|
|
|
JIT trampolines keep the Go stack pointer and the checked-call sentinels in
|
|
|
|
|
|
package globals, so a call is a process-wide, one-at-a-time operation. The
|
|
|
|
|
|
`gasm verify` sweeps therefore run each function in a child process, which
|
|
|
|
|
|
contains a crash and keeps the globals unshared.
|
|
|
|
|
|
- `lsp.Server` is long-lived: it runs a single read and dispatch loop over the
|
|
|
|
|
|
stream and touches its document store only from that loop, so one server
|
|
|
|
|
|
serves one connection.
|
|
|
|
|
|
- A `debug.Session` owns a traced child process and pins its goroutine to the
|
|
|
|
|
|
forking OS thread, because ptrace requests must stay on that thread.
|
|
|
|
|
|
|
|
|
|
|
|
## Dependencies
|
|
|
|
|
|
|
|
|
|
|
|
- **`golang.org/x/arch`** (v0.30.0) is the one module dependency: it is the
|
|
|
|
|
|
disassembler backend (`gasm dis` and the debugger's listings) and the source
|
|
|
|
|
|
of the register metadata the encoder consults (`asm/reg.go`, `asm/vex.go`).
|
|
|
|
|
|
The tests additionally decode through it to validate the encodings.
|
|
|
|
|
|
- **The Go toolchain**, as an oracle and never as a library: `go tool asm`
|
|
|
|
|
|
supplies the object preamble and the ground truth for `gasm verify
|
|
|
|
|
|
--ground-truth`, `go list -json -export` locates the archives of the packages
|
|
|
|
|
|
a GOOBJ object references, and `_gen` parses
|
|
|
|
|
|
`$GOROOT/src/cmd/internal/obj/<arch>/anames.go` to rebuild the tables.
|
|
|
|
|
|
- **Linux process interfaces** for the dynamic work: `mmap` and `mprotect` for
|
|
|
|
|
|
the JIT mapping, ptrace with `/proc/pid/mem` for the debugger. That is why
|
|
|
|
|
|
`verify` runs a JIT check only when the host architecture matches the
|
|
|
|
|
|
kernel's, and why `debug` is Linux-only.
|