Reading Assembly

Part 2 · The calling contract

The stack is a convention

Not a data structure the CPU provides. A register, a direction, and an agreement — which is why it can be corrupted.

30 minrefutation textPOE
What this lesson is trying to break (3)
  • The stack is a hardware data structure.
  • The CPU prevents you from corrupting it.
  • The stack grows upward, like an array.
Unlearn thiscarried over from every language where the stack is just where locals live
You think

The stack is a structure the CPU manages. It has push and pop, it keeps track of frames, and the hardware makes sure functions do not interfere with each other’s locals.

Wrong here

The CPU provides one registerrsp on x86-64, sp on ARM64 — and a couple of instructions that adjust it. That is the entire hardware contribution. There is no frame tracking, no bounds checking, and no protection between one function’s space and another’s.

Instead

Read the stack as a pointer plus an agreement. Generated code all agrees to grow downward, to leave rsp where it found it, and to respect certain regions. Because it is agreement rather than enforcement, it can be broken — which is the entire subject of memory-safety exploitation, and the reason Rust’s ownership rules exist one level up.

Downward

The stack grows toward lower addresses. This trips people up every time because it inverts the usual mental picture.

high addresses
   ┌──────────────────┐
   │ caller's frame   │
   ├──────────────────┤
   │ return address   │  ← pushed by `call`
   ├──────────────────┤
   │ our locals       │
   │ our spills       │
   ├──────────────────┤  ← rsp points HERE (lowest in-use address)
   │ (unused)         │
   ▼ grows this way
low addresses

So:

  • push rax = rsp -= 8, then store rax at [rsp]
  • pop rax = load rax from [rsp], then rsp += 8
  • sub rsp, 32 = allocate 32 bytes of local space
  • add rsp, 32 = free it

Allocating stack memory is a subtraction. That is the whole cost — no allocator, no bookkeeping, no free list. This is exactly why Rust could promise that stack values are cheap: putting a value on the stack costs the arithmetic that was going to happen anyway when the frame was set up.

Predict firstcommit in writing, then look
int f(int x) {
    int arr[4];
    for (int i = 0; i < 4; i++) arr[i] = x + i;
    return arr[0] + arr[3];
}

Compile it for x86-64. Do you expect to see sub rsp, ... at all?

call and ret are the only magic

call target does two things: push the return address, then jump. ret does one: pop into the instruction pointer.

That is the entire mechanism. Which means the return address is data on the stack, sitting a fixed distance from your locals, writable by any code that can compute its address. Overrun an array on the stack and you can overwrite where the function will return to.

Every stack-safety mechanism you have heard of exists because of that sentence: stack canaries put a known value between locals and the return address so overwriting it can be detected; non-executable stacks stop the overwritten target being useful; ASLR makes the target hard to predict.

ARM64 does this differently and better: bl (branch-with-link) puts the return address in the link register x30 rather than on the stack. A leaf function never spills it, so there is nothing on the stack to overwrite. Non-leaf functions must save x30 themselves — which is why ARM64 prologues look different, and why you will see stp x29, x30, [sp, #-16]! as the standard opening move.

Beaconsee this → think this, without decoding it

sub rsp, N at the top of a function

A stack frame of N bytes is being allocated, and N tells you roughly how much did not fit in registers.

A large N in a hot function is worth a second look: it usually means heavy spilling, a large local aggregate, or an address that escaped. In a loop, stores and loads to the same [rsp+k] addresses every iteration is the signature of spilling.

Note that N is usually rounded up for alignment (2.4), so it is a lower bound on padding, not an exact accounting.

Explain it to yourselfwrite it — thinking it does not count

Rust told you that moving a value is cheap and that stack allocation is free. Using this lesson, explain why both are true, mechanically — what instruction does the work in each case.

Then answer: what would have to be true of a Rust value for it to not be free to put on the stack?

What you should now be able to say

  • The hardware provides one register. Everything else is agreement.
  • The stack grows downward; push is a subtract-then-store.
  • Allocating stack space is sub rsp, N — arithmetic, not allocation.
  • Locals reach memory only when forced to; otherwise they stay in registers.
  • call pushes the return address, making it overwritable data — the root of stack-smashing and of every mitigation for it.
  • ARM64 uses a link register instead, so leaf functions keep the return address off the stack entirely.

Next: 2.2 — The ABI is a contract nothing enforces.

In the wildgo read the real thing now, while it is fresh
  • Smashing the Stack (Aleph One, 1996)The paper that made stack overflows famous. Read the first few sections only. Every mitigation you have heard of — canaries, ASLR, non-executable stacks — exists because the stack is a convention that can be violated.
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.