ISAassemblydisassemblymovjmpreading codecompiler output

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.

Follow the mechanism

Software view, hardware view

The gap between what you wrote and what the machine does is where this whole domain lives.

The question
What do assembly instructions actually mean, and how do I read them well enough to check what my compiler produced?
What you wrote
I write high-level code. Assembly is a lower layer I do not need to see.
What the hardware does
Assembly is a one-to-one textual rendering of the machine instructions the processor will decode. It is the last representation before bytes, and the only place to see what the compiler actually decided.
Every claim in this domain — whether a loop vectorized, whether a division survived, whether values spilled — is checkable in thirty seconds by looking. Reading assembly converts guesses about the compiler into facts.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

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.

The operations that make up most compiled code
CategoryTypical mnemonicsWhat it does
Data movementMOV, LDR/STR, PUSH, POPCopy between registers, stack and memory — usually the bulk of a function
ArithmeticADD, SUB, MUL, DIVCompute; the part that corresponds to your expressions
BitwiseAND, OR, XOR, SHL, SHRMasking, flags, and cheap alternatives to arithmetic
ComparisonCMP, TESTSet condition flags for a following branch
Control flowJMP, JE/JNE, JL/JGRedirect the program counter, conditionally or not
CallsCALL, RET, BLEnter and leave functions, saving the return address

A real function, annotated

ISA-SPECIFICx86-64 with AT&T-style operand order, simplified for readability. AArch64 and RISC-V use different mnemonics, different operand order and explicit load/store forms; the structure of the loop is recognisable across all of them.

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).

sum = 0; for (i = 0; i < n; i++) sum += a[i]; — typical scalar lowering
        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.
One question, one visual signature
QuestionWhat to look forRead next
Did the loop vectorize?Vector registers and vector instructions in the bodyAuto-Vectorization: Verify, Do Not Assume
Did values spill?Stack loads and stores between iterationsRegisters: The Fastest Storage, and There Is Almost None of It
Did a division survive?A divide instruction rather than a multiply-and-shiftThe ALU: Where Arithmetic Actually Happens
Was the function inlined?No call instruction where you expected oneYour Code Is Data Too
Is the body what you expected?Instruction count in the loop — surprises here are the findingThe 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.

  1. 1
    Source → compiler: high-level constructs are lowered to target instructions at the chosen optimisation level.
  2. 2
    Compiler → assembly: the instruction stream is emitted in textual form, one line per machine instruction.
  3. 3
    Assembly → machine code: the assembler encodes each instruction into the bytes the processor decodes.
  4. 4
    Machine code → decoder: those bytes are read by the front end and translated into internal operations.
  5. 5
    Disassembler → you: bytes are rendered back into readable mnemonics, which is what you are inspecting.
What people conclude from this — wrongly
  • "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

What it causes
  • • 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.
What you can do
  • • 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.
How to see it
  • • 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.
What it costs
  • • 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.

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

Claim
“Reading assembly requires being able to write it.”
Reality
Reading is far easier and is the skill that pays. You are checking for a handful of specific signatures in a function you already understand, not authoring anything.
Claim
“Each assembly line is one thing the CPU does.”
Reality
One instruction may expand into several internal operations, and adjacent instructions may be fused into one. The mapping to machine work is not one to one.
Claim
“Fewer instructions means a faster function.”
Reality
A four-instruction loop that misses cache every iteration is far slower than a twelve-instruction loop that does not. Instruction count says nothing about stalls.

Where the rest of this lives

Programming Languages & Runtime Internals
Compiler output and optimisation levels

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.