[4.2.2] Assembly & assembler
[4.2.2] Assembly language uses mnemonics and why an assembler is required
Assembly language is a low-level language that represents machine instructions using short, human-friendly words called mnemonics (e.g. MOV, ADD, SUB) instead of raw binary. Each assembly instruction usually corresponds closely to one machine instruction defined by the processor's instruction set architecture (ISA). Although assembly is more readable than pure machine code, computers still execute machine code only, so a special translator called an assembler is needed to convert mnemonics and symbols into the exact bit patterns the CPU can run.
This page explains what mnemonics are, how assembly relates to machine code, the need for an assembler, and the typical features assemblers provide. You will also see where and why developers still choose assembly language today, alongside a clear comparison with high-level languages in terms of readability, portability, and control of hardware.
From machine code to mnemonics
Machine code encodes operations as binary patterns, such as instructions like 10110000 or addresses like 00010110. Reading and writing long sequences of bits is difficult and error prone. Mnemonics give each instruction a short name and allow the use of operands such as registers, immediate values, and memory addresses. Assemblers may also allow labels to name addresses and directives (sometimes called pseudo-operations) to control data layout or the assembly process.
Task: Load the number 5 into a register and add it to a value stored in memory, then save the result.
This is the human description of the operation: clear and high level, but not precise about registers, opcodes, or address layouts.
Assembly: A typical sequence might look like: LOAD R1, #5; ADD R1, [VALUE]; STORE R1, [VALUE]. The exact mnemonics vary by ISA, but the idea is consistent: concise, symbolic instructions that are close to the hardware.
Here R1 is a register, #5 indicates an immediate constant, and [VALUE] refers to a memory location labelled VALUE in the programme's data section.
Machine code: The assembler outputs binary encodings such as 00100101 (opcode) followed by operands like a register code 0001 and an immediate value 00000101. For a memory address, the emitted bytes could include an address like 00000000 00010010. Exact patterns depend on the CPU's instruction format.
Humans rarely write these bit patterns by hand; they rely on the assembler to generate them correctly.
How assemblers work and why they are needed
An assembler translates assembly source into machine code the processor can execute. It also provides essential services that go beyond simple replacement of mnemonics with opcodes:
- Symbol resolution: Builds a symbol table mapping labels (e.g. START, LOOP) to numeric addresses. This lets you write JMP LOOP rather than a hard-coded address like
00010010. - Address calculation: Works out instruction sizes and data layout so references point to the correct locations, even after edits.
- Directive handling: Processes pseudo-operations to reserve storage, define constants, include files, or set alignment.
- Error checking: Reports syntax errors (e.g. invalid mnemonic), undefined symbols (labels never declared), and range errors (branch too far to encode).
- Output formats: Produces object code or executable files in standard formats so linkers and loaders can place them in memory correctly.
One-pass: The assembler reads the source once, translating as it goes. Fast, but forward references (jumping to labels not yet seen) can be hard; it may need temporary assumptions or limitations.
Two-pass: First pass builds the symbol table and calculates addresses; second pass emits machine code with correct targets. More reliable for forward references and widely used for clarity.
Macro assemblers: Allow the definition of macros that expand into sequences of instructions, improving reuse and readability. The assembler expands macros before or during translation, reducing repetition while keeping low-level control.
Operands, addressing modes, and labels
Assembly instructions usually include operands that specify where data comes from and where it goes. The ISA defines several addressing modes describing how to interpret those operands.
Immediate: The value is encoded inside the instruction itself, e.g. adding 00000101 (5) to a register. Fast and simple, but limited to the size the instruction can hold.
Direct/absolute: The instruction contains the memory address of the operand, such as 00000000 00101100. Flexible but may produce larger instructions and is tied to specific memory locations.
Register indirect/indexed: A register holds a base address and the instruction may add an offset to reach an element in a list or array. Efficient for moving through structures in memory.
Branching, labels, and status flags
Branch instructions alter the programme's flow by jumping to labels. Conditional branches test status flags set by previous operations (e.g. zero, negative, carry, overflow). Labels make code clearer and relocatable: the assembler calculates the numeric distance or absolute target so you do not need to count bytes manually.
Hard-coded addresses: Writing a jump to 00010010 is fragile: if code above changes length, that address becomes wrong. It also harms readability.
Labels: JMP LOOP is clearer and safer. The assembler resolves LOOP to the correct numeric address during translation, even after edits.
Range and overflow: Some branch encodings allow only a small offset. If LOOP is too far away, the assembler reports a range error or expands into a longer sequence where supported.
Why use assembly language?
Despite the strength of high-level languages, assembly is still used when precise hardware control, predictable timing, or minimal overhead are essential.
| Use case | Why assembly helps | Notes |
|---|---|---|
| Device drivers and firmware | Direct register access and exact ordering of operations | Often combined with C; careful testing required |
| Embedded systems with small memory | Tight code size and deterministic execution | Assembly can reduce bytes compared with high-level code |
| Performance-critical inner loops | Hand-optimised instruction sequences | Used sparingly within larger high-level programmes |
| Learning about CPU operation | Reveals how instructions, flags, and addressing modes work | Builds mental models for understanding low-level behaviour |
Assembler diagnostics and common issues
Assemblers provide feedback that helps catch mistakes early. Recognising these messages is part of effective low-level development.
Syntax errors: Misspelt mnemonics or wrong operand formats (e.g. writing MOOV or giving too many operands). The assembler flags the exact line and expected format.
Undefined symbols: Jumping to LOOP when no LOOP label exists. Two-pass assemblers catch these reliably.
Range/relocation: A short branch cannot reach a distant label, or absolute addresses conflict when linking modules. The solution may be to use a long jump or reorganise sections.
Comparing assembly with high-level languages
Assembly offers clarity about what the processor does cycle by cycle, but it is less portable and usually slower to develop. High-level languages prioritise readability and speed of development, relying on compilers or interpreters to generate appropriate machine code.
- Readability: Assembly is readable to trained developers but still dense; high-level code is more widely accessible.
- Portability: Assembly is specific to one ISA; high-level languages can be recompiled or interpreted on many platforms.
- Control: Assembly provides exact control of instructions, registers, and timing; high-level code trades some control for productivity and safety.
Deep Dive: Symbol tables and the two-pass process
In the first pass, the assembler scans every line and notes the addresses of labels in a symbol table. Each instruction and data directive has a size, so the assembler can increment a location counter accurately even before outputting any machine code. In the second pass, it revisits the source and replaces label references with the final numeric addresses, emitting the correct opcodes and operands. This two-step approach makes forward jumps straightforward and supports flexible editing without breaking addresses.
Deep Dive: Directives vs instructions
Instructions become machine code that the CPU executes. Directives guide the assembler itself. Examples include reserving bytes for variables, initialising data, or specifying alignment. These do not generate executable opcodes but determine how the programme is laid out in memory and how labels resolve. Understanding the difference avoids confusion when reading or writing assembly source.
Key Takeaways
- Assembly language uses mnemonics and symbols to represent machine instructions in a readable form.
- An assembler is required to translate assembly into machine code, resolve labels, handle directives, and report errors.
- Addressing modes (immediate, direct, register indirect) describe how operands are located and used by instructions.
- Labels improve clarity and relocation, while status flags enable conditional branching.
- Assembly is chosen for precise hardware control, small code size, and predictable timing, but it is less portable and slower to develop than high-level languages.