141 lines
6.3 KiB
Markdown
141 lines
6.3 KiB
Markdown
# 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.
|