Reading Assembly

Part 2 · The calling contract

Reading a call chain

Reconstructing what the source said from a listing with no symbols — and noticing when the compiler deleted the calls entirely.

35 minfaded worked exampleinterleavingPOE
What this lesson is trying to break (2)
  • Every function in the source appears in the output.
  • A call in the listing corresponds to a call in the source.

Part 2 gave you the contract. This lesson uses it to read something you have no source for — which is the actual situation in a profiler or a crash dump.

Reconstructing a call

The convention runs both ways. If you know arguments go in rdi, rsi, rdx, …, then a cluster of writes to those registers before a call is the argument list.

        mov     esi, 10
        mov     rdi, rbx
        call    process
        mov     r12, rax

Read it as r12 = process(rbx, 10). That is a complete recovery of a source line, from four instructions and one symbol name.

Do this repeatedly and you rebuild the call graph, which is usually what you actually want — far more than understanding every instruction.

Predict firstcommit in writing, then look
static int helper(int x) { return x * 2 + 1; }

int outer(int a, int b) {
    return helper(a) + helper(b);
}

Two calls in the source. How many call instructions at -O2?

Walking one chain

Step 1 — establish the frame and what survives

Read the prologue first, not the body.

Callee-saved pushes tell you how much state crosses calls (2.4). sub rsp, N tells you the frame size. On ARM64, whether x30 was saved tells you immediately whether this function calls anything.

From the prologue alone you can usually predict: this function makes calls, holds about three values across them, and has a small frame.

Step 2 — find every call and its arguments

Search for call. For each one, read the ~4 instructions above it as the argument list, and the instruction below it as what happened to the result.

Where does each argument come from? An argument register means it was passed through from this function’s own parameters. A callee-saved register means it was computed earlier and preserved. A stack slot means it was spilled or is a local whose address matters.

That sourcing information is often the interesting part — it tells you the data flow, not just the control flow.

Step 3 — check for tail calls, which hide in plain sight

A jmp to a function symbol, rather than a call, is a tail call: the call was the last thing the function did, so the compiler reused the frame and jumped.

Two consequences worth knowing:

  • No stack growth. Tail-recursive code compiles to a loop and cannot overflow.
  • The frame disappears from stack traces. A tail-called function appears to have been called by your caller, because your frame is gone. This makes perfectly correct traces look wrong, and it is a common source of confusion when reading profiles of functional-style or heavily-optimised code.

Interleaved practice

Predict firstcommit in writing, then look
        mov     edx, 3
        mov     rsi, r12
        mov     rdi, rbx
        call    lookup
        test    rax, rax
        je      .Lnotfound
Predict firstcommit in writing, then look
        add     rsp, 24
        pop     rbx
        jmp     format_error

Part 2 checkpoint

  1. In what sense is the stack a convention? (2.1)
  2. What does call actually do? (2.1)
  3. Which registers carry the first three integer arguments on SysV? (2.2)
  4. What happens if code violates the ABI? (2.2)
  5. Is the frame pointer required, and what does omitting it cost? (2.3)
  6. Why are argument registers caller-saved a problem, and how is it solved? (2.4)
  7. Why might a source function have no call in the binary? (2.5)

You can now read a function and a call chain. Part 3 is about the fact that what you are reading often bears no structural resemblance to the source — and that is the part of this course with the most direct daily use.

Next: 3.1 — Compiler Explorer as an instrument.

In the wildgo read the real thing now, while it is fresh
  • objdump on your own binaryBuild any small program on your M1 with `clang -O2` and run `objdump -d`. Find main, then follow one call. This is the same skill as the lesson but with the mess of a real linked binary — symbol stubs, PLT-equivalents, and library code.
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.