Exercises — Stack frames — how function calls work at the memory level
This page is a self-test ladder. Each rung is harder than the last: from just recognising the parts of a frame, up to synthesising your own layout and reasoning about security. Every problem has a full solution hidden inside a collapsible [!recall]- callout — try first, then reveal.
If any word here feels unfamiliar, go back to Stack frames — how function calls work at the memory level first; everything below assumes only what that note built. Related ideas: Recursion, Pointers and addresses in C, Heap vs Stack memory, Calling conventions (cdecl, stdcall), Buffer overflow & return-address smashing, Assembly: push, pop, call, ret, BL.
Level 1 — Recognition
Goal: can you name the parts and read a picture of the stack?
L1·Q1
Which register always points at the top (newest end) of the call stack: SP or BP?
Recall Solution
SP (the stack pointer).
- WHAT: SP holds the address of the top of the stack — the newest byte in use.
- WHY not BP: BP (base/frame pointer) is a fixed reference point inside the current frame; it does not move while the function runs, so locals can be found at constant offsets. SP moves every push/pop.
- Picture: in the figure above, SP is the arrow at the very bottom (lowest address, because the stack grows down).
L1·Q2
On a downward-growing stack, is the "top of stack" the highest or lowest address currently in use?
Recall Solution
The lowest address.
- Each
pushdecreases SP, so the newest data sits at the smallest address. - The word "top" is about recency, not about height on the page. Newest frame = smallest address = "top".
L1·Q3
Name the four things a typical stack frame contains.
Recall Solution
- Local variables of that call.
- Saved BP (the caller's frame reference, so it can be restored).
- The return address (where to jump back to) — on the stack for x86; in the link register first for ARM/RISC-V.
- Its arguments (often — depends on the calling convention).
Level 2 — Application
Goal: apply the prologue/epilogue steps to concrete code.
L2·Q1
For int add(int a, int b) { int s = a + b; return s; }, list — in order — the operations the callee's prologue performs on entry (conceptual x86-style).
Recall Solution
push old BP— WHY: BP is shared hardware; save the caller's so we don't lose the caller's frame.BP = SP— WHY: mark the base of this frame; now locals get constant offsets from BP.SP -= N— WHY: carveNbytes for locals (here, room fors).
The epilogue undoes it in reverse: SP = BP (drop locals) → pop old BP (restore caller) → RET (jump back).
L2·Q2
main calls add(2, 3). Give the stack contents (top written last) right after CALL add has executed, x86-style. Assume main already has local x.
Recall Solution
[ x ][ 3 ][ 2 ][ retaddr ]
x— main's local, oldest.3,2— arguments pushed by the caller (cdeclpushes right-to-left, so3first then2).retaddr— pushed by theCALLinstruction itself on x86, soaddknows where to come back to.add's prologue has not run yet, so no saved BP / noson the stack in this snapshot.
L2·Q3
Using addr(local_k) = BP - offset_k, a local sits at BP - 8 and BP holds address 0x7ffe_ff40. What address is the local at? Is it above or below BP in memory?
Recall Solution
0x7ffeff40 - 8 = 0x7ffeff38.- It is at a lower address than BP → below BP in memory. Locals are pushed after BP is set, and pushing moves down, so they live below BP → we subtract.
Level 3 — Analysis
Goal: reason about how many frames live, and what breaks.
L3·Q1
For int fact(int n) (the recursive factorial), how many frames are simultaneously alive at the deepest point when you call fact(4)?
Recall Solution
4 frames.
fact(4)callsfact(3)callsfact(2)callsfact(1). At the momentfact(1)runs, none has returned yet.- So
n=4, 3, 2, 1occupy four separate desks.fact(1)(deepest) returns first — strict LIFO — then 2, then 3, then 4. - See Recursion for why each
nis an independent memory slot.
L3·Q2
int *bad(void){ int local = 42; return &local; }. After bad returns, is the returned pointer valid? Explain in terms of SP.
Recall Solution
No — it is a dangling pointer (undefined behaviour).
locallives inbad's frame. On return, the epilogue moves SP back up, logically destroying that frame.- The pointer still holds the old address, but that memory no longer belongs to
bad— the next call will overwrite it. - It might still read
42by luck right after return, which is exactly what makes the bug sneaky. - Fix:
return local;(by value) ormallocon the heap if it must outlive the call.
L3·Q3
A function has no locals and makes no further calls. On ARM, does its return address ever touch the stack?
Recall Solution
No.
- On ARM,
BLstores the return address in the link register (LR), not the stack. - LR only gets spilled to the stack if the function calls another function (which would overwrite LR). A leaf function with no calls keeps its return address purely in LR and just does
BX LRto return. See Assembly: push, pop, call, ret, BL.
Level 4 — Synthesis
Goal: build a layout / trace yourself from scratch.
L4·Q1
Design the frame for int f(int a, int b, int c) with two locals int p, q. On a 32-bit x86 layout (4 bytes each; saved BP at [BP], return address at [BP+4]), give the offset of each argument and local relative to BP. Assume args were pushed right-to-left.
Recall Solution
| Item | Offset from BP | Reason |
|---|---|---|
q |
BP - 8 |
second local, deepest |
p |
BP - 4 |
first local |
| saved BP | BP + 0 |
prologue pushed old BP, then set BP here |
| return address | BP + 4 |
pushed by CALL, just above saved BP |
a |
BP + 8 |
first argument (pushed last → nearest above) |
b |
BP + 12 |
second argument |
c |
BP + 16 |
third argument (pushed first → farthest) |
- Below BP (subtract): the two locals
p, q, carved afterBP = SP. - Above BP (add): saved BP, return address, then the three args.
- Right-to-left push means
cwent on first (highest address),alast (closest to the return address).
L4·Q2
Trace fact(3) frame-by-frame. Show the value returned by each frame and the final answer.
Recall Solution
Build-up (push):
fact(3):n=3, not base → needs3 * fact(2), waits.fact(2):n=2, not base → needs2 * fact(1), waits.fact(1):n=1, base case → returns 1.
Unwind (pop, LIFO):
fact(2)resumes:2 * 1 = 2→ returns 2.fact(3)resumes:3 * 2 = 6→ returns 6.
Final answer: fact(3) = 6. Three frames were alive at the deepest point.
Level 5 — Mastery
Goal: connect layout to security and cross-architecture behaviour.
L5·Q1
A local char buf[8] sits at BP - 8, saved BP at BP + 0, return address at BP + 4 (32-bit). How many bytes must an attacker write past the start of buf to reach and overwrite the return address? Which classic attack is this?
Recall Solution
bufstarts atBP - 8. The return address starts atBP + 4.- Distance =
(BP + 4) - (BP - 8) = 12bytes. - So writing 12 bytes reaches the start of the return address; the 13th–16th bytes overwrite it.
- This is a stack buffer overflow / return-address smashing — because
bufgrows upward in index toward higher addresses (into the saved BP and return address), a write longer than 8 bytes clobbers control data. See Buffer overflow & return-address smashing.
L5·Q2
On ARM, a recursive fact obviously makes further calls. Explain precisely where the return address of fact(2)'s call to fact(1) lives while fact(1) is running, and why LR alone is insufficient.
Recall Solution
- When
fact(2)executesBL factto callfact(1), the return address intofact(2)is placed in LR. - But
fact(1)is itself non-leaf conceptually / shares the same LR — the instantfact(1)'s own prologue (or any nestedBL) runs, LR would be overwritten. - Therefore
fact(2)'s prologue spills LR to its stack frame before making the call, and its epilogue reloads LR beforeBX LR. - Why LR alone fails: LR is a single register — it can hold exactly one return address at a time. Recursion needs many simultaneously, so they must be parked on the stack. This is the register-file reason recursion still needs a stack even on ARM.
L5·Q3 (Synthesis)
Two threads run the same recursive function at the same time. Explain why their locals never collide, in one crisp sentence tied to the stack.
Recall Solution
Each thread has its own separate call stack, so each call's frame — and therefore each copy of every local — lives at a private address; identical code, independent desks.
- The name/offset of a local is fixed at compile time, but the storage is re-created per call per stack, so no two live frames ever share bytes.
Recall Final self-check (reveal after all levels)
Answer out loud: (1) Which end is "top"? lowest address. (2) Locals: add or subtract from BP? subtract. (3) Args: add or subtract? add. (4) How many frames for fact(4)? four. (5) Where does ARM put the return address first? the link register (LR). If any felt shaky, redo that level.