Between machine code and a programming language
Machine code is binary and unreadable. A high-level language is readable but hides the machine entirely. Assembly language sits between them: each instruction corresponds to exactly one machine instruction, but written as a short mnemonic such as LDD or ADD instead of a pattern of bits.
That one-to-one correspondence is the defining property. It means an assembler needs only to look up each mnemonic and substitute the opcode, which is why assembly is translated by a simple assembler rather than a compiler.
It is still used where precise control matters — device drivers, embedded controllers, and small routines where every cycle counts.
| Instruction | Meaning |
|---|---|
| LDD <address> | load the contents of that address into the ACC |
| LDM #n | load the immediate value n into the ACC |
| STO <address> | store the ACC into that address |
| ADD <address> | add the contents of that address to the ACC |
| SUB <address> | subtract the contents of that address from the ACC |
| INC <register> | add 1 to the register |
| CMP <address> | compare the ACC with the contents of that address |
| JMP <label> | jump unconditionally to the label |
| JPE <label> | jump to the label if the last compare was equal |
| JPN <label> | jump to the label if the last compare was not equal |
| END | end the program |
Immediate, direct, indirect and indexed
The addressing mode says how to interpret the operand. Immediate means the operand is the value. Direct means it is the address holding the value. Indirect means it is the address of an address. Indexed means add the contents of an index register to it — which is how arrays are walked through. Questions frequently give the same operand under different modes and ask for the resulting value.
Tracing a program
The standard exam task is a trace table: work through the instructions in order, recording the accumulator and any changed memory after each one. It is mechanical, and the marks are for accuracy rather than insight.
Two habits prevent most errors. Write one row per instruction executed, not per line of source — a loop body executed four times produces four sets of rows. And carry unchanged values down the table rather than leaving blanks, so the current state is always visible on the last row.
Trace this program, giving the final contents of the accumulator.LDM #5STO 100LDM #3ADD 100END
- LDM #5 — the immediate value 5 is loaded. ACC = 5.The # marks an immediate value, so 5 itself goes into the accumulator rather than the contents of address 5.
- STO 100 — the accumulator is stored at address 100. Memory[100] = 5, ACC still 5.STO copies rather than moves, so the accumulator is unchanged.
- LDM #3 — the immediate value 3 is loaded. ACC = 3.This overwrites the accumulator; 5 survives only because it was stored first.
- ADD 100 — the contents of address 100 are added. ACC = 3 + 5 = 8.ADD uses direct addressing, so it adds what is at address 100, not the number 100.
- END — the program stops with ACC = 8.The final state is what the question asks for.
ACC = 8
The same discipline applies to any trace: one row per executed instruction, and every variable carried forward so the current state is always readable off the bottom row.
The # changes everything
LDM #100 loads the number 100. LDD 100 loads whatever is stored at address 100 — which might be any value at all. Missing the # is the single most common trace error, and because the two look almost identical it usually goes unnoticed until the final answer is wrong.
Working on individual bits
Sometimes a whole byte is not the unit of interest — a single flag within it is. Bit manipulation provides the operations for reading and changing individual bits, using a mask: a pattern chosen so that the operation affects only the bits you want.
Three logical operations do the work, and each has one job. AND with a mask clears every bit where the mask is 0, so it is used to test or isolate. OR with a mask sets every bit where the mask is 1. XOR with a mask flips every bit where the mask is 1.
- mask
- the chosen bit patterndesigned so only the target bits are affected
- logical shift
- zeros shifted insuitable for unsigned values
- arithmetic shift
- sign bit preservedso negative numbers stay negative
Choosing the operation
- To check whether bit 3 is set: AND with
00001000and test for a non-zero result. - To turn bit 3 on: OR with
00001000. - To turn bit 3 off: AND with
11110111— the complement of the mask. - To toggle bit 3: XOR with
00001000. - A left shift multiplies by two, but bits shifted off the end are lost, which can overflow.
- An arithmetic right shift keeps the sign, so −8 shifted right becomes −4 rather than a large positive number.
Why anyone still writes it
Almost all software is written in high-level languages, so it is fair to ask why assembly survives at all. Three reasons keep it alive, and questions ask for them.
The first is direct hardware access. A device driver has to write specific values to specific hardware registers at specific addresses, and a high-level language deliberately hides exactly that. The second is predictable timing: in an embedded controller running a motor or an airbag, the number of clock cycles a routine takes may genuinely matter, and only assembly makes it exactly knowable. The third is size — a microcontroller with two kilobytes of memory cannot afford the overhead a compiler adds.
The costs are equally real. Assembly is specific to one processor family, so nothing ports. It is verbose, since one high-level statement may take a dozen instructions. And it is far harder to read, which makes maintenance expensive and errors more likely.
| Assembly | High-level language | |
|---|---|---|
| Hardware control | complete | abstracted away |
| Timing | exactly predictable | depends on the compiler |
| Portability | none — tied to one processor | recompile and run |
| Development speed | slow | fast |
| Readability | poor | good |
| Program size | very compact | larger |
The modern compromise
Most embedded projects are now written almost entirely in C, with assembly used only for the few routines that genuinely need it — an interrupt handler, or a timing-critical inner loop. That keeps the bulk of the code portable and readable while retaining exact control where it matters. A question asking whether a whole system should be written in assembly usually wants this answer rather than a straight yes or no.