Back to Home

Go Assembler: how code turns into machine instructions | Basics

The article reveals the mechanism of translating Go code into machine instructions through the lens of Go Assembler. Pseudo-registers, load/store operations, arithmetic instructions, and control flow are covered with practical examples.

Diving into Go Assembler: from code to machine instructions
Advertisement 728x90

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

Google AdInline article slot
  • 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 symbols
  • x-8(SP) — local stack variables
  • arg+0(FP) — function arguments

A function declaration in Go Assembler looks like this:

Google AdInline article slot
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:

Google AdInline article slot
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
  • CALL saves return address
  • RET restores 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

Advertisement 728x90

Read Next