Every time a procedure is called, it needs a private scratch space: somewhere to keep its parameters, its local variables, the address to jump back to when it finishes, and whatever registers it must borrow and restore. That bundle of per-call storage is an activation record — also called a stack frame. Because calls nest in strict last-in-first-out order (the last procedure called is always the first to return), these records live on a stack: push one on entry, pop it on exit. This single data structure is what makes procedures, and recursion, possible at all.
The frame is the meeting point of the whole run-time system. The
A frame is a small, ordered collection of fields. Compilers vary in the exact order, but a canonical activation record contains these parts, from the caller's side down into the callee's:
| Field | Purpose |
|---|---|
| Actual parameters | the argument values passed in by the caller |
| Returned value | slot for the result handed back to the caller |
| Return address | where to resume in the caller after this call finishes |
| Control link (dynamic link) | pointer to the caller's frame — restores the stack on return |
| Saved registers / machine state | register values to restore so the caller sees them unchanged |
| Local variables | the procedure's own automatic variables |
| Temporaries | intermediate values from evaluating expressions |
The control link (sometimes dynamic link) is the thread that ties the stack together: each frame points to the frame of the procedure that called it. Following the control links from the top of the stack downward walks the exact chain of active calls — the very thing a debugger prints as a backtrace.
The machine tracks the stack with two registers. The stack pointer (SP) marks the current top of the stack — the boundary between used and free space. The frame pointer (FP, also called the base pointer) marks a fixed anchor inside the current frame, from which every parameter and local is addressed at a constant offset.
Why two? Because the stack pointer moves during a procedure — pushing temporaries, making
nested calls — so offsets from SP would be a moving target. The frame pointer stays put for the whole
activation, so the compiler can compile "local x" into a fixed [FP − 8] and
"parameter n" into [FP + 16], once, at compile time. The control link is
exactly the saved old FP: on return, you restore FP from it and the previous frame snaps back
into place.
Reveal the frames one at a time. Here main calls fact(3), which calls
fact(2). Each new frame is pushed below the previous one (the stack grows toward
low addresses), and each carries a control link pointing back down to its caller's frame. When
fact(2) returns, its frame is popped and FP follows the control link back to
fact(3).
Setting up and tearing down a frame is a little dance split between the two parties. The caller knows the arguments and where it wants to resume; the callee knows its own locals and which registers it will clobber. A calling convention is precisely the contract that decides who does which step — get it wrong on either side and the stack corrupts.
| Step | Done by |
|---|---|
| Evaluate arguments, place them where the callee expects | Caller |
| Save caller-saved registers still needed after the call | Caller |
| Push return address and jump to the callee | Caller (the call instruction) |
| Save old FP (the control link), set FP to the new frame | Callee (prologue) |
| Allocate space for locals; save callee-saved registers it will use | Callee (prologue) |
| Run the body; place the result in the return slot | Callee |
| Restore callee-saved registers; restore FP from control link; pop the frame | Callee (epilogue) |
| Jump back to the return address | Callee (the ret instruction) |
| Restore caller-saved registers; read the returned value | Caller |
The neat property is symmetry: the epilogue undoes the prologue in reverse, and the caller's cleanup undoes its setup. Do them in the right order and the stack pointer ends exactly where it began, ready for the next call.
Recursion is the killer argument for stack-allocated frames. When fact(3) calls
fact(2), the value n = 3 must be remembered while n = 2 is being
worked on — and then n = 1, and so on. Each active call needs its own copy of
n. A single static slot could hold only one; a stack gives every activation a fresh frame,
automatically, at whatever depth the recursion reaches.
This simulator makes the frames explicit. It never calls the language's own recursion — it pushes and pops model activation records on an array, exactly as the machine would, and prints the frame at every step:
The output shows four frames stacking up on the way down and unwinding in reverse on the way back — the push order and the pop order are mirror images, which is the LIFO discipline that a stack exists to provide.
You do not strictly have to — many optimizing compilers omit the frame pointer and address
everything from SP, freeing that register for general use (the -fomit-frame-pointer flag).
The catch is that SP moves whenever the frame changes size (a push, an alloca,
a nested-call argument spill), so the compiler must track SP's offset at every program point — doable,
but it complicates debugging and stack unwinding, which is why frames used to always keep FP. Modern
toolchains split the difference: omit FP for speed, but emit separate unwind tables so
debuggers and exception handlers can still reconstruct the call chain.
The single most error-prone part of a calling convention is the register-saving split. Registers are divided into caller-saved (volatile — the callee may freely clobber them, so the caller must save any it still needs) and callee-saved (non-volatile — the callee must preserve them, saving and restoring any it uses). Save a register on the wrong side and one of two bugs appears: a value silently corrupted across a call (nobody saved it), or wasted work (both sides saved it). And the teardown order matters: the epilogue must restore callee-saved registers and the old FP before popping the frame and returning — reverse those and you either return to a garbage address or restore from bytes that are already gone. Prologue and epilogue must be exact mirror images.