OST2 Arch1001 · same C as 006, now GCC 9.3 / Ubuntu 20.04 / -O0 · SysV ABI · Intel syntax throughout — enter push rbp
RIP = 00005555`5555513f— no instruction yet executed
☒executed instruction
♍modified value
⌘start value
RIP▶next instruction
rbp⟶frame pointer (new!)
red-zone row (below rsp, yours anyway)
C source (identical to 006 — only the compiler changed)
Disassembly (objdump -M intel — execution starts at main)
⤴ execution continues at 00007fff`f7d90d90 — __libc_start_call_main hands eax to exit(); echo $? → 17
Registers (rbp joins the table — that's the lesson)
rax
rdi
rsp
rbp
Stack (8-byte rows — watch BOTH arrows)
↑higher addresses
lower addresses (stack grows down)↓
rsp
rbp
main's frame = one slot: its saved rbp. func's frame = saved rbp +
red-zone usage: i @ rbp−4 = E1FC
(high half of the E1F8 row), homed a @ rbp−14h = E1EC (high half of E1E8).
No sub rsp anywhere — the same C needed 0x28 + 0x18 of explicit frame in 006.
←/→ step · Space play/pause · Home/End jump
Pass1Parameter-on-Linux takeaways
Same C, completely different skeleton. Gone: sub rsp / add rsp, the
shadow store, rsp-relative everything. Instead: push rbp ·
mov rbp, rsp · body · pop rbp · ret, with every
variable addressed as [rbp−disp]. That two-instruction prologue is the
highlighted pair in the course's Hello.c slide.
Why push rbp at all? Because rbp is callee-saved in both ABIs — any function
that wants to use it as a frame pointer must first preserve the caller's copy. The push is
the save; the pop rbp in the epilogue is the matching restore. It has nothing
to do with parameter passing.
Two sanctioned trespasses, one per ABI. In 006 the MS callee wrote up, above
its return address, into the caller's 0x20-byte shadow store. Today the SysV leaf
callee wrote down, below rsp, into its 128-byte red zone. Neither is a bug —
recognizing them is half of reading a listing.
The RE pattern: see push rbp / mov rbp, rsp → you're in rbp-world:
[rbp−X] = locals & homed params, [rbp] = saved rbp,
[rbp+8] = return address, [rbp+10h] and up = stack-passed args
(7th onward in SysV). A run of mov [rbp−X], edi/esi/edx… at the top is SysV
parameter homing — count = arity, registers = order.
endbr64 is noise. Intel CET landing pads — Ubuntu compiles everything with
-fcf-protection, so every function entry gets one. Computationally a no-op;
filter them out mentally.
The full caller-/callee-saved team sheet (both ABIs, hover-able) lives in
006 — everything there still applies here.
The same function, two prologues
func() from Pass1Parameter.c, compiled by both planets. Red boxes = parameter homing,
blue boxes = frame management:
MSVC x64 /Od (006) — no frame pointer, rsp does everything:
func:1129 endbr64 ; CET pad112dpush rbp; save caller's anchor112emov rbp, rsp; drop ours1131mov dword ptr [rbp-14h], edi; home a ↓ red zone1134 mov eax, dword ptr [rbp-14h]
1137 mov dword ptr [rbp-4], eax ; i113a mov eax, dword ptr [rbp-4]
113dpop rbp; restore anchor113e ret
Anchor: MSVC measures everything from rsp (which moves exactly once, in the
prologue); GCC -O0 freezes rbp at frame-birth and measures from it. Same memory,
different ruler.
Offsets change sign: MSVC's are positive from rsp; GCC's locals are negative
from rbp, with the return address and any stack args at positive rbp offsets.
Where a gets homed: MSVC writes upward into the caller's shadow store
([rsp+8] = above the return address); GCC writes downward into its own
red zone ([rbp−14h] = below rsp). SysV has no shadow store; Windows has no red
zone — each ABI's magic area is the other's illegal move.
Arg register:ecx vs edi — arg1 differs per ABI. Return
value: eax on both, the one thing everyone agrees on.
Epilogue: MSVC undoes its arithmetic (add rsp, 18h); GCC just
pop rbp — rsp never moved, so there's nothing to add back. When rsp has
moved (non-leaf functions), GCC emits leave ≡
mov rsp, rbp + pop rbp — you'll meet it constantly.
The saved-rbp chain — a linked list of stack frames
Look at what the prologue pair builds, call after call: each frame's [rbp] slot
holds the address of the previous frame's saved-rbp slot. A singly-linked list,
threaded through the stack:
00007fff`ffffe218
ret addr → libc ← [rbp]+8 hop 2
00007fff`ffffe210
saved rbp = e250 (libc's) ── chain end
00007fff`ffffe208
ret addr → main ← [rbp]+8 hop 1
00007fff`ffffe200
saved rbp = e210 ──→ points one row up² ← rbp
² not literally one row — it points at
wherever the previous frame parked its rbp.
Two loads per frame walk the whole stack: [rbp] → caller's frame,
[rbp+8] → where you were called from. That is a backtrace — gdb's
bt can do exactly this on -O0 code.
The price: one register out of 16 permanently occupied, plus
push/mov/pop on every call. That's why optimized code on both OSes drops
the frame pointer…
…and still backtraces fine, via metadata instead: Windows keeps static
UNWIND_INFO tables in .pdata; Linux keeps DWARF CFI in
.eh_frame. Moral for RE: never trust an rbp chain in a release
binary — rbp is usually just another scratch variable there.
"When Does VS Use a Frame Pointer?"
Six steppers of MSVC output — 001 through 006, all at /Od — and rbp never once appeared.
That's the default answer: it doesn't. Fixed-size frame → pure rsp-relative
addressing, frame pointer omitted. The docs (the slide's two references) give the
exceptions:
A frame pointer becomes mandatory when the frame's size isn't a compile-time
constant — alloca() / dynamic stack allocation, or dynamic stack alignment
beyond 16 bytes. rsp wanders unpredictably then, so something stable must anchor the
fixed part of the frame.
When one is used, it doesn't have to be rbp — any nonvolatile register qualifies;
the prolog example in the docs establishes R13 as the frame pointer.
And it's typically parked mid-frame (e.g. lea r13, [rsp+80h] after the
allocation), not at a saved-rbp slot — that way one-byte displacements (−80h…+7Fh) reach
twice as much frame. No Linux-style saved-rbp chain is ever built; x64 Windows unwinds via
UNWIND_INFO tables instead.
One more doc line that explains a 006-vs-007 difference you just watched:
"All memory beyond the current address of RSP is considered
volatile: the OS, or a debugger, may overwrite this memory during a user debug session, or an
interrupt handler."
Windows has no red zone. That's why 006's func had to sub rsp, 18h for a
single int, while today's GCC func — same C! — allocated nothing and lived below rsp
rent-free.
The course's SysV maximum diagram (main → foo → bar), rebuilt next to the Microsoft one from
the stack-usage doc. "(if any)" everywhere — a real frame uses only the rows it needs:
SysV / Linux — every frame the same shape, saved rbp on top:
"return address" from main() to whoever called main()
main: "saved rbp"
main: local variables (if any)
main: callee-save regs (if any)
main: caller-save regs (if any, around the call)
main: stack args for foo — 7th onward (if any)
"return address" foo() to main()
— foo() frame: same shape —
"return address" bar() to foo()
bar: "saved rbp" ← rbp
bar: locals · callee-save regs ← rsp
red zone: rsp−1 … rsp−128 (ours, by contract)
Microsoft x64 — note the optional saved rbp and the
shadow store replacing the red zone:
caller: stack args — 5th onward (if any)
caller: shadow store — 4 × 8, always
"return address" into the caller
saved rbp — optional! only with alloca & co.
callee-save regs (if any)
local variables (if any)
alloca space (if any — this is what forces rbp)
outgoing: shadow store + args for the next call ← rsp
red zone — DOES NOT EXIST: "beyond rsp is volatile"
Structural differences, in one breath: SysV homes into its own frame (or red
zone); MS homes into the caller's shadow store. SysV spills at arg 7; MS at arg 5.
SysV saves rbp at every -O0 frame; MS almost never.
Both diagrams agree on the constants: stack grows down, call pushes
8 bytes, rsp is 16-byte aligned at every call — which is why misalignment crashes in
library code (movaps!) are the classic hand-written-asm bug on both OSes.
The "As seen in Hello.c" slide — translated to Intel
The course showed it in AT&T (objdump's default). Same bytes, our syntax:
0000000000001149<main>:1149 endbr64
114dpush rbp; the pair, again114emov rbp, rsp1151 lea rdi, [rip+0xeac] ; arg1 = &"Hello World!"1158 call 1050 <puts@plt>115d mov eax, 1234h ; return 0x12341162pop rbp1163 ret
1164 nop word ptr cs:[rax+rax] ; padding, never runs116e xchg ax, ax ; more padding
lea rdi, [rip+0xeac] — the string's address lands in rdi:
SysV arg1, exactly like our edi carried 0x11. RIP-relative addressing =
position-independent code; the string sits 0xeac bytes past the next instruction.
No sub rsp — main has no locals. And alignment works out by itself:
main entered with rsp ≡ 8 (mod 16); push rbp → ≡ 0; so the
call puts happens perfectly 16-aligned. The mandatory rbp save doubles as
re-alignment.
mov eax, 1234h → main returns 0x1234. But exit codes keep only the low
byte: $? = 0x34 = 52. (Ours returns 0x11 → $? = 17.)
AT&T → Intel decoder ring (for watching the slides)
AT&T (the slides)
Intel (these pages)
rule
push %rbp
push rbp
% sigils vanish
mov %rsp, %rbp
mov rbp, rsp
operand order flips — AT&T is src, dst
mov $0x1234, %eax
mov eax, 1234h
$ marks immediates
lea 0xeac(%rip), %rdi
lea rdi, [rip+0xeac]
disp(base) → [base+disp]
movl / movq / retq
mov dword ptr … / mov … / ret
size lives in a suffix, not "dword ptr"
The mov %rsp,%rbp ↔
mov rbp, rsp flip is the one that bites: same three bytes
(48 89 e5), opposite reading direction. When in doubt, remember what the
prologue must do — copy rsp into rbp.
32-bit — where the frame pointer was born
Everything above is the modern era. In 32-bit x86 (cdecl, both Windows and Linux),
there are no register arguments at all — every parameter is pushed onto the
stack, right-to-left — and the ebp frame isn't a debug-build quirk, it's the culture:
func:push ebp; the same dance, 1985 editionmov ebp, esp
sub esp, 4 ; room for i
mov eax, [ebp+8] ; a — args live ABOVE ebp
mov [ebp-4], eax ; i = a
mov eax, [ebp-4] ; return imov esp, ebp; ≡ leavepop ebp
ret
main:
push ebp
mov ebp, esp
push 11h; the argument — pushed, no register!
call func
add esp, 4; cdecl: CALLER cleans up
pop ebp
ret
The universal 32-bit compass — burn this in; every ebp frame
on earth reads the same way:
[ebp+0Ch]
arg2 (if any)
[ebp+8]
arg1 = a = 0x11
[ebp+4]
return address
[ebp]
saved ebp ← ebp
[ebp−4]
local i ← esp
+8 first arg · +4 return · 0 saved ebp · − locals. This offset map made the
frame pointer king: with args and locals both measured from one anchor, debuggers
and humans could read any frame cold.
With only 8 registers, burning ebp hurt — hence MSVC's famous /Oy frame-pointer
omission optimization, the historical seed of x64's "no rbp by default".
No shadow store, no red zone, esp only 4-byte aligned (16 on modern Linux). Simpler
world, slower calls: every argument = a memory write.
The 32-bit convention zoo (spot them in the epilogue)
convention
arguments
who cleans the stack
typical sighting
cdecl
all on stack, right→left
caller: add esp, N
default C · varargs (printf)
stdcall
all on stack, right→left
callee: ret N
Win32 API (MessageBoxA)
fastcall (MS)
ecx, edx, rest on stack
callee: ret N
scattered Win32 internals
thiscall
ecx = this, rest on stack
callee: ret N
MSVC C++ member functions
SysV i386
all on stack, right→left
caller: add esp, N
32-bit Linux — cdecl in a trench coat
Epilogue tell: ret 8 → callee-cleaning
(stdcall family, and the 8 = argument bytes → arity!); bare ret +
add esp, N back at the call site → cdecl.
The whole map — four ABIs, one table
MS x64
SysV x86-64
Win32 x86 (cdecl)
Linux i386
integer args
rcx, rdx, r8, r9
rdi, rsi, rdx, rcx, r8, r9
stack only
stack only
float args
xmm0–3
xmm0–7
stack
stack
overflow args
stack, past the 0x20 shadow
stack, from arg 7
all of them
all of them
stack cleanup
caller
caller
caller (stdcall: callee)
caller
return value
rax
rax (rdx:rax for 128-bit)
eax (edx:eax)
eax (edx:eax)
frame pointer
only for alloca / dynamic frames
-O0: always · optimized: omitted
customary (/Oy omits)
customary at -O0
magic zone
shadow store: 0x20 above the ret addr
red zone: 0x80 below rsp
none
none
rsp/esp at call
16-byte aligned
16-byte aligned
4
16 (modern GCC)
prologue pair → push rbp · mov rbp, rspepilogue → leave (or pop rbp) · retrbp = callee-saved in every x86 ABI — that's why it's pushed
Stick it:
Prologue chant — push rbp · mov rbp, rsp: "save the old anchor, drop a new
one." Epilogue: pop rbp (or leave) · ret: "weigh anchor, sail home."
Geography — Windows callers pay it forward: 0x20 of shadow above the return address.
Linux callees live on credit: 0x80 of red zone below rsp.
-O0 compass — [rbp+8] whence you came · [rbp] the caller's anchor ·
[rbp−X] what you own.
"Who knows what evil lies beneath the stack pointer?"
Windows: interrupts, debuggers, the OS — "all memory beyond RSP is volatile."
Linux: nothing — 128 bytes held clear in your name, by contract.
MIND THE RED ZONE
References — the "When Does VS Use a Frame Pointer?" slide