5.1.16 · D4C Programming

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

2,327 words11 min readBack to topic

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 push decreases 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
  1. Local variables of that call.
  2. Saved BP (the caller's frame reference, so it can be restored).
  3. The return address (where to jump back to) — on the stack for x86; in the link register first for ARM/RISC-V.
  4. 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
  1. push old BPWHY: BP is shared hardware; save the caller's so we don't lose the caller's frame.
  2. BP = SPWHY: mark the base of this frame; now locals get constant offsets from BP.
  3. SP -= NWHY: carve N bytes for locals (here, room for s).

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 (cdecl pushes right-to-left, so 3 first then 2).
  • retaddr — pushed by the CALL instruction itself on x86, so add knows where to come back to.
  • add's prologue has not run yet, so no saved BP / no s on 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) calls fact(3) calls fact(2) calls fact(1). At the moment fact(1) runs, none has returned yet.
  • So n=4, 3, 2, 1 occupy four separate desks. fact(1) (deepest) returns first — strict LIFO — then 2, then 3, then 4.
  • See Recursion for why each n is 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).

  • local lives in bad'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 42 by luck right after return, which is exactly what makes the bug sneaky.
  • Fix: return local; (by value) or malloc on 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, BL stores 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 LR to 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 after BP = SP.
  • Above BP (add): saved BP, return address, then the three args.
  • Right-to-left push means c went on first (highest address), a last (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 → needs 3 * fact(2), waits.
  • fact(2): n=2, not base → needs 2 * 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
  • buf starts at BP - 8. The return address starts at BP + 4.
  • Distance = (BP + 4) - (BP - 8) = 12 bytes.
  • 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 buf grows 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) executes BL fact to call fact(1), the return address into fact(2) is placed in LR.
  • But fact(1) is itself non-leaf conceptually / shares the same LR — the instant fact(1)'s own prologue (or any nested BL) 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 before BX 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.