Visual walkthrough — Stack frames — how function calls work at the memory level
Step 1 — Memory is just numbered boxes, and one number remembers "the top"
WHAT. Picture the computer's memory (RAM) as a tall column of boxes. Each box holds one small value and each has an address — a whole number that names it. In the figure the address is printed on the left of every box.
WHY. Before we can talk about "where a function keeps its stuff", we need a way to point at a box. That is all an address is: the box's house-number. We also need a special helper that always remembers where the currently-used region ends. That helper is a register (a tiny built-in storage slot inside the CPU) called the stack pointer, written .
PICTURE. Look at the orange arrow labelled . It is not a box of data — it is a finger pointing at the address of the topmost box we are currently using.

Step 2 — "Down" is the direction of growth (this trips everyone up)
WHAT. On every mainstream CPU (x86, ARM), the stack starts high and grows toward smaller addresses. "Making the stack bigger" means subtracting from .
WHY not the other way? It is a historical convention so the stack (growing down) and other memory (growing up) can share one region and only collide if the program is truly out of memory. The key rule you must burn in:
- — push: we claim new boxes below the current top, so the address number goes down.
- — pop: we give boxes back, so the address number goes up.
PICTURE. The two arrows: a magenta arrow shows a push dragging downward (address 1000 → 992), a violet arrow shows a pop letting climb back up. Same finger, opposite directions.

Step 3 — One call needs three things, so one frame has three parts
WHAT. When main calls a function f, three questions must be answered. Each answer becomes a
slot in f's private region — its stack frame.
WHY these three? Because a call must survive being interrupted by itself calling something else, and must come back cleanly. The three questions:
| Question the call must answer | Slot that answers it |
|---|---|
| "Where do I jump back to when done?" | return address |
| "Where do I put MY locals without wrecking the caller's?" | the local-variable slots |
| "How do I still find my own locals after moves?" | ==frame pointer == |
PICTURE. One frame, exploded into its parts: the return address at the top (highest address of the frame), then the saved old , then a fresh region of locals at the bottom (lowest addresses). A green finger is planted at a fixed spot; a orange finger sits at the very bottom.

Step 4 — The caller's job: hand over arguments, then CALL
WHAT. Before jumping into f, the caller pushes the argument values (or on ARM/x64 often loads
them into registers — see Calling conventions (cdecl, stdcall)), then executes CALL f.
WHY the two sub-steps?
- Push arguments first —
fneeds the values it was handed, so they must live somewherefcan read them. CALLnext — on x86 theCALLinstruction does two things atomically: it pushes the return address (the address of the instruction right after the call) and then jumps tof. That is exactly the "where do I come back to?" question being answered.
PICTURE. Left half: main's frame with x inside. Watch the two magenta pushes: first 3 then
2 land below x, then a violet push drops the return address on top. has crawled down
three boxes.

Step 5 — The callee's prologue: nail down BP, carve out locals
WHAT. The first few instructions inside f are boilerplate called the prologue. In order:
push BP— save the caller's frame pointer so we can restore it later.BP = SP— plant our own finger here, marking this frame's base.SP -= N— slide down by bytes to reserve room forf's locals.
WHY each line?
- Save old — is shared hardware; if we overwrite it without saving, the caller loses track of its own frame. So we stash the old value first.
- — now marks a fixed landmark inside our frame.
- — reserving bytes in one subtraction is cheaper than pushing each local separately, and it makes all locals addressable relative to .
- — locals were carved below (lower addresses) → subtract.
- — the arguments and return address were pushed before → they sit above it → add.
PICTURE. The full frame of f. Green finger points at the saved-old- box. Above it,
labelled with , are the args and return address; below it, labelled with
, are the freshly carved local slots. Orange rests at the very bottom.

Step 6 — Worked trace: add(2,3) builds and tears down a frame
WHAT. Let us run the full life of one real frame.
int add(int a, int b) {
int s = a + b; // one local: s
return s;
}
int main(void) {
int x = add(2, 3);
return 0;
}WHY trace it? To see that cleanup is just moving back — nothing gets individually erased.
PICTURE. Four panels, left to right:
- In
main: onlyxon the stack. - After pushing args +
CALL:[x][3][2][retaddr]. - After
add's prologue:[x][3][2][retaddr][savedBP][s], withs = 2+3 = 5computed. - After
RET: has jumped all the way back up; the four boxesaddused are logically gone; the value5rode home in a register and got stored intox.

Step 7 — Recursion: the same picture, stacked N times
WHAT. Recursion is not a new mechanism — it is Step 5 happening again before the previous one finished. See Recursion.
int fact(int n) {
if (n <= 1) return 1;
return n * fact(n - 1);
}WHY it "remembers" every n. Each call runs its own prologue, so each gets its own slot for
n. Three simultaneous calls ⇒ three separate n boxes stacked up, each waiting on the one below.
PICTURE. fact(3) at its deepest: three frames piled downward, n=3 on top waiting, n=2
below waiting, n=1 at the bottom about to return 1 first (deepest returns first — strict
LIFO). Then watch them unwind upward: .

Step 8 — The degenerate case: returning a pointer to a local
WHAT. The dangerous edge case. What if a function hands back the address of one of its own locals?
int *bad(void) {
int local = 42;
return &local; // BUG
}WHY it is poison. From Step 6 we learned that RET just moves back up — the frame's boxes
are not erased, but they are now fair game for the next call to overwrite. The returned pointer
aims at boxes that no longer belong to you. It may still read 42 by pure luck, right up until the
next function call reuses those exact boxes.
PICTURE. Two panels: (1) inside bad, the pointer aims at the box holding 42. (2) after
return, has climbed above that box, and a new call other() has moved in and written 99
there — the pointer now reads garbage.

The one-picture summary
Everything above compressed into a single frame diagram: caller region on top, one full callee frame below it, both fingers ( nailed down, at the floor), every slot labelled with which of the three questions it answers and which direction it was pushed.

Recall Feynman: retell the whole walkthrough in plain words
Memory is a tall stack of numbered boxes, and one finger () always points at the top box we are
using. On real chips "growing the stack" means the finger moves down to smaller numbers.
When one function calls another, the caller drops the argument values into boxes, then the CALL
also drops a return address — a note saying "come back here." The called function then plants a
second finger () at a fixed landmark so it can always find its own locals, and slides the top
finger down to reserve blank boxes for them. Doing real work means reading and writing those boxes
at fixed distances from . When it finishes, it just slides the top finger back up — the
boxes are not scrubbed, they are simply abandoned, which is why cleanup is instant and why
returning a pointer into them is a time bomb. Recursion is this exact ritual done again before the
previous one finishes, so many copies of a local stack up, and the deepest one always returns
first. That last-in-first-out pile is the call stack.
Recall
Which direction does move on a push? ::: Down — to a smaller address (on x86/ARM).
Why do locals use but args use ? ::: Locals were carved below (lower addresses → subtract); args and return address were pushed before , so they sit above it (add).
Why does returning &local fail? ::: On return moves up and the frame's boxes are abandoned; the next call may overwrite them, so the pointer aims at reusable/garbage memory.
In fact(3), which frame returns first? ::: fact(1) — the deepest one — strict LIFO.