Pass1Parameter.c — take two, on Linux

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

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: 0140001000 mov dword ptr [rsp+8], ecx ; home a ↑ shadow store 0140001004 sub rsp, 18h ; explicit frame 0140001008 mov eax, dword ptr [rsp+20h] 014000100C mov dword ptr [rsp], eax ; i 014000100F mov eax, dword ptr [rsp] 0140001012 add rsp, 18h ; frame release 0140001016 ret
GCC 9.3 -O0 (this page) — rbp anchors, rsp never moves:
func: 1129 endbr64 ; CET pad 112d push rbp ; save caller's anchor 112e mov rbp, rsp ; drop ours 1131 mov dword ptr [rbp-14h], edi ; home a ↓ red zone 1134 mov eax, dword ptr [rbp-14h] 1137 mov dword ptr [rbp-4], eax ; i 113a mov eax, dword ptr [rbp-4] 113d pop rbp ; restore anchor 113e ret

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:

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.

[1] x64 stack usage
the MS maximum-stack diagram · "optional frame pointer"
[2] x64 prolog and epilog
legal prologue/epilogue forms · the R13 frame-pointer example
System V x86-64 psABI
§3.2.2 — the red zone's birth certificate

Two "maximum stack" diagrams — SysV vs Microsoft

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"

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 114d push rbp ; the pair, again 114e mov rbp, rsp 1151 lea rdi, [rip+0xeac] ; arg1 = &"Hello World!" 1158 call 1050 <puts@plt> 115d mov eax, 1234h ; return 0x1234 1162 pop rbp 1163 ret 1164 nop word ptr cs:[rax+rax] ; padding, never runs 116e 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 %rbppush rbp% sigils vanish
mov %rsp, %rbpmov rbp, rspoperand order flips — AT&T is src, dst
mov $0x1234, %eaxmov eax, 1234h$ marks immediates
lea 0xeac(%rip), %rdilea rdi, [rip+0xeac]disp(base) → [base+disp]
movl / movq / retqmov dword ptr … / mov … / retsize 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 edition mov 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 i mov esp, ebp ; ≡ leave pop 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)

conventionargumentswho cleans the stacktypical sighting
cdeclall on stack, right→leftcaller: add esp, Ndefault C · varargs (printf)
stdcallall on stack, right→leftcallee: ret NWin32 API (MessageBoxA)
fastcall (MS)ecx, edx, rest on stackcallee: ret Nscattered Win32 internals
thiscallecx = this, rest on stackcallee: ret NMSVC C++ member functions
SysV i386all on stack, right→leftcaller: add esp, N32-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 x64SysV x86-64Win32 x86 (cdecl)Linux i386
integer argsrcx, rdx, r8, r9rdi, rsi, rdx, rcx, r8, r9stack onlystack only
float argsxmm0–3xmm0–7stackstack
overflow argsstack, past the 0x20 shadowstack, from arg 7all of themall of them
stack cleanupcallercallercaller (stdcall: callee)caller
return valueraxrax (rdx:rax for 128-bit)eax (edx:eax)eax (edx:eax)
frame pointeronly for alloca / dynamic frames-O0: always · optimized: omittedcustomary (/Oy omits)customary at -O0
magic zoneshadow store: 0x20 above the ret addrred zone: 0x80 below rspnonenone
rsp/esp at call16-byte aligned16-byte aligned416 (modern GCC)
prologue pair → push rbp · mov rbp, rsp epilogue → leave (or pop rbp) · ret rbp = 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