What Is the Stack?

Every program needs a place to keep the values it is working on right now: the arguments to the function it is currently running, its local variables, and a note of where to return when that function finishes. On x86-64, that place is the call stack, and almost everything in this chapter revolves around it.

If you have used the word "stack" for the data structure (push something on, pop it off the top), you already have the right mental model. The call stack is exactly that, living in memory, and the CPU has built-in instructions, push and pop, for using it.

A Region of Memory and a Register That Tracks It

The stack is just a contiguous region of memory the operating system hands your program at startup. What makes it a stack rather than a plain blob of memory is one register that always points at its current top:

  • rsp is the stack pointer. It holds the address of the most recently pushed item, the very top of the stack.

When you push a value, the CPU moves rsp and writes the value there. When you pop, it reads the value back and moves rsp the other way. Because rsp is updated automatically, the program always knows where the top is.

It Grows Downward

Here is the detail that trips up everyone at first: on x86-64 the stack grows toward lower addresses. "Pushing" something makes rsp smaller, not larger.

high addresses
   ...            <- where the stack started
   [ older data ]
   [ newer data ]
   [ top of stack ]   <- rsp points here
   ...
low addresses

So when you read assembly and see rsp being decreased, that is the stack growing; when rsp is increased, the stack is shrinking. This is the opposite of what intuition suggests, and getting it backwards is a classic source of confusion. Burn it in now: down = bigger stack, up = smaller stack.

push and pop

These two instructions are the stack in miniature.

  • push rax does two things: it subtracts 8 from rsp (because a 64-bit register is 8 bytes), then stores rax at the new top.
  • pop rax is the reverse: it reads 8 bytes from the top into rax, then adds 8 back to rsp.

You will see push and pop constantly, especially around function calls, because that is how functions save and restore registers and set up their workspace.

Calls Build the Stack Up; Returns Tear It Down

The reason the stack matters so much is that it mirrors the chain of function calls in your program. Each time you call a function, a new region (its stack frame, the subject of the next lesson) is carved out at the top. When that function returns, its region is released and the top moves back. Consider this program, where main calls outer, which calls inner:

#include <iostream>

int inner(int x)
{
    return x + 1;
}

int outer(int y)
{
    return inner(y) * 2;
}

int main()
{
    std::cout << outer(10) << '\n';
    return 0;
}

Compiled at -O0, the two helper functions look like this:

inner(int):
    push rbp
    mov  rbp, rsp
    mov  DWORD PTR -4[rbp], edi
    mov  eax, DWORD PTR -4[rbp]
    add  eax, 1
    pop  rbp
    ret
outer(int):
    push rbp
    mov  rbp, rsp
    sub  rsp, 8
    mov  DWORD PTR -4[rbp], edi
    mov  eax, DWORD PTR -4[rbp]
    mov  edi, eax
    call inner(int)
    add  eax, eax
    leave
    ret

You do not need to understand every line yet; that is what the rest of the chapter is for. But notice the shape of it. When main runs call outer(int), the stack grows to hold outer's frame. While outer is running it executes call inner(int), so the stack grows again to hold inner's frame on top. When inner hits ret, its frame is released and we are back inside outer. When outer returns, its frame is released too. The stack rises and falls exactly in step with the calls.

What call and ret Actually Do to the Stack

call and ret are not magic; they are push and pop in disguise.

  • call inner(int) pushes the return address (the address of the instruction right after the call) onto the stack, then jumps to inner. That pushed address is how inner knows where to come back to.
  • ret pops that address off the stack and jumps to it.

So a call quietly shrinks rsp by 8 to make room for the return address, and ret quietly grows it back by 8. This is the single most important use of the stack: remembering the trail of return addresses so that a deep chain of calls can unwind correctly, one return at a time.

A Quick Word on Recursion and Overflow

Because every call adds a frame, infinite or runaway recursion keeps pushing frames and driving rsp lower and lower until it runs off the end of the region the OS gave you. That is a stack overflow: the program has literally exhausted its stack. Now you know what is physically happening when you see that crash.

Key Takeaways

  • The stack is a region of memory used for function arguments, local variables, and return addresses.
  • rsp is the stack pointer; it always points at the current top of the stack.
  • On x86-64 the stack grows downward: pushing decreases rsp, popping increases it.
  • push subtracts from rsp then stores; pop reads then adds back.
  • call pushes the return address and jumps; ret pops it and jumps back, so the stack rises and falls in step with the call chain.
  • Runaway recursion drives rsp past the end of the region, causing a stack overflow.