When you write int x = foo(y);, the compiler emits a small ritual on entry to foo: save
the old frame pointer, anchor the stack pointer to it, reserve space. That ritual is the
prologue. Before returning, a mirror-image epilogue tears everything down. The stack
frame in between is where locals live, where the return address sits, and where the fixed
per-call cost of every program you've ever written is paid.
rsp, rbp, and the shape of a frame
Two registers define every frame on x86-64. rsp is the stack pointer — it always points at
the current top of the stack, and it is authoritative: the ABI assumes it is correct at
every function boundary. rbp is the frame pointer — a convenience register that freezes
the frame's base address, so the compiler can address locals and arguments relative to
something that doesn't move while the frame grows (it can grow if the function calls
alloca or spills register arrays).
┌─────────────────────────┐ lower addresses
│ locals (negative rbp offsets) │ ← rsp points here
├─────────────────────────┤
│ saved rbp │ ← rbp
├─────────────────────────┤
│ return address │ ← pushed by `call`
├─────────────────────────┤
│ caller's frame │
└─────────────────────────┘ higher addressesEverything above rbp belongs to the caller or the call machinery. Everything from rbp
down is the current frame. The frame pointer is pure convenience — the ABI does not require
it, and that fact shapes the performance story below.
The prologue: what the compiler does on entry
Compiled with a frame pointer, foo starts like this:
foo:
push rbp ; save caller's frame pointer
mov rbp, rsp ; freeze this frame's base
sub rsp, 32 ; reserve 32 bytes for locals
...Three instructions. The third one is where stack alignment happens: the System V AMD64 ABI
demands that rsp be 16-byte aligned at the point of a call, and the compiler sizes the
reservation so that property holds no matter how the frame is used.
The epilogue: leaving without a trace
The epilogue undoes all of it, usually with a single instruction:
leave ; mov rsp, rbp; pop rbp — both in one opcode
ret ; pop the return address, jump to itleave is literally mov rsp, rbp followed by pop rbp — it doesn't matter how much space
the prologue reserved, because rbp knows where the frame started. That is the frame
pointer's payoff: the epilogue is always one instruction, regardless of frame size.
The red zone: 128 bytes you didn't allocate
The System V ABI grants each function a red zone: 128 bytes below rsp that a leaf
function (one that calls nothing) may use as scratch without moving the stack pointer.
It's safe because nothing asynchronous writes below rsp — signal handlers and hardware
interrupts push above it. The compiler uses the red zone whenever a leaf's locals fit:
tiny:
mov QWORD PTR [rsp-8], rdi ; store arg directly in the red zone
mov rax, [rsp-8]
retThat saves the sub rsp, N / add rsp, N pair — two instructions per leaf call. Windows
x64 has no red zone, which is why equivalent code differs between platforms.
Frame pointers vs -fomit-frame-pointer
A frame pointer costs two things: two extra instructions per call (the push rbp /
mov rbp, rsp in the prologue, the leave in the epilogue) and a whole register that can't
be used for anything else. -fomit-frame-pointer removes both and instead locates locals
relative to rsp. The price is invisibility: without rbp chains, unwinding the stack
requires DWARF .eh_frame tables emitted by the compiler — and if the binary is stripped,
backtraces turn into noise. This is exactly the trade-off distros make when they ship
-O2 -fomit-frame-pointer production binaries that produce useless perf stacks, and why
kernel and debug builds keep frame pointers on.
The fixed cost of every call
Even the smallest call pays something:
call/ret: push and pop of the return address — a memory write and read of the stack, ~1–2 cycles each.- Return prediction: the CPU predicts
rettargets with a return stack buffer; a mispredicted return costs 15–20 cycles — more than the call itself. - The frame, if any: prologue, epilogue, and the cache/TLB pressure of a growing stack.
None of this shows in source. It's why compilers inline aggressively — an inlined call has zero frame cost — and why "it's just a function call" is never a complete sentence about performance.