Reading Assembly

Part 2 · The calling contract

Prologue, epilogue, frame pointers

The bookkeeping at the top and bottom of every function — and the one flag that makes your profiler useless.

30 minbeacon trainingPOE
What this lesson is trying to break (3)
  • The frame pointer is required for correctness.
  • Every function has a prologue.
  • Profilers read debug info to build stack traces.

Every non-trivial function opens and closes with bookkeeping. Learn to skip it, and learn what its absence tells you.

The standard opening

"f":
        push    rbp             ; save caller's frame pointer
        mov     rbp, rsp        ; rbp now marks this frame
        sub     rsp, 32         ; allocate 32 bytes of locals
        ...
        leave                   ; = mov rsp, rbp ; pop rbp
        ret

rbp becomes a fixed landmark: locals sit at [rbp-8], [rbp-16], and arguments that arrived on the stack sit at [rbp+16] and up. Because rbp does not move while the function runs, every offset is constant — which is convenient for humans and for debuggers.

leave is a single instruction that undoes both steps.

Beaconsee this → think this, without decoding it

push rbp / mov rbp, rsp

A frame-pointer prologue. Skip both lines — the function starts after them.

Its absence is the more informative case. If a function begins immediately with real work and refers to locals as [rsp+k], it was compiled with -fomit-frame-pointer and rbp is being used as an ordinary register.

The frame pointer is optional

The compiler knows exactly how far rsp has moved at every point, so it can address locals relative to rsp directly. rbp is redundant.

Omitting it buys two things: one extra general-purpose register (a real gain when you only have sixteen), and two fewer instructions per call. Historically that was measured at around 1%.

The cost is that you break cheap stack unwinding.

Predict firstcommit in writing, then look
int add(int a, int b) {
    return a + b;
}

Compile at -O2.

Why this is a real engineering decision

A sampling profiler interrupts the program thousands of times per second and asks “where am I, and who called me?”. Answering the second half requires walking the stack.

With frame pointers that is trivial: each frame stores the previous rbp, so they form a linked list. Follow it. Cheap enough to do at high sampling rates.

Without them, the profiler needs DWARF unwind tables — extra data describing how to recover the caller at every instruction. These are large, sometimes absent in third-party or JIT-generated code, and expensive to interpret while sampling. The practical result is truncated stacks, stacks that stop at a library boundary, or plausible-looking nonsense.

This is why Fedora and Ubuntu re-enabled frame pointers by default, accepting roughly 1% throughput to make production profiling work. It is a genuine trade-off, and knowing which side you are on is useful: if you cannot profile production, you will lose far more than 1% to problems you never find.

ARM64

The shape is the same, the spelling differs:

        stp     x29, x30, [sp, #-16]!   ; save frame pointer AND link register
        mov     x29, sp
        ...
        ldp     x29, x30, [sp], #16
        ret

x29 is the frame pointer, x30 the link register. stp/ldp store and load a pair in one instruction, and the ! means pre-decrement — so allocation and saving happen together.

The tell for a leaf function is clean: if x30 is not saved, the function makes no calls. On x86 you have to infer that from the absence of call.

Explain it to yourselfwrite it — thinking it does not count

Your team wants to profile a production service and the flame graphs are mostly [unknown] above the third frame.

Give the likely cause and two options, with the cost of each. One option is a build-flag change; the other is not.

What you should now be able to say

  • The prologue is push rbp / mov rbp, rsp / optional sub rsp, N; leave undoes it.
  • The frame pointer is optional — the compiler can address locals off rsp.
  • Omitting it gains a register and ~1%, and breaks cheap stack unwinding.
  • Small leaf functions at -O2 typically have no prologue at all.
  • ARM64 saves x29/x30 as a pair; unsaved x30 means leaf.

Next: 2.4 — Who saves what.

In the wildgo read the real thing now, while it is fresh
  • Why Linux re-enabled frame pointersFedora and Ubuntu turned frame pointers back on by default, trading ~1% performance for usable profiling. Read the argument — it is a real engineering trade-off you may have to make, and both sides are reasonable.
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.