JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

The Stack and the Stack Frame

Every function call you have ever written quietly carves out a little slab of memory, uses it, and throws it away. This guide opens up that slab — the stack frame — and shows how a whole tower of them grows and shrinks as your program calls and returns.

Where do local variables actually live?

By now you can take an address with `&x`, follow a pointer with `*p`, and you know an `int x` inside a function is a real box sitting somewhere in the address space. But where, exactly? When you write a function with three local variables and call it a thousand times, are there a thousand copies fighting over the same memory? The answer is the subject of this guide, and it is one of the most elegant ideas in how a computer runs your code: every call gets its own fresh slab of memory, automatically, and gives it back the instant the call returns.

To see how, remember that your program's memory is not one undivided blob — it is carved into regions with different jobs. Your machine code sits in one region. Global and `static` variables sit in another, fixed in place for the whole run. Then there are the two famous growing regions: the heap, which you ask for by hand (we meet it properly in the next rung), and the stack, which the machinery manages for you. Local variables — the ordinary ones you declare inside a function — live on the stack.

The word "stack" is doing real work here, so hold the picture: a stack is a pile where you only ever add to the top and remove from the top — last in, first out, like a stack of plates. That single rule is exactly what function calls need. When you call a function, you push a new slab on top; when it returns, you pop that slab off. Because calls always return in the reverse order they were made (the function you called most recently is the first to finish), the plate-stack discipline fits function calls perfectly, with nothing left over.

One call, one frame

Each plate on that pile has a name: a stack frame (also called an activation record). A stack frame is the private slab of memory belonging to one in-progress call of one function. It holds that call's local variables, space to save a few values across nested calls, and — crucially — the return address: the place in the calling function to jump back to when this call finishes. One call, one frame; ten thousand calls over a program's life, ten thousand frames born and destroyed, but only the currently-active ones exist at any instant.

This is the clean answer to the puzzle we opened with. Calling a function a thousand times does not make a thousand copies coexist — each call builds its frame, uses it, and tears it down before the next begins. And calling a function recursively is no longer mysterious: in recursion, a function that calls itself simply gets a brand-new frame for each level of the recursion, each with its own copy of the locals. Ten levels deep means ten frames stacked up at once, each remembering its own values and its own place to return to. The same code, ten independent slabs.

How the machine grows and shrinks the stack

Underneath the tidy plate-stack picture is one register doing the real bookkeeping: the stack pointer, called rsp on x86-64. It always points at the current top of the stack — the edge of the most recent frame. On every common machine the stack grows downward: pushing a new frame means rsp decreases to a lower address, and popping means rsp increases back up. That downward-growth feels backwards, but it is just a convention chosen so the stack can grow toward the heap from the opposite end of the address space, leaving them maximum room between.

So the actual mechanism of "making a frame" is humble: subtract from rsp to reserve some bytes, and you have just allocated all of this call's locals in one stroke. There is no search, no list, no asking the operating system — just one arithmetic instruction. That is why stack allocation is essentially free compared to the heap, and it is the heart of the stack-versus-heap tradeoff: the stack is blindingly fast and fully automatic, but its lifetimes are rigidly tied to call-and-return, whereas the heap is slower and manual but lets a value outlive the function that made it.

  high addresses
  +---------------------+  <- bottom of stack (oldest frame)
  |  frame: main()      |     locals of main, return addr into _start
  +---------------------+
  |  frame: compute()   |     main called compute()
  +---------------------+
  |  frame: helper()    |  <- compute called helper(); newest frame
  |    return address   |     where to jump back into compute()
  |    saved rbp        |     caller's frame base
  |    int n; char buf[8]     this call's locals
  +---------------------+  <- rsp points HERE (top of stack)
  low addresses              the stack grew DOWNWARD to get here
Three live frames while helper() runs; rsp marks the top, and the stack grew downward from main() to helper().

Walking through a single call

Let us trace what happens when `compute()` calls `helper()`, in slow motion. You will not write these steps yourself — the compiler-generated prologue and epilogue do them at the start and end of every function — but seeing them once makes the whole edifice click. The matched pair of frame-setup and frame-teardown is what guarantees the stack is left exactly as it was found, so the caller never even knows a frame came and went.

  1. The caller puts the arguments where the calling convention says (the first few in registers on x86-64, the rest pushed onto the stack), so helper() can find them.
  2. The call instruction pushes the return address — the address of the very next instruction in compute() — onto the stack, then jumps to helper()'s code.
  3. helper()'s prologue saves the caller's frame base (rbp), sets up its own, and subtracts from rsp to carve room for n and buf. The new frame now exists.
  4. helper() runs, reading and writing only inside its own frame and the arguments it was handed.
  5. The epilogue undoes the prologue: it restores rsp and rbp, discarding the frame in one move, so the locals are gone.
  6. The ret instruction pops the saved return address and jumps to it, landing back in compute() exactly where it left off — with the stack restored to precisely its earlier state.

Notice how much trust this scheme places in those few saved values. The return address and the saved frame base are ordinary bytes sitting in the frame, right next to your local arrays. If a program writes past the end of a local `char buf[8]` — a stack-resident buffer overflow — it can clobber the saved return address sitting just beyond it, so that when `ret` runs, the CPU jumps to an address the attacker chose. That is the classic stack-smashing attack, and it is one honest reason C earns its reputation for sharp edges. The mechanism that makes calls fast also leaves the controls within arm's reach of a stray write.

The limits, honestly

The stack is wonderful but finite. The operating system gives each thread a fixed-size region for its stack — commonly around 8 MiB on Linux by default, smaller elsewhere — and that is all it gets. Push too many frames and you run off the end: a stack overflow. The usual culprit is runaway recursion (a missing base case so the function calls itself forever), but a single enormous local can do it too — declare `char buf[16 * 1024 * 1024]` and you have asked one frame for more than the whole stack holds. When the stack pointer crosses its guard boundary, the program is killed, typically with a segmentation fault.

That last distinction is worth slowing down on, because it trips nearly everyone. The call stack is a region of memory the hardware and OS manage, growing downward, holding frames. The stack as a data structure is an abstract last-in-first-out container you might build yourself out of an array or a heap block. They share a name because the call stack behaves like that data structure, but they are not the same object — confusing the two is exactly the kind of layer-muddle that makes systems programming feel slippery. Keep them crisp: one is where your function calls live; the other is a thing you might code up on a Tuesday.