The Assembler
Assembly language is still human-readable text — an assembler is what turns that text into the actual binary machine code a CPU executes, along with the bookkeeping (an object file) later stages need to finish the job.
An assembler translates assembly language (a human-readable, mnemonic-based representation of machine instructions, like mov or add) into the actual binary machine code those mnemonics represent, packaging the result into an object file.
Machine code is just raw binary — mnemonics like mov and add exist purely for human readability. An assembler is the direct, mostly one-to-one translator between that human-readable form and the actual bit patterns a CPU's hardware decodes and executes.
Every compiler that generates native code eventually invokes (or acts as) an assembler as one of its final steps — you can also write assembly directly by hand (common in embedded systems' startup code, or in extremely performance-critical inner loops) and assemble it with the same tool a compiler's back end would use.
Think of Morse code translation. A trained human reads and writes dots and dashes using memorable letter-by-letter patterns (mnemonics). A machine converts those exact patterns into the raw electrical pulses actually sent over the wire (binary). The mnemonics and the pulses represent exactly the same message — the assembler is simply the mechanical, essentially one-to-one translator between the human-friendly notation and the machine-native binary form.
The mnemonic mov eax, 5 (move the value 5 into register eax) corresponds to a specific, fixed binary encoding on this architecture — B8 is the opcode meaning "move an immediate 32-bit value into eax," followed by the four bytes encoding the value 5 itself. Real encodings for other instructions and architectures vary, but the direct mnemonic-to-bit-pattern relationship is the same idea throughout.
An opcode is the specific bit pattern identifying which operation an instruction performs; an operand specifies what the operation acts on (a register, a memory address, or an immediate/literal value) — together they make up a complete machine instruction's encoding.
Labels let assembly reference a location in the code (a jump target, the start of a piece of data) by a symbolic name rather than a raw numeric address, since the final address often isn't known until later — the assembler (and, for cross-file references, the linker afterward) resolves labels to actual addresses.
Assembler directives (lines starting with a period in many syntaxes, like .data or .text) aren't themselves machine instructions — they're instructions to the assembler itself, controlling things like which section of the output file subsequent content belongs in, or reserving a block of uninitialized memory.
The assembler's output is an object file — not yet a runnable program, but a package containing the generated machine code, a table of symbols this file defines and needs from elsewhere, and enough information for the next stage, the linker, to combine it with other object files and libraries into one complete executable.
Assuming assembly is architecture-independent — assembly syntax, available instructions, and register names are all specific to one target CPU architecture (and often even to one specific assembler's particular syntax dialect for that architecture, like AT&T vs. Intel syntax for x86).
Confusing an assembler's job with a compiler's — an assembler performs an almost entirely mechanical, roughly one-to-one text-to-binary translation of already-generated assembly; it does none of the higher-level analysis (parsing a rich grammar, type checking, optimization) a compiler's earlier stages perform.
Forgetting that an object file (the assembler's output) is not directly executable on its own — it still needs the linker to resolve any references to symbols defined in other files or libraries, and to combine everything into one final executable.
Reading a compiler's generated assembly output (via a -S flag) as a normal part of understanding what code generation actually produced, before that assembly is itself turned into an object file by the assembler.
Writing small pieces of hand-tuned assembly directly for extremely performance-critical inner loops, or for embedded startup code that must run before a C runtime environment is even set up — both genuine, real-world uses of assembly language beyond compiler output.
Using a disassembler (like objdump) to go the opposite direction — turning an existing object file or executable's raw machine code back into readable assembly mnemonics, useful for debugging or reverse-engineering when source isn't available.
Instruction encoding complexity varies enormously by architecture
Some architectures (notably RISC-style ones like RISC-V and, to a lesser degree, ARM) use fixed-width, relatively uniform instruction encodings that are comparatively simple for an assembler to produce. x86-64, a CISC-style architecture with a long legacy of backward compatibility, has famously variable-length, intricate instruction encodings with many optional prefixes and addressing-mode variations — making a correct, complete x86-64 assembler (and disassembler) meaningfully more complex to implement than an equivalent tool for a cleaner RISC-style ISA.
When an assembler encounters a reference to something whose final address isn't known yet (a call to a function defined in a different, not-yet-linked object file, for example), it can't emit a final absolute address — instead it emits a placeholder along with a relocation entry, a piece of metadata telling the linker exactly how to patch that location once the real final address is known. Relocation is one of the central concepts the following Linker section builds on directly.
Assemblers as their own small language processors A modern assembler often supports macros, conditional assembly, and other features that go well beyond pure mechanical mnemonic-to-binary translation — meaning a full-featured assembler is, in its own right, a small compiler-like tool with its own (much simpler) lexing and parsing concerns, even though its final translation step remains close to one-to-one with the target machine's actual instruction encoding.
What is the difference between a compiler and an assembler?
A compiler performs deep analysis of a rich, high-level language (parsing, type checking, optimization) before producing lower-level output. An assembler performs a comparatively mechanical, largely one-to-one translation of already-generated assembly mnemonics into their exact binary machine-code equivalents.
What is an opcode?
The specific bit pattern within a machine instruction's encoding that identifies which operation the instruction performs (such as "add" or "move") — distinct from the operands, which specify what the operation acts on.
Why can't an assembler always emit a final, absolute address for a label?
Because the label might refer to something in another object file or a library that hasn't been combined into the final executable yet, so its ultimate address isn't known at assembly time. The assembler emits a placeholder plus a relocation entry, which the linker later uses to patch in the correct final address.
Is an object file directly runnable?
No — it contains machine code and symbol/relocation information, but typically still has unresolved references to other object files or libraries. The linker must combine it with everything else it depends on before producing a complete, runnable executable.
Assembly: mov eax, 5
Machine code: B8 05 00 00 00 (hexadecimal bytes)