What Is the Stack?
The call stack, the rsp register, push and pop, and how the stack grows downward in memory.
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:
rspis 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 raxdoes two things: it subtracts 8 fromrsp(because a 64-bit register is 8 bytes), then storesraxat the new top.pop raxis the reverse: it reads 8 bytes from the top intorax, then adds 8 back torsp.
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 thecall) onto the stack, then jumps toinner. That pushed address is howinnerknows where to come back to.retpops 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.
rspis 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. pushsubtracts fromrspthen stores;popreads then adds back.callpushes the return address and jumps;retpops it and jumps back, so the stack rises and falls in step with the call chain.- Runaway recursion drives
rsppast the end of the region, causing a stack overflow.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
What Is the Stack? - Quiz
Test your understanding of the lesson.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!