Files
2026-09-21 19:49:04 +02:00

6.3 KiB
Raw Permalink Blame History

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:

// 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:

{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:

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

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.