# How Go Code Turns into Machine Instructions: Basics of Go Assembler
The Go compiler translates high-level code into machine instructions executed by the processor. In this article, we'll break down how the Go Assembler works, the instructions it generates, and how they relate to the source code. We'll focus on practical analysis of architecture-independent abstractions and their ties to actual hardware.
Pseudo-Registers and Architectural Independence
Go Assembler uses its own syntax, fundamentally different from standard GAS/Intel. The key feature is pseudo-registers, which hide target architecture details:
- SB (Symbol Base): Global variables and static symbols
- SP (Stack Pointer): Top of the current stack frame
- FP (Frame Pointer): Pointer to function arguments
- PC (Program Counter): Address of the next instruction
These abstractions provide:
- Code portability across x86, ARM, RISC-V
- ABI stability across Go versions
- Integration with the garbage collector via stack maps
- Simplified linker work
Example translation:
MOVQ AX, main.y(SB) ; GoASM
mov [rip+offset], rax ; x86 ASM
Go Assembler addressing uses specific constructs:
name(SB)— global symbolsx-8(SP)— local stack variablesarg+0(FP)— function arguments
A function declaration in Go Assembler looks like this:
TEXT main.main(SB), ABIInternal, $0-8
Where ABIInternal denotes the internal interface used by runtime, and $0-8 is the stack frame size.
Mechanics of Load/Store Operations
The processor works only with registers and memory. All variable operations reduce to:
- Loading data from memory into a register (load)
- Computations in registers
- Storing the result to memory (store)
Consider a simple example:
x := 1
y = x
Without optimizations, the compiler generates:
MOVQ $1, main.x-8(SP) ; Write constant to stack
MOVQ main.x-8(SP), AX ; Load x into register
MOVQ AX, main.y(SB) ; Store to y
The MOVQ instruction (Move Quadword) handles 8-byte values. Suffixes indicate size:
- B (Byte) — 1 byte
- W (Word) — 2 bytes
- L (Long) — 4 bytes
- Q (Quadword) — 8 bytes
For struct copying, the compiler breaks it down:
type Point struct { x, y int }
p1 := Point{1, 2}
p2 := p1
Corresponding assembly:
MOVQ main.p1(SP), AX ; Copy x
MOVQ AX, main.p2(SP)
MOVQ main.p1+8(SP), AX ; Copy y
MOVQ AX, main.p2+8(SP)
Pointer operations use LEAQ (Load Effective Address):
x := 10
p := &x
*p = 20
LEAQ main.x(SP), AX ; Get address of x
MOVQ AX, main.p(SP) ; Store in pointer
MOVQ $20, (AX) ; Dereference and write
Arithmetic and Logical Operations
Arithmetic instructions match the processor ISA but are adapted to Go semantics. Let's examine basic operations:
Addition/subtraction:
ADDQ BX, AX ; AX = AX + BX
SUBQ BX, AX ; AX = AX - BX
Multiplication/division:
IMULQ BX, AX ; AX = AX * BX
CQO ; Sign-extend AX into DX:AX for division
IDIVQ BX ; AX = (DX:AX)/BX, DX = remainder
Bitwise operations:
ANDQ BX, AX ; Logical AND
ORQ BX, AX ; Logical OR
XORQ BX, AX ; Exclusive OR
SHLQ CL, BX ; Shift left
SHRQ CL, BX ; Shift right
Comparison:
CMPQ BX, AX ; Sets flags
JLT less ; Jump if AX < BX
TESTQ is special—it performs bitwise AND of a value with itself. For example, to check if x == 0:
TESTQ AX, AX
JEQ zero ; Jump if result is 0
Division is the most expensive operation. The compiler often replaces it with bit shifts when possible.
Control Flow Management
The processor executes instructions sequentially by default. To alter the flow:
- Conditional jumps:
JEQ,JLT,JGT, etc. - Unconditional jumps:
JMP - Function calls:
CALL/RET
Branching example:
if x < y {
// ...
}
CMPQ BX, AX ; Compare x and y
JGE skip ; Jump if x >= y
... ; if block code
skip:
In Go Assembler, control flow ties closely to ABI. For function calls:
- Arguments go to stack or registers
CALLsaves return addressRETrestores context on completion
It's crucial to note that the compiler can reorder instructions within language guarantees. For instance, read/write operations may reorder if it doesn't affect observable behavior.
Key Points
- Pseudo-registers—key to Go's architectural independence. SB, SP, FP abstract hardware details.
- Load/Store model—all memory ops go through registers. No direct memory-to-memory operations.
- Operand sizes—B/W/L/Q suffixes set byte width. Wrong choice leads to bugs.
- Division—slowest arithmetic op. Avoid in hot paths; use bit shifts instead.
- Processor flags—comparison results set flags, not registers, for conditional jumps.
To analyze generated code, use:
go tool compile -S main.go
Or for debug version:
go build -gcflags="-N -l" -o app main.go
go tool objdump -s "main.main" ./app
Remember: Go Assembler isn't the end goal—it's a tool to understand code-hardware interaction. This model helps write efficient code by consciously managing memory and CPU resources.
— Editorial Team
No comments yet.