From 837231c068f76407f66fbe3aa579a855f58e1414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Mon, 21 Sep 2026 19:49:04 +0200 Subject: [PATCH] docs(asm): open the assembly language reference Assisted-by: GLM 5.3 Flash --- CHANGELOG.md | 9 +++ README.md | 6 +- docs/asm/DIRECTIVES.md | 148 +++++++++++++++++++++++++++++++++++++++ docs/asm/LANGUAGE.md | 140 ++++++++++++++++++++++++++++++++++++ docs/asm/OPERANDS.md | 114 ++++++++++++++++++++++++++++++ docs/asm/PREPROCESSOR.md | 79 +++++++++++++++++++++ docs/asm/README.md | 53 ++++++++++++++ docs/asm/RUNTIME.md | 120 +++++++++++++++++++++++++++++++ 8 files changed, 667 insertions(+), 2 deletions(-) create mode 100644 docs/asm/DIRECTIVES.md create mode 100644 docs/asm/LANGUAGE.md create mode 100644 docs/asm/OPERANDS.md create mode 100644 docs/asm/PREPROCESSOR.md create mode 100644 docs/asm/README.md create mode 100644 docs/asm/RUNTIME.md diff --git a/CHANGELOG.md b/CHANGELOG.md index fd16b22..30f1c7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **The Plan 9 assembly language reference.** [docs/asm/](docs/asm/README.md) + opens the complete language reference with its common core: the lexicon, + statement structure and constant expressions, the operand grammar with + the pseudo-registers and symbol naming, the directives and the function + flag vocabulary, preprocessing with `#define` and `#include`, and the + Go-embedded layer (ABI0, prototypes, `go_asm.h`, `funcdata.h` and the + runtime contract). Every claim is verified against `go tool asm` of + Go 1.27.1 and gasm's differential tests; the per-architecture pages and + generated instruction appendices follow. - **GOOBJ format specification.** [docs/GOOBJ.md](docs/GOOBJ.md) documents the Go object file format in full: both containers, the 96 byte header and all 19 blocks, every structure with its byte diff --git a/README.md b/README.md index f9d8fd8..4937744 100644 --- a/README.md +++ b/README.md @@ -186,8 +186,9 @@ verified the way the code is verified: an encoding documented here is one that differential tests against `go tool asm` confirm byte-for-byte, and a format field documented here is one the linker demonstrably reads. The work has begun: [docs/GOOBJ.md](docs/GOOBJ.md) -specifies the object file format completely. The assembler language -reference follows. +specifies the object file format completely, and +[docs/asm/README.md](docs/asm/README.md) opens the language reference +with its common core. The per-architecture pages follow. ## Direction @@ -315,6 +316,7 @@ recipe. them - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow - [docs/GOOBJ.md](docs/GOOBJ.md): the GOOBJ object file format specification +- [docs/asm/](docs/asm/README.md): the Plan 9 assembly language reference - [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md): development setup and recipes - [CHANGELOG.md](CHANGELOG.md): release history diff --git a/docs/asm/DIRECTIVES.md b/docs/asm/DIRECTIVES.md new file mode 100644 index 0000000..55e9ccb --- /dev/null +++ b/docs/asm/DIRECTIVES.md @@ -0,0 +1,148 @@ +# Directives: TEXT, DATA, GLOBL and the annotations + +Layer 1, the common language, with the flag vocabulary both layers share. +Verified against `go tool asm` of Go 1.27.1, against the shipped headers +`textflag.h` and `funcdata.h` in `$GOROOT/pkg/include`, and against gasm's +parser. Where gasm extends a directive, the extension says so and is marked. + +Six directives exist. Three define things: TEXT, DATA, GLOBL. Three +annotate: FUNCDATA, PCDATA, PCALIGN. + +## TEXT + +```text +// func Add(a, b int64) int64 +TEXT ·Add(SB), NOSPLIT, $0-24 + ...instructions... + RET +``` + +```text +TEXT symbol(SB), [flags,] $framesize[-argsize] +``` + +- The symbol is an `·Name(SB)` reference into the current package, or a + fully qualified name. +- The optional flag argument is a constant expression, normally an OR of the + names from `textflag.h`, the table below. Without `#include "textflag.h"` + the names are not macros and the assembler reports the misleading error + `illegal or missing addressing mode for symbol NOSPLIT`: include the + header first. +- `$framesize-argsize` is two constants, not a subtraction: the local frame + size in bytes, and the caller's argument area in bytes. The argument size + may be omitted entirely, `$16`, which marks the argument size unknown + (0x80000000 in the object, the value of `ArgsSizeUnknown` from + `funcdata.h`); a frame size may be negative only in the generated ABI + wrappers. +- A function whose last instruction is not a branch cannot fall through into + the next TEXT: the toolchain appends a jump to itself, so end functions + with `RET` deliberately. +- One TEXT per symbol; redeclaring is an error. The TEXT line also fixes the + function's source line for traceback: it is the line number that pcln + reports for the function's start. + +The framesize and argsize fields do real work: the framesize drives the +stack-split preamble (RUNTIME.md carries the contract), and both travel into +the FuncInfo record of the object (GOOBJ.md carries its layout). + +### The flag table + +Values from `textflag.h`, in agreement with `cmd/internal/obj/textflag.go`: + +| Name | Value | Applies to | Meaning | +|---|---|---|---| +| NOPROF | 1 | both | do not profile; deprecated | +| DUPOK | 2 | both | the linker may keep one of several duplicates | +| NOSPLIT | 4 | TEXT | no stack-split preamble | +| RODATA | 8 | data | put the data in a read-only section | +| NOPTR | 16 | data | the data contains no pointers | +| WRAPPER | 32 | TEXT | a wrapper; must not disable `recover` | +| NEEDCTXT | 64 | TEXT | a closure consuming the context register | +| TLSBSS | 256 | data | a thread local word in BSS | +| NOFRAME | 512 | TEXT | no frame setup; only valid with a frame size of 0 | +| REFLECTMETHOD | 1024 | TEXT | the function calls `reflect.Type.Method` or `MethodByName` | +| TOPFRAME | 2048 | TEXT | the outermost frame; unwinders stop here | +| ABIWRAPPER | 4096 | TEXT | an ABI transition wrapper | + +Rules with teeth: + +- `NOSPLIT` removes the split check, so the frame plus everything the + function calls must fit in the stack segment that remains. It exists to + protect the splitting code itself; reaching for it to save two instructions + is how stack overflows corrupt memory. On amd64 the assembler additionally + marks small leaf functions NoSplit itself and omits the check, so the + absence of the preamble is not proof the flag was written. +- A TEXT whose symbol is declared `ABIInternal` must carry NOSPLIT: the + assembler rejects it otherwise, because it cannot generate + the split path for a register-ABI function. +- `RODATA` implies NOPTR for the garbage collector. + +## DATA + +```text +DATA ·table+0(SB)/8, $0x0102030405060708 +DATA ·msg+0(SB)/14, $"hello, world\n" +GLOBL ·msg(SB), RODATA, $14 +``` + +```text +DATA symbol+offset(SB)/width, value +``` + +- `width` is exactly 1, 2, 4 or 8: the initialiser is written into the data + image at `symbol+offset` in that many bytes. +- The value is an integer or character constant of the width, or a string + literal whose byte length equals the width exactly; escapes count. Long + data is written as successive DATA lines at increasing offsets; bytes the + directives never name are zero. +- Every symbol initialised with DATA ends with a GLOBL line declaring its + total size, after all of its DATA lines. + +A symbol containing pointers cannot be defined in assembly, because the +collector cannot see into it: define it in Go and refer to it by name. As a +rule, data that is not read-only belongs in Go. + +Extension, gasm only: a DATA initialiser may name a symbol, +`DATA ·fn+0(SB)/8, $·handler(SB)`, which gasm lays down as an absolute +relocation on that field. The toolchain offers no ground truth for this +form; gasm's behaviour is verified by linking and execution. + +## GLOBL + +```text +GLOBL symbol(SB), [flags,] $size +``` + +Declares the symbol global with its total size in bytes. The useful flags +are RODATA, NOPTR, DUPOK and TLSBSS from the table above. Uninitialised +bytes are zero, which makes GLOBL with no DATA the language's BSS. + +## FUNCDATA and PCDATA + +```text +FUNCDATA $functypeid, symbol(SB) +PCDATA $pctypeid, $value +``` + +The compiler's annotations for the garbage collector and traceback, named by +the ids in `funcdata.h`: FUNCDATA 0 to 7 (args pointer maps, locals pointer +maps, stack objects, inline tree, open-coded defer info, argument info, +argument liveness, wrap info), PCDATA 0 to 4 (unsafe point, stack map index, +inline tree index, argument liveness index, panic bounds). Assembly code +normally reaches them only through the macro forms in `funcdata.h`, which +RUNTIME.md explains. Outside the macros, hand-written PCDATA is meaningless: +the values are pc-value tables the compiler builds from its own view of the +program. + +## PCALIGN + +```text +PCALIGN $32 +``` + +Pads the code so that the next instruction lands on the given boundary, +which must be a power of two and at least the target's instruction +alignment. Supported on amd64, arm64, ppc64, loong64 and riscv64. The +padding instructions are the target's NOP encoding, so the bytes between +functions differ from what the instruction stream alone would produce, which +matters to anyone comparing encodings byte for byte. diff --git a/docs/asm/LANGUAGE.md b/docs/asm/LANGUAGE.md new file mode 100644 index 0000000..94d7066 --- /dev/null +++ b/docs/asm/LANGUAGE.md @@ -0,0 +1,140 @@ +# Language: lexicon, statements and expressions + +Layer 1, the common language, the same on every target. Verified against +`go tool asm` of Go 1.27.1 and against gasm's parser, which is differentially +tested against the toolchain. The authoritative sources behind this page are +the assembler's lexer (`cmd/asm/internal/lex`), its parser +(`cmd/asm/internal/asm/parse.go`) and the toolchain's own test data. + +## Source files and targets + +An assembly source is a `.s` file. The Go build convention names a +target-specific file with the architecture suffix, `_amd64.s`, `_arm64.s`, +`_riscv64.s` or `_loong64.s`; files without a suffix are portable across +targets. The same assembler program assembles every target: `go tool asm` +picks the target from the `GOOS` and `GOARCH` environment variables, and gasm +from the file name suffix or the `--arch` flag. + +## Character set and identifiers + +Sources are ASCII text. An identifier is a sequence of ASCII letters, digits +and underscores, digits never first, with exactly two additions: + +- U+00B7, the middle dot `·`, stands for the period in a symbol's + package-qualified name; +- U+2215, the division slash `∕`, stands for the slash in a package path. + +The two substitutions exist because the parser treats a real period and a +real slash as punctuation. The syntax is otherwise uppercase throughout: +instructions, registers and directives are written in upper case. The one +inherited exception is the `g` register name on 32-bit ARM. + +## Comments + +Two comment forms, both Go's: + +```text +// a line comment +/* a block comment */ +``` + +A comment of the form `//go:build` or the legacy `+build` comment is not a +plain comment: the lexer reports it to the build system as a build +constraint. + +## Statements + +The grammar of one line, from the parser: + +```text +{label:} WORD[.qualifier] [ arg {, arg} ] (';' | '\n') +``` + +- A **label** is an identifier followed by a colon. Labels are + function-local: two functions in one file may reuse the same name, and a + reference resolves within the function that contains it. A branch + instruction names its target with a bare label operand, and the assembler + resolves it PC-relative. The explicit forms `offset(PC)`, a constant + counting instructions from the branch, and `name(SB)`, a cross-function + static reference, appear as branch targets as well. +- **WORD** is the instruction or directive name, upper case. On the ARM + family the word may carry a dot qualifier selecting a condition or shift + mode, such as the condition suffixes on 32-bit ARM; the amd64, arm64, + riscv64 and loong64 assemblies carry no instruction qualifiers apart from + their own width suffixes, which are part of the mnemonic. +- **Arguments** are separated by commas, with no trailing comma. +- A statement ends at a newline or at a semicolon, so several statements fit + on one line separated by `;`. Blank lines are free. + +The first word of a line is a directive if it is one of the directive names +(TEXT, DATA, GLOBL, FUNCDATA, PCDATA, PCALIGN) and an instruction otherwise. +Unknown instruction names are errors; the instruction set is the set the +toolchain itself defines per target, plus the common pseudo-instructions. + +## Literals + +| Form | Examples | Notes | +|---|---|---| +| Integer | `0`, `42`, `0x2a`, `0o52`, `0b101010`, `1_000` | decimal, hexadecimal, octal and binary forms with Go's digit separators | +| Character | `'a'`, `'\n'`, `'\x41'` | single quoted, Go escape rules | +| String | `"this program can only run\n"` | double quoted, Go escape rules; accepted where an operand takes raw bytes, in practice a DATA initialiser | +| Float | `1.5`, `1e9` | accepted by the lexer; only meaningful where the target's encoding takes a float operand | + +## Expressions + +Constant expressions may appear wherever a constant is expected: in +immediates after `$`, in memory offsets, in frame and data sizes. The +evaluator works on unsigned 64-bit values with Go's operator precedence, and +the parser states its grammar in exactly those terms: + +```text +expr = term { '+' term | '-' term | '|' term | '^' term } +term = factor { '*' factor | '/' factor | '%' factor | '<<' factor | '>>' factor | '&' factor } +factor = const | '+' factor | '-' factor | '~' factor | '(' expr ')' +``` + +Two consequences are worth naming, because the arithmetic surprises people +who read it as C: + +- Shifts bind at the multiplicative level, next to `*` and `&`, while `|` + and `^` bind at the additive level. `$x<<1|3` computes `(x<<1)|3`, which + differs from `x*2+3` whenever `x` is odd. Plan 9 arithmetic is Go + precedence applied to a byte-oriented language, not the C expression it + resembles. +- The evaluator is unsigned and guarded: division or modulo by zero is an + error, and so is dividing a value with the high bit set; shift counts must + be non-negative; and a right shift of a value with the high bit set is + rejected rather than sign-extended. + +An address expression such as `(index*4)(base)` is evaluated at assembly +time only if every name in it is a constant; a name that resolves to a +symbol turns the expression into a relocation request, never into a folded +constant. + +Named constants enter expressions through the preprocessor (`#define`, +`-D`) and, in Go-embedded packages, through the generated `go_asm.h`; see +PREPROCESSOR.md and RUNTIME.md. + +## The common pseudo-instructions + +A handful of instructions exist on every target, assembled by the assembler +itself rather than the encoder: `NOP`, which emits the target's no-operation +encoding, and the frame-management pseudo-instructions the compiler emits +(`FUNCDATA`, `PCDATA`) which DIRECTIVES.md specifies. Everything else is the +target's own instruction set, and the assembler knows only the instructions +the toolchain's compiler emits; a hand-written kernel wanting more lays the +encoding down with `BYTE` on amd64 or waits for the extended layer. + +## Case study: three lines, decomposed + +```text +B.EQ 1(PC) // arm64: condition qualifier on the mnemonic, + // target one instruction past the branch +JMP done // every target: bare label, function-local, + // resolved PC-relative +MOVQ $reader__size>>3, CX // amd64: expression over a go_asm.h constant +``` + +The first shows a qualifier and the explicit relative target form; the second +the ordinary label reference; the third an expression over a generated +constant. Labels are reusable between functions without conflict. diff --git a/docs/asm/OPERANDS.md b/docs/asm/OPERANDS.md new file mode 100644 index 0000000..2e7960e --- /dev/null +++ b/docs/asm/OPERANDS.md @@ -0,0 +1,114 @@ +# Operands: grammar, pseudo-registers, addressing and symbols + +Layer 1, the common language. Verified against `go tool asm` of Go 1.27.1 and +against gasm's parser. The operand grammar is the part of the language that +varies most between targets, so this page fixes the common grammar and the +pseudo-registers; the per architecture pages carry the register names and the +addressing quirks each target adds. + +## The four operand kinds + +Every operand is one of four kinds: + +```text +R1 register +$4 immediate +label branch target or symbol +-8(BX)(DI*4) memory +``` + +**Operands go source first, destination last**: `MOVQ x+0(FP), AX` loads the +argument into AX. This is the opposite of Intel order and the same order as +AT&T, with the sigils removed: registers are bare names, immediates take +`$`, memory is `offset(base)`. + +## Registers + +A register operand is its bare name, with no prefix: `AX`, `X15`, `R14` on +amd64; `R0` to `R30`, `ZR`, `V0` to `V31` on arm64; `X0` to `X31`, `F0` to +`F31`, `V0` on riscv64; `R0` to `R31`, `F0` to `F31`, `V0` on loong64. +Sub-register and width selection rides the mnemonic, not the operand: the +amd64 family spells `MOVB`, `MOVW`, `MOVL`, `MOVQ`, and the arm64 family +suffices `B`, `H`, `S`, `D`, `Q` on the shared forms. Each architecture page +lists its registers and the reserved ones. + +## Immediates + +`$` introduces a constant: `$42`, `$-1`, `$0x2a`, `$'A'`, `$bufSize`. The +`$` applies to the whole constant expression that follows, so +`$(4*8+reader__size)` is one immediate. Without the `$`, a number in operand +position is an address, not a value; the classic error `ADDQ 1, AX` asks the +assembler for the byte at address 1. + +The one place a `$` number is not an immediate is the frame and argument +size field of TEXT, `$16-24`, which is two separate constants and not a +subtraction; DIRECTIVES.md specifies it. + +## Memory + +```text +offset(base) +offset(base)(index*scale) +``` + +Both parts are optional where the target allows them: `(BX)` is the memory +at BX, `foo+16(SB)` is a global, and on amd64 `foo+32(SP)(R9*8)` adds a +scaled index. `offset` is a constant expression, optionally carrying a +symbol name. The extensions beyond `offset(base)` are where the targets +diverge, and each belongs to its architecture page: amd64 carries the +`index*scale` form with scale 1, 2, 4 or 8 and its own rules on which +registers may index; loong64 writes base plus index as `(R4)(R5)`; the ARM +family attaches shift amounts to the index register in its own spelling. + +The address arithmetic is on **byte addresses**: the offset is added to the +base as it stands, whatever the operand width of the instruction. Loading +the third 8-byte word of an array at BX is `16(BX)`, not `2(BX)`. + +## The four pseudo-registers + +Four names denote locations no target register holds, and they mean the same +on every architecture: + +- **FP**, the frame pointer: the arguments and results of the current + function, at positive offsets, in the order the Go prototype declares + them. Every FP reference must carry a name: `x+0(FP)`, and an unnamed + `0(FP)` is rejected. Results follow arguments; an unnamed result is called + `ret`. +- **SP**, the virtual stack pointer: the high end of the function's local + frame, so locals live at negative offsets, `x-8(SP)`. A reference without + a name and without a plus, `-8(SP)`, addresses the **hardware** stack + pointer instead: the two spellings are one character apart and mean + different registers. That is the sharpest edge in the language and the + source of the deepest bugs. +- **SB**, the static base: the origin of memory, used for globals and + cross-package symbols, always with a name: `foo(SB)`, `foo+4(SB)`. +- **PC**, the program counter: branch targets, and the explicit relative + form `1(PC)`. + +## Symbol names + +A symbol's full name is the package path, a period, and the base name. In +source, the period is written U+00B7 (`·`) and a slash in the path U+2215 +(`∕`), because the parser treats the ASCII forms as punctuation. Inside the +package's own file, `·Name` is enough and is the preferred spelling, since +it survives a rename of the import path. + +| Spelling | Meaning | +|---|---| +| `·Name(SB)` | this package's Name | +| `runtime·morestack(SB)` | another package's morestack | +| `sourcedock.dev∕petrbalvin∕pkg·Name(SB)` | fully qualified | +| `msg<>(SB)` | file-local, the static of this language; `<>` also makes the ABI field static in the object | +| `Name(SB)` | ABI-qualified reference, the ABI in angle brackets after the name | + +The object file these symbols produce, with the index rules that decide what +is referenced by name and what by index, is specified in +[GOOBJ.md](../GOOBJ.md). + +## What vet adds in Go + +Inside a Go package, `go vet`'s asmdecl analyzer checks every FP offset and +name against the Go prototype, and checks the declared argument area against +the frame. That layer, the prototype requirement and `go_asm.h`, belongs to +RUNTIME.md; the grammar above is the whole of what the assembler itself +requires. diff --git a/docs/asm/PREPROCESSOR.md b/docs/asm/PREPROCESSOR.md new file mode 100644 index 0000000..1c100b8 --- /dev/null +++ b/docs/asm/PREPROCESSOR.md @@ -0,0 +1,79 @@ +# Preprocessing: include, define and selection + +Layer 1, the common language. Verified against the preprocessor inside +`go tool asm` of Go 1.27.1 (`cmd/asm/internal/lex`), whose directives are +`#define`, `#undef`, `#include`, `#ifdef`, `#ifndef`, `#else`, `#endif` and +`#line`, and against gasm's implementation, which is differentially tested +against the toolchain's. + +Input runs through a simplified C preprocessor before the parser sees it. +The set is deliberately small: there is no `#if` with constant expressions +and no token pasting with `##`. `#line` is honoured, so it changes the +positions the assembler reports and records. + +## #include + +```text +#include "textflag.h" +#include "go_asm.h" +#include "defs_linux_amd64.h" +``` + +The search path, in order: the directory of the including file, then the +directories given by repeatable `-I` flags. The assembler seeds no default +of its own: a bare `go tool asm` invocation finds none of the standard +headers, and it is the `go` build system that passes `$GOROOT/pkg/include` +among the `-I` directories when it drives the build. That directory ships +`textflag.h`, `funcdata.h` and the per architecture register headers. +Includes nest; a file included twice through different paths is processed +twice, which is why headers guard their defines. + +## #define and #undef + +```text +#define bufSize 1024 +#define MOVD(d, s) MOVQ s, d +#undef bufSize +``` + +- An object macro replaces its name with its token sequence at the point of + use. +- A parameterised macro takes its arguments in parentheses and substitutes + them into the body. Macro parameters compose with the rest of the + language: an argument used with an element suffix, as in `A.S4` on the + vector forms, substitutes correctly. +- Redefinition is an error; `#undef` first, or pick a new name. +- The `-D name[=value]` flag predefines an object macro from the command + line, repeatable, exactly as `#define` would; a `-D` without a value + defines the name as `1`. +- Expansion happens when the name is used, so a macro may expand to + instructions, operands or fragments of either, and a macro body may use + macros defined before it. + +`textflag.h` and `funcdata.h` are themselves ordinary `#define` files: the +flag names and the runtime macros are preprocessor definitions, not language +keywords. That is why a missing include produces a parser error at the first +use of `NOSPLIT` rather than a complaint about the name. + +## #ifdef, #ifndef, #else, #endif + +```text +#ifdef GOOS_windows + #define SYSCALL_INT 0x2b +#endif +``` + +Selection is by defined-name only: `#ifdef`, `#ifndef`, `#else`, `#endif`, +nesting freely. There is no `#if defined(x) && y`, because the preprocessor +evaluates no expressions; reach that with a build-tag Go file generating a +header, which is exactly how the runtime's own `go_asm.h` and defs headers +are produced. + +## What preprocessing does not cover + +The preprocessor is textual and runs first, so it knows nothing of assembly +semantics: it does not check that a macro expansion is a legal instruction, +and it does not participate in the constant expression evaluator, which runs +later, in the parser. A constant folded with `#define` and a constant folded +in an operand expression end at the same value through different doors; +GOOBJ.md records both in the object identically. diff --git a/docs/asm/README.md b/docs/asm/README.md new file mode 100644 index 0000000..a0f96e6 --- /dev/null +++ b/docs/asm/README.md @@ -0,0 +1,53 @@ +# The Plan 9 assembly language + +This directory is the reference for the Plan 9 assembly language as the Go +toolchain and gasm accept it, written to be complete enough to implement +against. It exists because no such reference exists upstream: Go documents +the language on a single page, and the rest of the knowledge lives in the +toolchain's source and in the practice of reading it. + +Every page carries the same conformance statement: which layer of the system +it describes, which toolchain release it was verified against, and how the +claims were checked. Pages in this directory are verified against Go 1.27.1 +and against gasm's own differential test suite, which compares gasm's +behaviour with `go tool asm` byte for byte and output for output. + +## The three layers + +The reference deliberately separates three layers, because their rules have +different owners and different lifetimes: + +1. **The common language** (LANGUAGE, OPERANDS, DIRECTIVES, + PREPROCESSOR): the syntax, operands, directives and preprocessing, the + same on every target and meaningful without a Go runtime. +2. **The Go-embedded layer** (RUNTIME): everything that exists only because + the code runs inside a Go program: the ABI0 contract, generated wrappers, + `go_asm.h`, the garbage collector annotations and `go vet` checks. +3. **The standalone layer** (STANDALONE, planned with the standalone + compilation phase): using the language outside Go, through gasm's ELF + output and the extended instruction set, where the toolchain offers no + ground truth and execution testing is the only verification. + +A rule stated in layer 1 holds on every target. A rule stated in layer 2 +says which part of the Go machinery imposes it. Nothing in layer 3 changes +layers 1 or 2; it extends them. + +## Pages + +| Page | Layer | Contents | +|---|---|---| +| [LANGUAGE.md](LANGUAGE.md) | 1 | lexicon, statement structure, labels, literals, expressions | +| [OPERANDS.md](OPERANDS.md) | 1 | operand grammar, pseudo-registers, addressing modes, symbol naming | +| [DIRECTIVES.md](DIRECTIVES.md) | 1 | TEXT, DATA, GLOBL, FUNCDATA, PCDATA, PCALIGN and the function flags | +| [PREPROCESSOR.md](PREPROCESSOR.md) | 1 | `#include`, `#define`, `#ifdef` and friends, `-D`, `-I` | +| [RUNTIME.md](RUNTIME.md) | 2 | ABI0, prototypes, `go_asm.h`, `funcdata.h`, `go vet` | +| AMD64, ARM64, RISCV64, LOONG64 | 1, 3 | per architecture: registers, conventions, addressing, instruction families and the generated instruction appendices | +| STANDALONE.md | 3 | the language outside Go | + +## Status + +The common-language core and the Go-embedded layer are written and verified. +The four per-architecture pages and their generated instruction appendices +follow, architecture by architecture; STANDALONE.md lands with the standalone +compilation phase. The object format these pages feed is specified in +[GOOBJ.md](../GOOBJ.md). diff --git a/docs/asm/RUNTIME.md b/docs/asm/RUNTIME.md new file mode 100644 index 0000000..b504649 --- /dev/null +++ b/docs/asm/RUNTIME.md @@ -0,0 +1,120 @@ +# The Go-embedded layer: ABI0, prototypes and the runtime contract + +Layer 2: everything that exists only because the assembly runs inside a Go +program. Without a Go runtime this page does not apply; the language of +OPERANDS.md and DIRECTIVES.md still does. Verified against Go 1.27.1, against +the shipped `funcdata.h` header, and against the object files the toolchain +produces, which were parsed and checked field by field while writing +[GOOBJ.md](../GOOBJ.md). + +## Hand-written assembly is ABI0 + +Go functions compiled from source use ABIInternal, the register-based +calling convention, which the toolchain documents as unstable and free to +change between releases. A `.s` function is written against ABI0, the stack +based convention: arguments and results live in the caller's frame at +positive FP offsets, byte-addressed, in declaration order, with no registers +assigned at all. The toolchain generates the wrapper that translates between +the two; a caller in Go calling an assembly function goes through it, and it +is marked `ABIWRAPPER` in the object. Hand-writing a bridge is never needed +and never correct. + +## Every assembly function carries a Go prototype + +```go +package add + +func Add(x, y int64) int64 +``` + +The body-less declaration is not optional, and not only for the linker: it +is what tells the garbage collector which arguments and results hold +pointers, and what `go vet` checks the assembly against. Even a function +nothing in Go calls gets one. Consequences: + +- The FP operand names and offsets are checked by vet's asmdecl analyzer + against the prototype: `x+0(FP)` must name an argument that exists, at the + offset the prototype says. A file that assembles and links can still fail + vet. +- The declared argument area in `$framesize-argsize` is checked against the + prototype's size. An omitted argsize marks the argument size unknown + (0x80000000 in the object, the value of `ArgsSizeUnknown` from + `funcdata.h`), which is the normal spelling for functions with no Go + callers. +- `//go:noescape` on the declaration tells the compiler that a pointer + argument does not escape, for assembly that keeps the pointer beyond the + call. + +## The frame, the stack and the collector + +The runtime owns the stack and the pointer map, and assembly must hold up +its end of four rules: + +1. **Arguments are initialised on entry; results are not.** A function whose + results hold live pointers across a call must zero them and then execute + `GO_RESULTS_INITIALIZED`. Designing functions that return no pointers + avoids the problem. +2. **A frame with calls and no local pointers says so** with + `NO_LOCAL_POINTERS`. A frame with local pointers that the runtime cannot + see is not allowed at all: assembly cannot describe a pointer-containing + local, so it must not have one. Data symbols containing pointers are the + same: define them in Go. +3. **The stack may move.** Stack growth copies the frame, so no pointer into + the frame may be held across a call, and the raw hardware SP register may + not be cached across a call either. +4. **The split check is not optional by default.** Without NOSPLIT, the + assembler inserts the stack-growth preamble, including the morestack + block for framed functions; NOSPLIT is a contract that the frame and + everything below it fit in the remaining stack segment. On amd64 the + assembler also marks small leaf functions NoSplit itself and skips the + preamble, so silence is not a promise. + +The simplest safe shape is a leaf function with no local frame and no calls: +it needs no annotation beyond the prototype. + +## go_asm.h: Go constants and layout in assembly + +A package with `.s` files gets a generated header. Include it and use the +generated names instead of hard-coding layouts, which lie silently when the +Go side changes: + +| Go declaration | Assembly name | +|---|---| +| `const bufSize = 1024` | `const_bufSize` | +| field `r` of `type reader struct` | `reader_r` | +| size of `type reader struct` | `reader__size` | + +The constants arrive as macros, usable as immediates and offsets, computed +from the Go declarations. An ambiguous name, such as a struct that really +has a `_size` field, fails the generation with a redefinition error. + +## funcdata.h: the runtime macros + +`$GOROOT/pkg/include/funcdata.h` defines the PCDATA and FUNCDATA ids and the +three macros assembly normally uses instead: + +| Macro | Expands to | Meaning | +|---|---|---| +| `GO_ARGS` | `FUNCDATA $FUNCDATA_ArgsPointerMaps, go_args_stackmap(SB)` | the Go prototype defines the argument pointer map | +| `GO_RESULTS_INITIALIZED` | `PCDATA $PCDATA_StackMapIndex, $1` | results are initialised; treat them as live from here | +| `NO_LOCAL_POINTERS` | `FUNCDATA $FUNCDATA_LocalsPointerMaps, no_pointers_stackmap(SB)` | the frame holds no pointers | + +`GO_ARGS` is inserted implicitly by the assembler for any function whose +package-qualified name belongs to the current package, which is why most +assembly never writes it. `NOSPLIT` leaf functions that call nothing need +none of the three. + +The underlying ids, for reading toolchain output rather than for writing +source: FUNCDATA 0 to 7 are args pointer maps, locals pointer maps, stack +objects, inline tree, open-coded defer info, argument info, argument +liveness and wrap info; PCDATA 0 to 4 are unsafe point, stack map index, +inline tree index, argument liveness index and panic bounds. + +## What the runtime does with all of this + +The object file records the annotations as aux symbols and FuncInfo records; +GOOBJ.md specifies the encoding. The linker assembles them into the runtime's +pclntable, which traceback and the collector consume. An assembly function +that misdeclares its frame is not a compile error and usually not a link +error: it is a wrong collector decision or a wrong traceback at runtime, +which is why the annotations are a contract and not documentation.