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], 10 is int a{10}; — store 10 into a's slot at -4[rbp].
  • mov DWORD PTR -8[rbp], 20 is int b{20}; — store 20 into b's slot at -8[rbp].
  • The next three lines are int c{a + b}; — load a into edx, load b into eax, add them, and store the sum into c's slot at -12[rbp].
  • mov eax, DWORD PTR -12[rbp] is return c; — load c into eax so 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 below rbp, such as -4[rbp].
  • int slots step downward in 4-byte (DWORD) increments: -4, -8, -12, and so on.
  • Negative offsets from rbp are 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 -O0 function has.
  • Locals are kept in memory at -O0 for predictability and debuggability, not speed.
  • At -O2 the optimizer often keeps locals in registers or eliminates them entirely, so the stack slots disappear.