the assembler
/ uh-SEM-bler /
After the compiler has done its thinking, it does not hand the machine raw numbers directly. It first writes out assembly language: a thin, human-readable shorthand for the processor's actual instructions, with lines like mov rax, 5 and add rax, rbx. The assembler is the small, fairly mechanical program that takes this assembly text and converts each line into the exact bytes of machine code the processor reads. It is a near one-to-one translation, more a transcription than a transformation.
Concretely, assembly is a list of mnemonics (short names like mov, add, call, ret) with operands (the registers, numbers, and addresses they act on). The assembler looks up each mnemonic, encodes it together with its operands into the numeric opcode the chip understands, and lays those bytes down in order. Where the assembly refers to a name whose final address is not yet known, such as a function defined elsewhere, the assembler leaves a labelled blank to be filled in later and records the name in a symbol table. Its output is an object file, not yet a runnable program.
Why a separate step rather than jumping straight to bytes? Mostly history and modularity: keeping assembly as a clean text format lets compilers, hand-written assembly, and tools all feed the same assembler, and it gives humans a readable layer to inspect. For everyday programming you rarely run the assembler by hand; the gcc or clang driver runs it for you between the compile and link stages. You can ask to see its input with a flag like gcc -S, which stops after producing the .s assembly file.
; assembly the compiler emitted machine bytes the assembler produces mov eax, 5 ; -> b8 05 00 00 00 ret ; -> c3
Each assembly mnemonic maps to a fixed sequence of opcode bytes the CPU executes.
The assembler produces an object file, not an executable; addresses of things defined in other files are still blank, to be patched in later by the linker.