# 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.