13.5 Emitting Assembly
After instruction selection, register allocation, and frame construction, the backend has target-specific machine instructions. It can emit textual assembly for an external assembler or encode object-file bytes directly. Text is easier to inspect and reuse with mature assemblers; direct emission gives tighter control and faster integrated pipelines.
Assembly emission is not stringifying IR. The emitter must choose syntax, print legal operands, create unique labels, order blocks, emit sections and alignment, preserve symbol visibility, and attach unwind/debug directives.
From machine instructions to text
An assembly printer is target- and dialect-specific. x86 Intel syntax writes mov rax, [rbp-8], while AT&T syntax reverses operands and decorates registers and immediates. AArch64 and RISC-V have their own register names, relocation modifiers, and directive conventions.
The printer should consume already-validated machine operands rather than make allocation decisions. Pseudo-instructions must be expanded or intentionally delegated to the assembler. Labels need function-local uniqueness so two source blocks named loop do not collide.
.text
.globl sum
.type sum, @function
sum:
push rbp
mov rbp, rsp
; selected and allocated body
pop rbp
retDirectives describe code/data sections, symbol binding, alignment, constants, CFI, and metadata. They do not execute, but downstream tools depend on them.
Block layout and fallthrough
CFG successors need not appear in source order. Placing the likely successor immediately after a conditional branch creates a fallthrough and can remove or invert a jump. Cold error paths can move to a separate section, while loop headers may receive alignment for fetch performance.
Layout has tradeoffs. Alignment inserts padding bytes; duplicating tiny blocks may remove branches but grow code; hot/cold splitting improves instruction-cache locality but increases branch distance. Profile data can guide decisions, but correctness must hold for every path, not only hot ones.
Encoding instructions
A direct encoder maps opcode and operands into instruction fields. Fixed-width ISAs such as AArch64 and base RISC-V often use 32-bit instruction words with bit fields for opcode, registers, and immediate fragments. x86 instructions have variable length with optional prefixes, opcode bytes, ModR/M, SIB, displacement, and immediate fields.
Every immediate and displacement has a range and interpretation. If a branch target is too far for a short encoding, the backend may relax it to a longer sequence. Loading a large constant can require several instructions. These decisions can change code size, which changes label distances again, so assemblers may iterate layout and relaxation.
Symbols and relocation requests
The final address of an external function or global is not usually known while compiling one file. The emitter places a placeholder and records a relocation request describing the symbol, relocation kind, addend, and patch location. Position-independent code prefers PC-relative or table-based forms. Chapter 14 follows these requests into object files and the linker.
Local labels may be resolved within the assembler once layout is known. External or inter-section references normally remain relocations. The backend must choose a relocation kind compatible with the selected instruction sequence and code model.
Testing the emitter
Golden assembly tests are readable, but formatting changes can create noise. Pair them with structural machine-instruction tests and, where possible, assemble/disassemble round trips. Compare encoded bytes against a trusted assembler for boundary immediates, every addressing mode, register extremes, short and long branches, external symbols, PIC, CFI, and section switches.
Finally, execute generated snippets in a harness and compare their results with the IR interpreter. The backend is where abstract correctness meets exact bit fields; layered verification is far cheaper than diagnosing a wrong byte in a large executable.