Reading Assembly

Part 1 · The register machine

Reading your first function

Everything from Part 1 applied to one real function, in both instruction sets — then a mixed set that refuses to tell you which idea it is testing.

40 minfaded worked exampleinterleavingself-explanation
What this lesson is trying to break (2)
  • Understanding each instruction means understanding the function.
  • A shorter listing is a better listing.

Part 1 gave you registers, widths, addressing and flags. This lesson checks whether they work together, then deliberately mixes them up.

The function

Read it once the way you would in review.

int count_positive(const int *a, int n) {
    int c = 0;
    for (int i = 0; i < n; i++) {
        if (a[i] > 0) c++;
    }
    return c;
}

Compile it for x86-64 and arm64 and keep both visible.

Predict firstcommit in writing, then look
  1. Which registers hold a and n on entry?
  2. Will there be a branch inside the loop for the if, or will it be branchless?
  3. Will the loop counter i appear as a register that counts 0, 1, 2, …?

Line by line

Open one step at a time, and try to answer each before expanding.

Step 1 — find the skeleton before reading any instruction

Do not start at the top. Do this instead:

  1. Find the entry label — the function name.
  2. Find the backward jump. A conditional jump to a label that appears earlier is the loop. That single line locates the loop body for free.
  3. Find the exits — every ret, plus any jump to a label near the bottom.
  4. Note which register ends up in eax (x86) or w0 (ARM64) before the ret. That is your return value, and it tells you which register was the accumulator.

You now know the shape without having decoded a single instruction. That order — structure first, instructions last — is the whole reading method, and it is the same top-down approach that works on unfamiliar source code.

Step 2 — the guard before the loop

Almost every compiled loop begins with a check that it should run at all:

        test    esi, esi
        jle     .Lempty

test esi, esi then a “less or equal” jump: if n <= 0, skip everything and return zero. The source for (int i = 0; i < n; i++) implies this — with n = 0 the body never runs — and the compiler must emit it explicitly.

Note jle rather than jbe: signed, because n is int. Lesson 1.2 is already paying off.

Step 3 — the loop body, and where the counter went

Inside, expect roughly:

  • a load from the pointer — real brackets this time, with a size annotation
  • a compare against zero
  • a conditional accumulate into the counter register
  • a pointer increment by 4
  • a compare against an end address and a backward jump

The two things worth dwelling on: i does not exist, replaced by the pointer walk; and the loop-exit test compares addresses, not counts. The end address was computed before the loop — often with a lea, which you can now read at a glance.

Step 4 — the ARM64 version, and what actually differs

Compare the two listings side by side and sort the differences into two piles.

Incidental:

  • ldr w2, [x0], 4 — ARM64 has post-increment addressing, so the load and the pointer bump are one instruction. x86 needs two.
  • Three-operand arithmetic means fewer movs.
  • Register names, and cmp+b.le instead of cmp+jle.

Essential — identical in both:

  • Arguments arrive in registers, in a fixed order.
  • There is a guard before the loop.
  • The counter became a pointer walk.
  • The condition is branchless.
  • One load per element, and no way around it.

That second list is what you actually learned. The first list is trivia about one instruction set — and the only way to know which pile something belongs in is to have seen both.

Interleaved practice

Not in lesson order, not labelled by topic. Work out which idea applies first.

Predict firstcommit in writing, then look
        lea     eax, [rdi+rdi*4]
Predict firstcommit in writing, then look
        cmp     edi, esi
        jae     .L2
Predict firstcommit in writing, then look
        mov     rax, -1
        mov     al, 5
Predict firstcommit in writing, then look
        lea     rax, [rdi+rsi*8]
        mov     rdx, [rax+16]
        lea     rcx, [rdx+rdx*2]

Part 1 checkpoint

Out loud, no notes:

  1. How many general-purpose registers, and why that number? (1.1)
  2. What does writing al do to the rest of rax? (1.1)
  3. Where does signedness live in compiled code? (1.2)
  4. What does lea do? (1.3)
  5. What are the four addressing-mode components and the legal scales? (1.3)
  6. How does a comparison communicate its result? (1.4)
  7. What does cmov tell you the compiler decided? (1.4)

You now have fiction 1 — the register machine — solidly enough to read a function. Part 2 is about how functions compose, which is entirely convention and not hardware at all.

Next: 2.1 — The stack is a convention.

In the wildgo read the real thing now, while it is fresh
  • Your own machineRun `clang -O2 -S -o - f.c` on the function below on your M1 and compare it with the ARM64 output from this site. Differences come from the compiler (clang vs gcc) and the target CPU, and noticing which is which is the skill.
Scheduled for recall3 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.