Reading Assembly Without Writing It
Assembly is the readable form of what the machine will actually execute. Most engineers will never write it, and being able to read it is the single most direct way to check what the compiler did rather than guessing.
Software view, hardware view
The gap between what you wrote and what the machine does is where this whole domain lives.
A small vocabulary covers most of what you will see
Assembly looks forbidding mostly because of unfamiliar notation, not because there is much to learn. A handful of operation categories account for the overwhelming majority of instructions in ordinary compiled code: move data, do arithmetic, compare, jump conditionally, call and return.
The category worth understanding first is data movement, because it dominates. A typical compiled function is mostly moving values between registers, the stack and memory, with the arithmetic that motivated the function occupying a small fraction of the instructions. That proportion is itself informative — it is the same observation as What Is Actually Inside a CPU made about the machine, appearing now in the code.
Control flow is the second category worth reading fluently: a compare that sets flags, followed by a conditional jump that reads them (The ALU: Where Arithmetic Actually Happens). Once you can see that pair, the shape of loops and conditionals in a disassembly becomes obvious, and the structure of the original code becomes recoverable.
| Category | Typical mnemonics | What it does |
|---|---|---|
| Data movement | MOV, LDR/STR, PUSH, POP | Copy between registers, stack and memory — usually the bulk of a function |
| Arithmetic | ADD, SUB, MUL, DIV | Compute; the part that corresponds to your expressions |
| Bitwise | AND, OR, XOR, SHL, SHR | Masking, flags, and cheap alternatives to arithmetic |
| Comparison | CMP, TEST | Set condition flags for a following branch |
| Control flow | JMP, JE/JNE, JL/JG | Redirect the program counter, conditionally or not |
| Calls | CALL, RET, BL | Enter and leave functions, saving the return address |
A real function, annotated
The fragment below is a straightforward loop — sum an array — as it typically compiles at moderate optimisation. It is worth reading line by line once, because almost every structural feature you will meet in real disassembly appears in it: a register zeroed as an accumulator, a bounds check, a load using a computed address, and a conditional jump closing the loop.
Notice specifically the addressing mode on the load. The array element is not fetched by a separate address calculation followed by a load; the address arithmetic is folded into the load instruction itself, which is exactly the hardware capability Addressing Modes: How an Index Becomes an Address describes. This is the single most common place a beginner over-counts instructions.
Notice also what is absent. There is no bounds check inside the loop in an optimised build of a language that does not require one, and no arithmetic beyond the addition and the index increment. If you expected vectorization and see a scalar loop like this, you have just learned something the source could not have told you (Auto-Vectorization: Verify, Do Not Assume).
xor %eax, %eax ; sum = 0 (xor-with-self is the idiomatic zero)
xor %ecx, %ecx ; i = 0
test %edx, %edx ; compare n with 0
jle .Ldone ; if n <= 0, skip the loop entirely
.Lloop:
add (%rdi,%rcx,4), %eax
; sum += a[i]
; %rdi = base address of a
; %rcx = i
; 4 = sizeof(int) -> base + i*4
; the address arithmetic is INSIDE the
; instruction: one instruction, not three
inc %rcx ; i++
cmp %rcx, %rdx ; compare i with n
jl .Lloop ; loop while i < n
.Ldone:
ret ; return value is already in %eax
; Four instructions in the loop body. No bounds check, no call.
; Scalar, one element per iteration -> this loop did NOT vectorize.What to actually look for
You are not reading assembly to understand the program — you wrote the program. You are reading it to answer one specific question, and knowing which question makes the task quick. The useful questions are few and each has a visual signature.
Did the loop vectorize? Look for vector registers and vector instructions instead of scalar ones (SIMD: One Instruction, Many Elements). Did anything spill? Look for stack loads and stores inside the loop body (Registers: The Fastest Storage, and There Is Almost None of It). Did a division survive? Look for a divide instruction (The ALU: Where Arithmetic Actually Happens). Was the function inlined? Look for the absence of a call. Each is a five-second check once you know the shape.
The practical workflow matters as much as the reading: compile at the optimisation level you actually ship, use a tool that annotates source alongside output, and look only at the function profiling already told you is hot. Reading disassembly of code that does not matter is an excellent way to spend an afternoon achieving nothing (Self Time, Total Time, and Where the CPU Went).
- Did it vectorize? Vector registers and vector instructions in the loop body.
- Did it spill? Stack loads and stores between iterations.
- Did the division survive? A divide instruction, versus a multiply-and-shift sequence.
- Was it inlined? No call instruction where you expected one.
- Is the loop body what you expected? Count the instructions — surprises here are usually the finding.
| Question | What to look for | Read next |
|---|---|---|
| Did the loop vectorize? | Vector registers and vector instructions in the body | Auto-Vectorization: Verify, Do Not Assume |
| Did values spill? | Stack loads and stores between iterations | Registers: The Fastest Storage, and There Is Almost None of It |
| Did a division survive? | A divide instruction rather than a multiply-and-shift | The ALU: Where Arithmetic Actually Happens |
| Was the function inlined? | No call instruction where you expected one | Your Code Is Data Too |
| Is the body what you expected? | Instruction count in the loop — surprises here are the finding | The Compiler Reordered It Before the CPU Did |
Key points
- Assembly is a textual rendering of the instructions the machine will decode — the last representation before bytes.
- A handful of categories — move, arithmetic, compare, jump, call — account for most of ordinary compiled code.
- Data movement usually dominates a compiled function; arithmetic is a small fraction of the instructions.
- Address arithmetic is often folded into the load instruction, so counting separate operations over-counts the work.
- Read it to answer one specific question about one hot function, not to understand a program you already wrote.
Follow the mechanism
The path through the machine, hop by hop — and the conclusions it invites that are wrong.
- 1Source → compiler: high-level constructs are lowered to target instructions at the chosen optimisation level.
- 2Compiler → assembly: the instruction stream is emitted in textual form, one line per machine instruction.
- 3Assembly → machine code: the assembler encodes each instruction into the bytes the processor decodes.
- 4Machine code → decoder: those bytes are read by the front end and translated into internal operations.
- 5Disassembler → you: bytes are rendered back into readable mnemonics, which is what you are inspecting.
- • "More instructions means slower." Instruction count is a weak proxy for time; a short loop stalling on memory beats a long one that never misses.
- • "I see a MOV, so a value moved in memory." Most moves are register to register and are essentially free.
- • "The debug build shows what runs." Optimised builds differ enormously; reading unoptimised output answers the wrong question.
Consequences, controls and cost
- • Compiler decisions that are invisible in source — vectorization, inlining, spilling, strength reduction — become directly checkable.
- • Assumptions about generated code are frequently wrong, and the check is faster than the argument about it.
- • Instruction counting from a disassembly overestimates work when address arithmetic is folded into memory operations.
- • Use a tool that shows source and generated code side by side; this removes most of the difficulty of orienting yourself.
- • Compile at the optimisation level you ship — debug-build assembly answers no question you actually have.
- • Look only at functions profiling has flagged as hot.
- • Learn the four visual signatures — vector instructions, stack traffic, divides, calls — and stop there; that covers most questions.
- • Disassemble the hot function at shipping optimisation and check for the specific signature you are asking about.
- • Diff generated code across compiler flags or versions to see exactly what a flag changed.
- • Confirm any generated-code hypothesis with a timing measurement — the assembly tells you what happened, not what it cost.
- • Reading disassembly is precise and slow, and only pays off on code that profiling has already flagged.
- • Findings are specific to one compiler, version, flag set and target; they do not generalise across builds.
Scope
§224 — what these claims are specific to.
- ISA-SPECIFICMnemonics, operand order and syntax conventions differ between x86-64, AArch64 and RISC-V, and x86-64 alone has two common syntaxes with reversed operand order.
- ABI-SPECIFICWhich register holds an argument or a return value is a platform convention, not an instruction property — the same disassembly means different things under different ABIs.
Misconceptions
Where the rest of this lives
What appears in a disassembly is the compiler's decision, not the machine's. Which optimisations ran, and why one was declined, belongs to the toolchain rather than to the processor.