docs(asm): open the assembly language reference

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-21 19:49:04 +02:00
parent 95025be1bc
commit 837231c068
8 changed files with 667 additions and 2 deletions
+9
View File
@@ -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
+4 -2
View File
@@ -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
+148
View File
@@ -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.
+140
View File
@@ -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.
+114
View File
@@ -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<ABIInternal>(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.
+79
View File
@@ -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.
+53
View File
@@ -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).
+120
View File
@@ -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.