What does one frame on a thread's call stack hold, and why does returning from the call release it for free?
answer
- one slice per call in progress
- strictly last in, first out
- return address plus saved registers
- locals and spilled temporaries live there
- epilogue just moves the stack pointer
basics
~20 sA call frame holds that call's return address, saved registers, local variables and spilled temporaries, plus space for outgoing arguments. Returning moves the stack pointer back past the whole frame, so release is one instruction with no bookkeeping.
solid answer
~50 sEach call in progress owns a contiguous slice of its thread's stack, reserved on entry and given back on return. That slice - the frame - typically holds the return address, the caller's saved frame pointer and any callee-saved registers, the call's local variables, temporaries the compiler could not keep in registers (spills), and space for arguments to further calls. Frames nest strictly last-in-first-out: a callee's frame sits above its caller's and always dies first. That ordering is what makes release free - the epilogue restores the stack pointer to where the frame began and everything above it is instantly reusable. There is no free structure to search, no size to look up and nothing to decide later, which is why per-call storage costs a pointer move while a general heap block costs a lookup now and a reclamation decision afterwards.
code
pseudocode · 17 lines// caller
record return address
jump to callee
// callee prologue
push saved frame pointer
frame pointer <- stack pointer
stack pointer <- stack pointer - 48 // locals + spills, size fixed before the call runs
// callee body
write local_a at frame pointer - 8
write local_b at frame pointer - 16
// callee epilogue
stack pointer <- frame pointer // the whole frame is released in one move
pop saved frame pointer
return // control resumes at the return addressgo deeper
Be able to name what a frame carries - return address, saved registers, locals, spills - and say that the call stack is last-in-first-out, so returning gives the space straight back.
Explain the prologue and epilogue: one constant subtracted from the stack pointer on entry, one move back on exit, with locals at fixed offsets. Say why that makes release cost a pointer move rather than allocator work.
Draw the consequence for real defects: the freed bytes stay mapped and get overwritten by the next call, so a pointer to a dead frame reads another call's data rather than faulting cleanly.
Frame the trade: free deallocation is bought with a fixed per-thread ceiling and a lifetime you cannot choose. Know when a design should pay allocator cost instead of living inside that ceiling.
## The frame is a slice of one thread's stack A thread runs with a **stack pointer** that marks the boundary between the memory in use by the calls currently in progress and the free space beyond it. Starting a call moves that pointer to reserve a contiguous slice; returning moves it back. That slice is the **call frame** (also called an activation record), and there is exactly one live frame for every call that has begun and not yet returned. Frames nest **last in, first out**. A callee's frame sits between its caller's frame and the free space, and it always dies first, because control returns to the caller before the caller can return to anyone. That ordering is not a policy some runtime chose; it falls directly out of how calls nest. ## What one frame holds The exact layout is the compiler's business and differs across platforms, but the categories are stable: - **The return address** - where execution resumes in the caller once the callee finishes. - **The caller's saved frame pointer**, when the compiler keeps one: it gives a stable base for addressing locals while the stack pointer keeps moving, and it chains the frames so tooling can walk the live call chain. - **Callee-saved registers** the function intends to overwrite, parked so the caller's values survive the call. - **Local variables** the compiler cannot keep in registers - typically those whose address is taken, aggregates, and anything still live across another call. - **Spilled temporaries**: intermediate values evicted from registers because the function needs more simultaneously live values than the machine has registers. - **Outgoing argument space** for the calls this function makes, when the arguments do not fit in the registers reserved for that purpose. Two consequences are worth naming. First, a great deal of a function's data never touches the frame: a value that lives its whole life in a register costs zero stack bytes. Second, the frame's size is normally fixed before the function runs - the prologue subtracts a constant from the stack pointer - unless the language permits a local whose size is known only at run time, which forces a dynamic adjustment instead. ## Why returning releases it for free A call and its return are essentially four steps: 1. The call instruction records the return address and jumps to the callee. 2. The callee's prologue saves what it must save and subtracts its frame size from the stack pointer. 3. The body runs, addressing locals at fixed offsets from the frame's base. 4. The epilogue restores the saved registers, moves the stack pointer back past the frame, and returns. Step 4 is the whole of deallocation. Nothing is searched, no size is looked up, no free list is threaded, no neighbouring block is coalesced, and nobody has to decide later whether the storage is still needed. Because frames die in reverse order of birth, the free area is always the one contiguous span above the stack pointer, so a single pointer move restores it exactly. | | a frame on the stack | a block from a general-purpose allocator | |---|---|---| | reserving | subtract a constant from the stack pointer | find a free block of a suitable size | | releasing | move the stack pointer back | hand it back, or wait for reclamation | | lifetime | fixed: it ends when the call returns | chosen by the program or by reachability | | ordering | strictly last in, first out | any order at all | | running out | overflow past the thread's stack limit | allocation failure, or free space you cannot use | ## What the cheap release does not buy you - **The bytes are neither erased nor unmapped.** After the return that span is simply free area again, and the next call writes its own frame over it. A pointer kept to a returned call's local therefore reads whatever the next call put there - usually plausible-looking garbage, occasionally another call's live data. - **The budget is fixed and small.** A thread's stack has a hard limit, so the price of free release is a ceiling: how deep you can nest is that limit divided by what each frame costs. - **Not every call in the source gets a frame.** A compiler may inline a callee into its caller, and a small leaf function may run entirely in registers, so the frames on the stack are the ones that survived optimisation rather than the calls you wrote. The first point is why a stale pointer into a dead frame is a real bug class and not a harmless read. The second is why the stack is a budget you are expected to do arithmetic about rather than a resource you can treat as unlimited.
- If the stack pointer already tracks the top of the stack, what does the saved frame pointer in a frame buy?It gives a fixed base for addressing locals while the stack pointer keeps moving during the call, and it chains each frame to its caller's so a debugger or profiler can walk the live call chain. Compilers can omit it, address everything relative to the stack pointer, and publish separate unwind data instead.
- Why is a frame's size usually decided before the call runs, and what changes when a language allows a local sized at run time?A fixed size lets the prologue reserve the frame by subtracting one constant and lets every local sit at a known offset. A run-time-sized local forces the reservation to be computed during the call, so the stack pointer moves by a variable amount and a saved frame pointer becomes the practical way to restore it on return.
A pile of note cards on a spike: each call drops one card holding where to go back to and its working notes. Returning is lifting the top card off - there is nothing else to tidy, and the next card written lands on the same spot.
saying these in an interview costs you the question
- Thinks local variables always live on the heap and frames hold only return addresses
- Believes a returned frame is wiped or unmapped, so stale pointers into it are harmless
- Says releasing a frame needs an allocator to find and merge a free block
- Gets the ordering wrong: expects a caller's frame to be released before its callee's
- Assumes every call written in the source must push a frame at run time