roadmap
week 2 · day 14

Stack Frames & Local Variables

Prologue, epilogue, and rbp

Easy 34 min 180 xp
After this you can
  • 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:

a typical prologueasm
1func:
2 push rbp ; (1) save the CALLER's frame-pointer onto the stack
3 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):

one stack frame
  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
Key idea
Two facts to carry: (1) locals live at *negative* offsets from rbp ([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:

the epilogueasm
1 mov rsp, rbp ; discard locals: put rsp back at the frame top
2 pop rbp ; restore the caller's saved rbp
3 ret ; pop the return address into rip -> back to caller
4 ; the pair 'mov rsp,rbp; pop rbp' is exactly what the single 'leave' does

leave 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.

Predict first
A function's prologue is 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?
Note
In gdb, break after a function's prologue and run 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.
finished reading?
Your task, you write the code

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.

deliverable: frame.c (+ a drawing of its stack frame with offsets)
build & run
$ gcc -O0 -g frame.c -o frame
$ objdump -d -M intel frame
$ gdb ./frame # break after prologue; p $rbp; x/8gx $rsp
self-review before running
  • 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
stretchMake the char array large and, in your drawing, mark exactly how many bytes of overflow from the array's start would reach the return address. You've just computed an overflow offset from first principles — the same number you'd need for a real exploit.

Self-check

01What does `mov rbp, rsp` in the prologue accomplish?
02Where do a function's local variables live relative to rbp?
03In the frame, the return address is at:
04Why does a char buffer among the locals set up a stack overflow?
0/4 correct · 0/4 checked