Stack Frames & Local Variables
Prologue, epilogue, and rbp
- › Explain what a stack frame is and what the prologue and epilogue do, instruction by instruction
- › Derive a local variable's address from rbp/rsp instead of memorizing offsets
- › Draw the layout of a frame: saved rbp, return address, and locals
- › See how a buffer among the locals sits below the return address — the overflow setup
Each function gets a drawer
You've seen the pieces: the stack (day 11), call/ret and the return address (day 12), the ABI (day 13). Now assemble them. When a function runs, it carves out a stack frame — its own private slice of the stack holding its local variables and bookkeeping. The prologue builds the frame on entry; the epilogue tears it down on exit. Understanding the frame's layout is the last piece of the low-level foundation — and the exact map an attacker reads.
The prologue, line by line
You saw push rbp; mov rbp, rsp back on day 7 and I said "trust it for now." Now, to the bottom:
1func:2 push rbp ; (1) save the CALLER's frame-pointer onto the stack3 mov rbp, rsp ; (2) rbp now marks the top of THIS frame (a fixed anchor)4 sub rsp, 32 ; (3) reserve 32 bytes below for local variables- (1) push rbp — the previous function was using rbp to anchor *its* frame, so we save it before
- reusing rbp. (This is why rbp is callee-saved.)
- (2) mov rbp, rsp — plant rbp at the current top. From now on rbp is a *fixed* reference point
- for this whole function, even as rsp moves around. Locals get addressed relative to rbp.
- (3) sub rsp, 32 — remember from day 11 that subtracting from rsp reserves space. Those 32 bytes
- between the new rsp and rbp are this function's locals.
The frame's layout — draw it once, know it forever
After the prologue, the stack around this frame looks like this (addresses increase upward):
higher addresses
┌───────────────────────┐
│ return address │ <- [rbp+8] (pushed by 'call', day 12)
├───────────────────────┤
│ saved rbp (caller's) │ <- [rbp] (pushed by prologue step 1)
├───────────────────────┤
│ local var A │ <- [rbp-8]
│ local var B │ <- [rbp-16]
│ buffer[16] ... │ <- [rbp-32] .. [rbp-17]
└───────────────────────┘ <- rsp (top)
lower addresses[rbp-8], [rbp-16]…) — you
derive each address from the frame, you never memorize "the offset is 8." (2) The saved rbp is
at [rbp] and the return address is just above it at `[rbp+8]`. So a local buffer sits *below* the
return address in memory, and the stack grows down while a buffer is written *upward*. Write past the
end of that buffer and you march straight into the saved rbp and then the return address. That
geometry — buffer below, return address above — is the stack overflow, and you're seeing exactly why
it works.The epilogue undoes it
Exit is the mirror image:
1 mov rsp, rbp ; discard locals: put rsp back at the frame top2 pop rbp ; restore the caller's saved rbp3 ret ; pop the return address into rip -> back to caller4 ; the pair 'mov rsp,rbp; pop rbp' is exactly what the single 'leave' doesleave is shorthand for mov rsp, rbp; pop rbp. So a compact epilogue is just leave; ret. The
frame is dismantled in reverse order of how the prologue built it: give back the locals, restore the
old frame pointer, return. Perfectly nested, thanks to the stack.
push rbp; mov rbp, rsp; sub rsp, 48. It has a char buf[32] starting at
[rbp-48] and an int len at [rbp-4]. Roughly how many bytes past the *start* of buf would a
write have to reach to begin overwriting the return address?p $rbp, p $rsp, and x/8gx $rsp. You'll see
the locals, the saved rbp at [rbp], and the return address at [rbp+8]. Computing a local's address as
rbp minus its offset, then confirming with x, turns this diagram into something you can see.Map a real frame
Write frame.c with a function that has two local ints and a small char array, does something trivial with them, and is called from main. Compile at -O0 and disassemble. Identify the prologue (push rbp; mov rbp,rsp; sub rsp,N), compute each local's address as an offset from rbp, and in gdb verify with `p $rbp` and `x/`. Draw the frame: locals, saved rbp at [rbp], return address at [rbp+8]. Then find the epilogue (leave/ret). Only the frame facts from this lesson are needed.
$ gcc -O0 -g frame.c -o frame$ objdump -d -M intel frame$ gdb ./frame # break after prologue; p $rbp; x/8gx $rsp- › You derived each local's address from rbp (not guessed)
- › You located the saved rbp at [rbp] and the return address at [rbp+8]
- › Your drawing shows the char array sitting BELOW the return address
- › You found the leave/ret (or mov rsp,rbp; pop rbp; ret) epilogue