A Quick Guide to Go's Assembler
This document is a concise overview of the unique assembly language employed by the gc Go compiler. It is not exhaustive and assumes familiarity with Plan 9 assemblers for detailed descriptions. To learn assembly language, readers should consult the separate Plan 9 manual, though many concepts apply broadly. The current guide focuses on syntax, differences from Plan 9's documentation, and specific considerations when writing Go-compatible assembly code.
One key distinction is that Go's assembler does not directly represent the underlying machine; some mappings are precise, while others are not. This is due to the compiler's operating on a semi-abstract instruction set rather than the machine code itself. The assembler processes this intermediate representation, translating it into actual instructions for the linker.
Some instructions map directly to hardware operations, while others are represented more abstractly, like memory moves or subroutine calls. The specifics vary by architecture. The assembly program functions as a parser for this semi-abstract instruction set, generating input for the linker. Examples of assembly instructions for various architectures, such as amd64, can be found in the standard library sources, particularly in packages like runtime and math/big.
The compiler also provides assembly code that can be inspected using go tool objdump, though the output may differ from what is presented here. Plan 9 assemblers influenced Go's assembler, but it remains a separate program with some variations. One notable difference is in constant evaluation; Go's assembler parses constant expressions using its own precedence rules, not C's.
For instance, 3&1 2 evaluates to 4, not 0, due to the precedence difference. Moreover, all constants are treated as 64-bit unsigned integers. This distinction is minor but avoids ambiguity, disallowing division or right shift operations with a high bit set in the right operand. The assembler recognizes several predefined symbols, such as R1 or LR, which correspond to registers.
The exact set of symbols depends on the architecture. Four pseudo-registers are predeclared and refer to virtual registers managed by the toolchain, including a frame pointer. These pseudo-registers are consistent across all architectures. User-defined symbols are expressed as offsets relative to the FP (frame pointer) and SB (symbol base) pseudo-registers.
The SB pseudo-register represents the base address of memory, enabling naming of global functions and data. By appending a number to the name, such as foo(SB), it specifies an address in memory. This approach is used for global functions and data. To mark a name as local to the current source file, add a double underscore, as in foo__(SB).
This creates a top-level static declaration akin to a C file's static keyword. Offsets can be added to names, like foo+4(SB), indicating a location four bytes past foo's start. FP, the virtual frame pointer, is used to reference function arguments. Arguments in memory are accessed via offsets from FP, like 0(FP) for the first argument and 8(FP) for the second on a 64-bit system.
When referring to arguments, FP must be prefixed with a name to avoid ambiguity, even though the name is arbitrary. This convention is enforced by the assembler, rejecting plain 0(FP) and 8(FP). The naming of arguments is purely for documentation purposes. It's important to note that FP is always a pseudo-register, not a hardware register, even on architectures featuring a hardware frame pointer.
For 32-bit systems, the low and high 32 bits of a 64-bit value are differentiated by adding _lo or _hi suffixes to the name, e.g., arg_lo+0(FP) and arg_hi+4(FP). When a Go prototype lacks a result name, the assembly name defaults to ret. The SP pseudo-register denotes the virtual stack pointer, pointing to the highest address within the local stack frame.
Negative offsets are used to reference frame-local variables and arguments being prepared for function calls, e.g., x-8(SP) and y-4(SP). On architectures with a dedicated hardware register for SP, the prefix differentiates between the virtual and hardware registers. On such machines, SP and PC may refer to different physical registers; the Go assembler treats them specially, requiring symbols for SP-like accesses.
To access the actual hardware register, use the R prefix. For example, on ARM architectures, the assembler uses R names for hardware registers.
Written by urgent.news from Lobsters's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.