Reading Assembly

Part 2 · The calling contract

The ABI is a contract nothing enforces

Identical hardware, different rules. Which register holds your first argument is a political decision, not a physical one — and violating it fails silently.

40 minThreshold conceptcontrasting casesPOErefutation text
What this lesson is trying to break (3)
  • Argument registers are determined by the hardware.
  • The same machine code works anywhere the CPU is the same.
  • Struct arguments are passed on the stack.
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.

Everything in Part 1 was about the machine. This lesson is about an agreement, and the difference matters: an agreement can be violated, and nothing will stop you.

Unlearn thiscarried over from the reasonable assumption that hardware decides these things
You think

The CPU knows about function calls. There must be designated argument registers, because otherwise how would a function know where to look?

Wrong here

The CPU has no concept of a function argument. It executes call, which pushes an address and jumps. It has no idea anything was passed. rdi is not special in any way the silicon knows about.

Instead

The caller and the callee agree in advance, and the agreement is written down in a document called an ABI. Both sides were compiled against the same document, so it works. Compile them against different documents and you get silent corruption — no error, no crash at the boundary, just wrong values.

Three agreements for one machine

Varies: only the ABI — the CPU is identical in the first two columns

SysV AMD64
Linux, macOS
Windows x64AAPCS64
ARM64
Integer argsrdi rsi rdx rcx r8 r9
(6)
rcx rdx r8 r9
(4)
x0x7
(8)
Float argsxmm0xmm7 (8)xmm0xmm3 (4)v0v7 (8)
Can int and float args share slots?No — counted separatelyYes — one shared position countNo — counted separately
Return valuerax (+ rdx)raxx0 (+ x1)
Return addresspushed on stackpushed on stacklink register x30
Scratch space for calleered zone, 128 bytesshadow space, 32 bytes — caller allocatesnone

The first two columns run on the same silicon. Everything that differs between them is pure convention — which is the whole point of this lesson.

The Windows row about shared slots is the nastiest difference: in Windows x64, a function taking (int, double, int) puts them in rcx, xmm1, r8 — the positions are shared, so the float consumes slot 2 and the second int takes slot 3. Under SysV the same function uses edi, xmm0, esi. Identical source, identical CPU, incompatible machine code.

Predict firstcommit in writing, then look
struct Pair { long a; long b; };

long sum(struct Pair p) {
    return p.a + p.b;
}

A 16-byte struct, passed by value. Does it arrive in memory or in registers?

Where the contract shows up in a listing

Three things become readable once you know the convention:

Argument count and types. A function that reads edi, esi, and xmm0 on entry took two integers and a float. You can recover a signature from an unnamed function.

Whether a call happened. Before any call, arguments are marshalled into the convention’s registers. A cluster of movs into rdi/rsi/edx immediately before a call is argument setup, and reading it tells you what was passed.

Whether a function is a leaf. A leaf makes no calls, so it can use caller-saved registers freely and skip most of the prologue. On ARM64 the tell is whether x30 is saved. That is 2.3.

Beaconsee this → think this, without decoding it

a run of movs into rdi, rsi, edx immediately before a call

Argument marshalling. Read it backwards to recover the call: the value going into rdi is argument one.

This is the single most useful pattern for navigating unfamiliar disassembly, because it lets you reconstruct the call graph and the data flowing through it without any symbols beyond the call target.

Why you should care beyond reading

Two practical consequences a tech lead actually meets.

ABI stability is a real constraint on library design. Changing a struct’s layout in a published header changes how it is passed and where its fields are. Recompiling one side and not the other produces silent corruption. This is why C libraries version so carefully, why Rust has no stable ABI and passes everything through extern "C" at boundaries, and why “just add a field to that struct” is sometimes a breaking change.

FFI is where this bites. When you call C from Rust, or anything from JavaScript through a native module, both sides must agree on the convention. Most FFI bugs that produce garbage values rather than crashes are ABI mismatches.

Explain it to yourselfwrite it — thinking it does not count

A colleague has a native module that returns correct results on their Linux CI and garbage on a developer’s Windows machine. Same architecture, same compiler version, same source.

Give the most likely explanation and the specific thing you would ask them to check first.

What you should now be able to say

  • The CPU has no concept of an argument. The ABI is a document, not hardware.
  • SysV uses rdi rsi rdx rcx r8 r9; Windows x64 uses four registers and shares positions between integer and float args; ARM64 uses x0x7.
  • Violations fail silently — wrong values, not errors.
  • Small structs are often passed in registers, so by-value can beat by-pointer.
  • Argument marshalling before a call lets you reconstruct signatures from unnamed code.
  • ABI stability constrains library design, and ABI mismatch is the usual cause of garbage-value FFI bugs.

Next: 2.3 — Prologue, epilogue, frame pointers.

In the wildgo read the real thing now, while it is fresh
  • System V AMD64 ABIThe actual document. Find the argument-classification algorithm for structs and read one page of it — it is far more intricate than anyone expects, and that intricacy is why struct passing surprises people.
  • ARM64 (AAPCS64) calling conventionThe ARM64 equivalent, and notably cleaner. Compare its argument rules with SysV's and you can see twenty years of hindsight.
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.