Reading Assembly

Part 1 · The register machine

Registers are a scarce physical resource

Sixteen boxes, and the compiler spends most of its effort deciding who gets one. The first fiction, and the one you must accept before anything else makes sense.

35 minThreshold conceptrefutation textcontrasting casesPOE
What this lesson is trying to break (3)
  • A variable is a name for a value.
  • Registers are just fast variables.
  • Writing to a 32-bit register leaves the upper bits alone.
Liminality warning

This is a threshold concept: troublesome, transformative, and irreversible. You should expect to be genuinely stuck here, and being stuck is the mechanism rather than a sign that you are failing. Budget several sessions. Do not move on because you are bored — move on when you can explain it out loud without notes.

Unlearn thiscarried over from JavaScript, TypeScript, and Rust
You think

A variable is a name for a value. You can have as many as you like; naming one is free; the machine keeps them all somewhere and fetches them when needed. Rust made you think about where values live — stack or heap — but not about how many you were allowed.

Wrong here

At this level there are exactly 16 places to hold an integer on x86-64, and 31 on ARM64. Not sixteen thousand. Sixteen. Every value your function is working with competes for one, and when they run out the compiler starts writing values to memory and reading them back.

Instead

Read a register as a scarce physical resource, and read register allocation as the compiler’s central problem. Most of what looks like pointless shuffling in a listing — mov between registers, stores to stack slots — is the compiler solving that problem. Once you see it that way, the noise becomes signal.

The sixteen

Varies: only the width being addressed — every row names part of the same physical register

64-bit32-bit16-bit8-bitConventional use (SysV)
raxeaxaxalreturn value
rdiedididil1st argument
rsiesisisil2nd argument
rdxedxdxdl3rd argument
rcxecxcxcl4th argument
r8r15r8dr8wr8b5th, 6th args; then scratch
rspstack pointer — never general-purpose
rbpebpbpbplframe pointer, or general if omitted

`rax` and `eax` are not two registers. `eax` is the low half of `rax`. The four columns are four ways of naming parts of one 64-bit box, and which name appears in the listing tells you the width of the operation.

The names are historical wreckage. a was the accumulator, c the counter, d data, si/di source and destination index — from the 8086, in 1978. e meant extended to 32 bits in 1985, r meant 64 bits in 2003. r8r15 were added at the same time and got no letters, which is why they are the only sanely-named ones.

Why sixteen? Instruction encoding. A register number has to fit in the instruction bytes, and x86-64 extends the original 8 to 16 using an extra prefix byte. It is a compatibility artifact, not a design target. ARM64, designed in 2011 with no such baggage, has 31 — and you can see the difference in the naming: x0x30 for 64-bit, w0w30 for their 32-bit halves. That is the whole scheme.

Register pressure

Because registers are scarce, “how many values are live at once” becomes a real constraint with a name: register pressure. When live values outnumber registers, the compiler spills — stores a value to a stack slot and reloads it later.

Predict firstcommit in writing, then look
int f(int a, int b, int c, int d, int e, int g) {
    return a*b + c*d + e*g;
}

Six arguments, three multiplies. Does the compiler need to touch the stack?

The partial-register trap

This one produces genuine bugs and genuine slowdowns, and it is not guessable.

mov eax, 5      ; rax is now exactly 5 — upper 32 bits ZEROED
mov al, 5       ; rax's upper 56 bits are UNCHANGED

Writing a 32-bit register zeroes the upper half of the 64-bit register. Writing an 8-bit or 16-bit register does not — it leaves the rest alone.

Two consequences:

For reading code: mov eax, edi is the idiomatic way to zero-extend a 32-bit value into 64 bits. It looks like a plain copy and it is also a conversion. When you see a 32-bit mov where you expected 64, that is usually why.

For performance: writing al creates a dependency on the previous contents of rax, because the CPU must merge your 8 bits with the existing 56. That is a false dependency — you did not want the old value, but the hardware has to wait for it. This is the flip side of the xor eax, eax beacon from 0.2: one idiom exists to break dependencies, and this trap accidentally creates them.

Beaconsee this → think this, without decoding it

mov eax, edi (32-bit mov where 64 was expected)

A zero-extending copy. The value is being widened from 32 to 64 bits, for free, as a side effect of the width rule above.

You will see this constantly in code that indexes arrays with int — the index is 32-bit, the address arithmetic is 64-bit, so a widening step is required and this is the cheapest form of it.

What ARM64 does differently

Compile anything for both targets and the register differences are the first thing you notice.

  • 31 registers, uniformly named. x0x30, with w0w30 as the 32-bit halves. No accumulator, no counter, no history.
  • Writing w0 zeroes the top half of x0, exactly like eax/rax. ARM64 has no 8- or 16-bit register names at all, so the partial-register trap simply does not exist.
  • A dedicated zero register. xzr/wzr reads as zero and discards writes. So ARM64 does not need the xor idiom — mov w0, wzr is unambiguous and has no dependency by construction. Compare that with x86 needing a recognised pattern to achieve the same thing, and you can see a genuine design improvement rather than a cosmetic difference.
  • Three-operand arithmetic. add w0, w1, w2 means w0 = w1 + w2, leaving both inputs intact. x86’s add eax, edi means eax += edi, destroying eax. This is why x86 listings contain so many more mov instructions: two-operand arithmetic forces you to copy anything you still need.

That last point explains most of the length difference between the two listings you saw in 0.1, and it is a good example of the distinction this course keeps making: more instructions was incidental, caused by operand count. The work was identical.

Explain it to yourselfwrite it — thinking it does not count

Explain to a colleague why the following two lines are not the same thing, and why the difference matters for performance and not just for correctness:

mov eax, 0
xor eax, eax

Two or three sentences. You should use the words dependency and renaming.

What you should now be able to say

  • There are 16 general-purpose registers on x86-64, 31 on ARM64, and the limit is encoding, not need.
  • rax/eax/ax/al are one register, four widths. The width in the listing tells you the width of the operation.
  • Register pressure and spilling are the compiler’s central problem, and most apparent shuffling in a listing is it solving that problem.
  • Writing a 32-bit register zeroes the upper half; writing 8/16-bit does not, which creates false dependencies.
  • ARM64’s three-operand arithmetic and zero register eliminate whole classes of x86 idiom — a real design difference, not cosmetics.

Next: 1.2 — There are no types, only widths.

In the wildgo read the real thing now, while it is fresh
  • x86-64 register referenceKeep this open for a week. You do not memorise the table — you look it up until the names become automatic, which takes about three lessons.
  • ARM64 register conventionsThe ARM64 equivalent. Note how much simpler the naming is: x0-x30 and their w0-w30 halves, no historical accidents.
Scheduled for recall4 items added to your review queue

These are not shown to you now on purpose. They will surface in Review tomorrow, mixed in among items from other parts of the course — not grouped by topic. Sorting out which idea applies is the part that transfers.