5.1.16 · D5C Programming

Question bank — Stack frames — how function calls work at the memory level

1,943 words9 min readBack to topic

Picture the whole idea before the traps: a downward-growing stack with main calling add.

Figure — Stack frames — how function calls work at the memory level

True or false — justify

Every answer must include why, never a bare yes/no.

The stack grows toward higher memory addresses on mainstream CPUs.
False. On x86/x64 and ARM each push decreases the stack pointer, so the stack grows toward lower addresses — the "top" of the stack is the lowest address in use (see the figure above, where SP sits at the bottom).
Every function call creates a brand-new stack frame.
True (for a normal, non-inlined call). Each call needs its own private scratch region for locals, saved registers, and a return address, so a fresh frame is carved every time — this is exactly why recursion keeps each n separate.
Two simultaneously-active calls to the same function share the same storage for their local variables.
False. Each active call has its own frame, so each has its own copy of every local. In Recursion fact(3) and fact(2) are alive at once and have two independent n slots.
A stack frame is destroyed the instant its function returns.
True in effect. "Destroyed" just means the stack pointer is moved back so those bytes are reusable; nothing is erased, but the memory no longer belongs to that call and the next call may overwrite it.
The return address is always stored on the stack.
False. On x86 the CALL instruction pushes it to the stack, but on ARM BL (branch-and-link) and RISC-V JAL (jump-and-link) it first goes into a link register (LR); it only reaches the stack if that function calls another and must spill the link register.
The frame/base pointer (BP) and the stack pointer (SP) always point to the same place.
False. SP marks the moving top of the stack (it shifts as you push args for further calls); BP is a fixed anchor inside the current frame so locals stay at constant offsets like [BP-8] even while SP moves.
Cleaning up a frame on return requires deleting each local variable one at a time.
False. Cleanup is a single move of SP back to where it was before the call — that is the whole point of LIFO discipline and why returns are cheap.
Because function lifetimes are nested, a stack is the natural structure for calls.
True. If f calls g, then g always finishes before f, so the newest frame is always the first to die — which is exactly last-in-first-out (LIFO), what a stack provides.
Arguments passed to a function are always placed on the stack.
False. Many calling conventions pass the first few arguments in registers for speed; only extra arguments (or under old conventions) spill onto the stack.
A malloc'd block lives in the same region as a function's local variables.
False. Locals live on the stack (freed automatically on return); malloc gives you heap memory that outlives the call and must be freed manually.

Spot the error

State what is wrong and why, in one or two sentences.

int *f(void){ int x=5; return &x; } — what's the bug?
x lives in f's frame, which is logically destroyed the moment f returns; the returned pointer aims at memory that no longer belongs to you (undefined behaviour). Fix: return x by value or use malloc.
"The dangling-pointer program printed 42, so returning &local is fine."
The bytes often still happen to hold 42 right after return, but it's undefined behaviour — the next function call reuses that stack space and the value silently changes.
"On ARM, a leaf function that calls nobody must still push LR to the stack."
A leaf function makes no further calls, so LR is never overwritten; it can keep the return address in the link register and skip the stack spill entirely.
"I stored the address of a local, waited, and it's still valid because nothing else ran."
Even with no further call, the stack region below the old SP is fair game the moment the frame is gone; correctness cannot depend on "nothing overwriting it yet."
"Recursion is slow only because of the extra math."
The real cost is that each call carves a full new frame; too many pending frames never get freed, which is how deep recursion causes a stack overflow.
"Arguments always sit below BP because everything below BP is the frame."
On a downward-growing stack, locals (pushed after BP) sit at lower addresses (BP - offset), but arguments and any spilled return address were pushed before BP and sit at higher addresses (BP + offset).

Why questions

Explain the reasoning, not just the fact.

Why do we save and restore the caller's BP in every prologue/epilogue?
BP is a single shared hardware register; the callee overwrites it to anchor its own frame, so it must save the caller's BP on entry and restore it on exit or the caller loses its frame reference.
Why are locals addressed relative to BP instead of SP?
SP keeps moving (for example when pushing arguments for a nested call), so an offset from SP would change mid-function; BP stays fixed for the frame's whole life, giving each local a constant [BP - offset].
Why does the CALL instruction need to save a return address at all?
So the called function knows exactly which instruction to jump back to when it finishes; without it, control flow would never find its way home after the function ends.
Why does deeply nested recursion cause a stack overflow but a deep loop does not?
A loop reuses one frame each iteration, while each recursive call stacks a new frame that isn't freed until it returns, so the stack region eventually runs out of space.
Why can a program's stack cleanup be "just move SP" while heap cleanup needs explicit free?
Stack lifetimes are perfectly nested (LIFO), so resetting SP frees everything above it at once; heap lifetimes are arbitrary, so each block must be released individually.
Why is overwriting the saved return address dangerous?
RET blindly jumps to whatever address is stored there, so corrupting it (for example via a buffer overflow) can redirect execution to attacker-chosen code.

Edge cases

Cover the boundaries the mechanism invites.

What happens to the frame of fact(1) (the base case) relative to fact(3)?
fact(1) is the deepest and newest frame, so by LIFO it returns and is freed first, then fact(2), then fact(3) — the reverse of the order they were created.
A function with zero local variables and zero arguments — does it get a frame?
It still needs somewhere for its return address (and possibly a saved BP), so it has at least a minimal frame; the "carve N bytes for locals" step just uses N = 0.
At the very deepest point of fact(3), how many frames are live at once?
Three: fact(3), fact(2), and fact(1) are all paused simultaneously, each holding its own n, because none can finish until the one below it returns.
If main itself is a function, does it have a frame too?
Yes — main is an ordinary function with its own frame holding its locals and a return address back into the C runtime startup code that called it.
What is the state of the stack pointer immediately after a function returns but before the caller stores the result?
SP has been reset to exactly where it was just after the arguments were placed (or fully back, depending on convention), and the return value sits in a register waiting to be stored.
Does a tail call — return g(x); as the very last action — always create a new frame?
Not necessarily: with tail-call optimization the compiler can reuse the current frame instead of stacking a new one, since the caller has no work left to do after g returns, which turns tail recursion into a loop and avoids stack overflow.
Do all CPUs grow their stack downward?
No — downward is just the mainstream convention (x86, ARM). A few rare architectures (some older or embedded designs, and certain configurable cores) grow the stack upward toward higher addresses; the physics is identical, only the direction of push flips, so [BP + offset] and [BP - offset] swap roles.
Can a returned value (not pointer) safely carry a local's data out of a dying frame?
Yes — returning by value copies the data into a register or the caller's frame before the callee's frame is destroyed, so nothing dangles; only returning the address of a local is unsafe.

Recall One-line summary of the traps

Stack usually grows down; each call gets a fresh frame freed by moving SP; locals anchor to BP not SP; the return address may live in a link register first; tail calls may reuse a frame; and never hand out the address of something on a frame that's about to die.