Files
gasm-sdk/docs/asm/RUNTIME.md
2026-09-21 19:49:04 +02:00

5.7 KiB

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.

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

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.