One register, one region, and a deal between them
From the earlier guides in this rung you know the CPU is mostly a small set of fast registers plus a program counter (rip) that says which instruction comes next, all driven by the fetch-decode-execute cycle. That is enough to add numbers and jump around, but it is not enough to call a function and come back. Coming back needs memory — specifically a region called the stack, and one register, the stack pointer (rsp), that always points at its current top. This guide is about the deal those two strike: the region holds the data, and the pointer remembers where the top of the pile is right now.
Before anything else, two words that sound the same must be pulled apart, because confusing them is the classic beginner trap. The stack data structure is the abstract last-in-first-out idea you push onto and pop off of. The stack region (or call stack) is a concrete slab of bytes the operating system handed your program when it started, living up near the high end of its address space. The region happens to be used in a last-in-first-out way, which is why it borrowed the name — but it is real memory at real addresses, not an idea. When this guide says "the stack," it means the region.
Push and pop: two instructions that move one register
The whole machinery rests on two of the common instructions you met earlier: `push` and `pop`. A `push rax` does two tiny things atomically — it subtracts 8 from rsp (making room, because the stack grows down), then writes the 8 bytes of rax to the new address rsp points at. A `pop rcx` does the mirror image — it reads 8 bytes from where rsp points into rcx, then adds 8 to rsp, releasing that slot. Nothing is erased on a pop; the old bytes simply stop being "in use," and the next push will overwrite them. That is the entire contract: rsp marks the boundary between the live stack and the free space below it.
Before: rsp = 0x7fffffffe040
push rax ; rax = 0x11
rsp -= 8 -> 0x7fffffffe038
store 0x11 at [0x7fffffffe038]
high addr
+------------------+ 0x7fffffffe040 (old top, untouched)
| 0x11 | 0x7fffffffe038 <- rsp now points HERE
+------------------+
low addr (free space below, not yet used)
pop rcx ; rcx = 0x11, rsp += 8 -> back to 0x7fffffffe040Notice that the stack is not some special hardware — it is ordinary memory, and `push`/`pop` are just shorthand for an arithmetic-on-rsp plus a memory move you could write out yourself with `mov` and `sub`. The CPU offers these two instructions because the pattern is so common it is worth a single short opcode. Everything fancy that follows — calling functions, saving registers, making room for local variables — is built out of exactly this: adjust rsp, then read or write the bytes it now points at.
call and ret: how a function remembers the way home
Now the central question: when the CPU jumps into a function, how does it ever find its way back to the exact instruction after the call? A plain jump would lose that information — it overwrites rip and the original location is gone. The answer is that `call` is a smarter jump. A `call func` does two things: it pushes the address of the next instruction onto the stack (this saved address is the return address), and only then sets rip to `func`. The breadcrumb is now sitting safely on the stack, at the very top.
Returning is the exact undo. A `ret` instruction pops the top of the stack into rip — it reads the return address back off the stack and jumps there, landing precisely on the instruction after the original `call`. So `call`/`ret` are a matched pair built entirely out of the push/pop idea from the last section: `call` is "push the return address, then jump," and `ret` is "pop into rip." The stack is what makes this work for nested calls too — if `main` calls `f` which calls `g`, two return addresses stack up, newest on top, and each `ret` peels off exactly the right one in reverse order. That last-in-first-out unwinding is why this region is called the call stack.
The stack frame: each function's private scratchpad
A return address alone is not enough — a function also needs somewhere to keep its local variables and a few saved registers. The chunk of stack belonging to one active function call is its stack frame. When a function starts it runs a short prologue that builds the frame, and when it ends it runs an epilogue that tears the frame back down so the stack is exactly as the caller left it. The frame is private to that one call: while `g` is running, its frame sits on top, and `f`'s frame waits frozen just below it.
A classic prologue makes room with two moves. First it may save the previous frame's base pointer with `push rbp`, then copy rsp into rbp with `mov rbp, rsp` so rbp becomes a stable anchor for this frame while rsp keeps moving. Then it carves out space for locals by simply subtracting from rsp, for example `sub rsp, 0x20` to reserve 32 bytes. Local variables then live at fixed offsets from rbp, like `[rbp - 0x4]` for one `int`. The epilogue undoes it: `mov rsp, rbp` then `pop rbp` restores both registers, and `ret` heads home. This is the machine-level shape of every ordinary C function you have ever written.
There is a deep payoff in this picture, and one honest danger. The payoff: because each call gets its own frame carved fresh from the top, every invocation of a function has its own private copy of its locals — that is exactly why recursion and re-entrant code can keep separate state without colliding. The danger: a local array lives in this frame, right next to the saved return address. Writing past the end of that array — an off-by-one, an unchecked copy — can overwrite the return address sitting just above it. That is a buffer overflow, and when the function runs `ret` it will jump to whatever garbage now occupies that slot. It is one of the oldest and most exploited bug classes in all of computing, and it lives or dies right here, at the instruction level.
Walking a real call, step by step
Let us trace one full call in slow motion to make the moving parts concrete. Imagine `main` is about to execute `call f` at some address, and the instruction physically following the call sits at 0x401150. We follow only rsp and rip — the two registers that tell the whole story. Watch how the return address goes onto the stack and comes back off it, and how rsp ends exactly where it began.
- main runs `call f`. The CPU pushes the return address 0x401150 onto the stack (rsp drops by 8), then sets rip to the first instruction of f. Execution is now inside f.
- f's prologue runs: `push rbp` saves main's frame anchor, `mov rbp, rsp` sets f's own anchor, and `sub rsp, 0x10` reserves 16 bytes for f's locals. f's frame now exists on top of the stack.
- f does its work, reading and writing its locals at fixed offsets like `[rbp - 0x8]`. The return address sits safely further up the stack, untouched as long as f stays within its reserved space.
- f's epilogue runs: `mov rsp, rbp` then `pop rbp` discards the locals and restores main's anchor, leaving rsp pointing exactly at the saved return address again.
- f runs `ret`. It pops 0x401150 into rip and rsp rises by 8. Control lands on the instruction right after the original call, with rsp back at its pre-call value. main never noticed the detour.
That trace is the heart of the whole subject, and it leaves two threads dangling that the rest of this rung picks up. First: who owns rsp and rbp across a call, and which registers must a function preserve versus which it may freely clobber? That is the calling convention and the ABI, the shared rulebook every function obeys so independently compiled code can interoperate — guide 4. Second: how do you see any of this in your own program? You read it off a disassembly, the human-readable listing of the actual machine instructions — guide 5. The stack is the stage; conventions are the script; disassembly is finally pulling back the curtain.