Local Variables on the Stack
How local variables are stored at fixed offsets from the frame pointer.
Local Variables on the Stack
You now know what a stack frame is and how rbp anchors it. This lesson puts the two together to answer a concrete question: when you write int a{10}; inside a function, where does a actually live? At -O0 the answer is almost always the same: in a slot on the stack, addressed as a fixed offset below rbp.
Three Locals, Three Slots
Here is a function with three local variables and nothing else:
#include <iostream>
int locals()
{
int a{10};
int b{20};
int c{a + b};
return c;
}
int main()
{
std::cout << locals() << '\n';
return 0;
}
Compiled at -O0:
locals():
push rbp
mov rbp, rsp
mov DWORD PTR -4[rbp], 10
mov DWORD PTR -8[rbp], 20
mov edx, DWORD PTR -4[rbp]
mov eax, DWORD PTR -8[rbp]
add eax, edx
mov DWORD PTR -12[rbp], eax
mov eax, DWORD PTR -12[rbp]
pop rbp
ret
After the prologue, read the body as a direct translation of the C++:
mov DWORD PTR -4[rbp], 10isint a{10};— store 10 intoa's slot at-4[rbp].mov DWORD PTR -8[rbp], 20isint b{20};— store 20 intob's slot at-8[rbp].- The next three lines are
int c{a + b};— loadaintoedx, loadbintoeax, add them, and store the sum intoc's slot at-12[rbp]. mov eax, DWORD PTR -12[rbp]isreturn c;— loadcintoeaxso it becomes the return value.
Each local got its own slot, and each slot is just a fixed offset from rbp. There is no magic to a "variable" at this level: it is a named region of the stack frame.
Reading the Offsets
Notice the slots step downward in 4-byte increments: -4, -8, -12. Each int is 4 bytes (a DWORD), so the compiler packs them one after another below rbp. The negative sign is the giveaway that we are looking at the current frame's locals: addresses below rbp (negative offsets) are this function's own storage, while addresses above rbp (positive offsets, which you will see less often) reach toward the caller, such as the saved rbp and the return address.
A useful habit when reading a listing: scan for the distinct [rbp-N] offsets. Each unique offset is, roughly, one local variable (or a spilled argument). Here we see -4, -8, and -12: three offsets, three int locals. That count won't always be exact once the compiler reuses slots, but at -O0 it is a reliable first read.
Locals and Spilled Arguments Share the Frame
Arguments and locals live in the same frame, just at different offsets. When a function both takes arguments and declares locals, the compiler often places the spilled arguments at the more-negative offsets and the locals nearer to rbp, or vice versa; the exact layout is the compiler's choice. The important point is that, at -O0, both arguments and locals end up as [rbp-N] slots, and you read them the same way. You saw this in earlier lessons: a function spilled its edi/esi arguments into slots and also stored its locals into slots, all relative to the one fixed rbp.
Why the Stack and Not Registers?
If registers are faster, why does unoptimized code insist on shoving every local into memory? Two reasons. First, there are only a handful of registers, far fewer than a function might have variables, so the stack is the general-purpose home that always has room. Second, -O0 deliberately keeps each variable in a single, predictable memory location so that a debugger can find it and you can inspect it at any breakpoint. The store-then-reload pattern that looks so wasteful is the price of that predictability.
The optimizer pays no such price. With -O2, locals() evaporates entirely:
locals():
mov eax, 30
ret
Every local, every store, every reload, gone. The compiler saw that a, b, and c were just constants combining to 30, computed the answer at compile time, and returned it directly. The stack slots existed only to make the unoptimized code literal and debuggable; once correctness no longer depends on them, they vanish. This is the single most important contrast in reading C++ assembly: the same source produces verbose, stack-heavy code at -O0 and razor-thin register code at -O2.
What to Take to the Exercise
When you do this lesson's exercise, you will write a function with a couple of locals, run it to check the output, and then read the assembly panel. Look for the [rbp-N] slots: count the distinct offsets, match each one to a variable in your source, and watch the values being stored into and loaded out of them. Then flip to -O2 if you can and see how many of those slots survive. Seeing your own variables become stack offsets is the moment this lesson clicks.
Key Takeaways
- At
-O0, each local variable lives in a stack slot addressed as a fixed offset belowrbp, such as-4[rbp]. intslots step downward in 4-byte (DWORD) increments:-4,-8,-12, and so on.- Negative offsets from
rbpare the current frame's locals and spilled arguments; positive offsets reach toward the caller. - Counting the distinct
[rbp-N]offsets is a quick way to estimate how many locals (and spilled arguments) a-O0function has. - Locals are kept in memory at
-O0for predictability and debuggability, not speed. - At
-O2the optimizer often keeps locals in registers or eliminates them entirely, so the stack slots disappear.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Local Variables on the Stack - Quiz
Test your understanding of the lesson.
Practice Exercises
Find Your Locals on the Stack
Complete rectangleArea so it uses two local variables, area and perimeter, and returns their sum. The program prints the result for a 3x4 rectangle and a 5x5 rectangle. Once your output is correct, switch the assembly panel to -O0 and find the two stack slots your locals live in, addressed as offsets below rbp such as -4[rbp] and -8[rbp].
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!