This is a set of notes on the Go assembler for the ARM (GOARCH=arm, Aarch32) architecture. The documentation of the assembler is the Quick Guide. It is quite incomplete, hence these notes.
Instructions are generally written with their arguments reversed compared to
the ARM/GNU convention. If the final two arguments are the same, only one needs
to be provided: ADD R0, R1
and ADD R0, R1, R1
are the same.
To inspect the assembler that the compiler generates, the easiest option is
go build -gcflags=-S
, but that actually shows pseudo-assembler. The more
accurate option is go tool objdump
on a binary or object archive. That has
some quirks, though, mainly in using ARM syntax in some cases:
MULA
is spelled MLA
, MOVM
is spelled LDM
or STM
(with Rn!
syntax for post-increment) and shift constants have a leading $
.
The ARM has 16 general-purpose registers, called R0-R15 in Go. Ten of these, R0-R7, R9 and R12, are available for Go assembly routines. Four others are reserved. Two are quasi-reserved for pseudo-operations; of these, the Quick Guide fails to mention R8.
Go name | Reserved for |
---|---|
R8 | Division pseudo-ops |
R10 | g (thread-local storage) |
R11 | Pseudo-ops |
R13 | Stack pointer (RSP) |
R14 | Link register (return address) |
R15 | Program counter |
Loads, stores and moves are all written MOV.
ARM | Go | Remarks |
---|---|---|
MOV | MOVW | |
LDR | MOVW | |
STR | MOVW | |
LDRB | MOVBU | |
LDRSB | MOVB | |
STRB | MOVB | Can also be spelled MOVBU. |
LDRH | MOVHU | |
LDRSH | MOVH | |
STRH | MOVH | Can also be spelled MOVHU. |
LDM | MOVM | |
STM | MOVM | |
MOV32 | MOVW | Pseudo-operation. |
There doesn't seem to be a Go name for LDRD/STRD.
MOVM can take .IA, .IB, .DA and .DB modifiers.
Write-back is enabled by .P (post-increment) and .W (pre-increment) modifiers:
MOVW.P 8(R0), R1
is the same as
MOVW (R0), R1
ADD $8, R0
Some operations aren't really ARM instructions, but pseudo-instructions that compile to instruction sequences.
The Go assembler allows pseudo-immediates that are larger than the immediates that fit in an ARM instruction:
MOVW $0x55554444, R0
becomes something like
MOVW 0x8(R15), R0
// rest of the function
MOVBU.PL -0x444(R5), R4 // 0x55554444
An additional instruction has been generated after the return instruction that encodes to the value of pseudo-immediate. A PC-relative MOVW loads it into R0. This is similar to how the ARM mov32 instruction works with literal pools.
When a large immediate is used with other instructions, the assembler may generate a load through R11:
ADD $0x11112222, R0
becomes
MOVW 0xc(R15), R11
ADD R11, R0, R0
// rest of the function
TST.NE R2>>$4, R1 // 0x11112222
The DIV, MOD, DIVU and MODU pseudo-instructions perform division in a way that works even on older ARMs that don't have division hardware:
DIV R0, R1, R2 // R2 = R1/R0
generates a call to runtime._div. That function saves and restores the registers it uses, except that it wants one of its arguments in R8 and stores its return value in R11 (the Quick Guide doesn't mention R8 in this regard). The generated code looks as follows:
MOVW 0x18(R10), R11 // R10 = g.
MOVW R0, 0x20(R11) // Push denominator.
MOVW R1, R8 // Pass numerator as R8.
BL runtime._div(SB)
MOVW R11, R2 // Move return value to final location.
Condition codes don't work on DIV/MOD/DIVU/MODU. The condition code is moved to the first instruction (the MOVW), so the call gets made with the wrong argument.
A function that calls no other functions is a leaf function. As the Quick Guide explains, the special value $-4 for the frame size declares a leaf function. Go >=1.11 has the NOFRAME macro for the TEXT directive argument, which has the same effect.