OST2 Arch1001 · first function parameter · 0x11 rides ecx into func() — which immediately writes it back to the stack, above its own frame
RIP = 00000001`40001020— no instruction yet executed
☒executed instruction
♍modified value
⌘start value
RIP▶next instruction
shadow-store row (caller-allocated, callee-owned)
C source
Disassembly (execution starts at main, not at the top!)
⤴ execution continues at 00000001`40001359 — the code that called main()
Registers
rax
rcx
rsp
Stack (8-byte rows — two frames this time)
↑higher addresses
lower addresses (stack grows down)↓
rsp
main's frame = 0x28: the four gray shadow-store slots
(FDE0–FDF8) + 8 alignment bytes. func's frame = 0x18 with i @ FDC0.
a's home slot lives in main's frame: main sees it as
[rsp], func as [rsp+8] then [rsp+20h].
←/→ step · Space play/pause · Home/End jump
Pass1Parameter.c takeaways
Something very interesting is going on with the stack! The value passed in a
register is still stored on the stack — doesn't that kind of defeat the speed benefit
of passing arguments in registers? At /Od, honestly, yes: the compiler
unconditionally homes every register parameter to memory so each variable has a
stable address (which is exactly what a debugger wants). Optimized builds skip the store
when they can — but the space is reserved either way.
func() wrote above its own return address — legally.mov dword ptr [rsp+8], ecx lands in main's frame because the x64
convention makes the caller pre-allocate 0x20 bytes of shadow store that belong to
the callee by contract.
An RE pattern worth memorizing: a run of
mov [rsp+8], ecx / mov [rsp+10h], edx / … at the very top of a
function is the callee homing its parameters — the count tells you the arity, the
registers tell you the order.
After homing, a parameter behaves exactly like the locals from 002–005: plain
rsp-relative displacements, no special instructions. The only novelty is whose frame
the slot is in.
The experiment continues — TooManyParameters.c
One parameter told us "arg1 → ecx, homed at [rsp+8]". To find the full pattern, the course
throws five parameters at the compiler and watches what falls out:
Source — five uint64 parameters, int arithmetic.
#defineuint64unsigned long longintfunc(uint64 a, uint64 b, uint64 c,
uint64 d, uint64 e){
int i = a+b-c+d-e;
return i;
}
intmain(){
returnfunc(0x11,0x22,0x33,0x44,0x55);
}
The stack at line 1031 — the pattern
from our stepper, ×4, plus a fifth argument that never saw a register:
00000000`0014FE08
return address = 00000001`40001399
00000000`0014FE00
16-byte-stack-alignment padding
00000000`0014FDF8
undef (chunk round-up)
00000000`0014FDF0
arg5 = 0x55 (stack-passed)
00000000`0014FDE8
arg4 = r9 = 0x44 (homed)
00000000`0014FDE0
arg3 = r8 = 0x33 (homed)
00000000`0014FDD8
arg2 = edx = 0x22 (homed)
00000000`0014FDD0
arg1 = ecx = 0x11 (homed)
00000000`0014FDC8
return address = 00000001`40001078
00000000`0014FDC0
16-byte-stack-alignment padding
00000000`0014FDB8
16-byte-stack-alignment padding
00000000`0014FDB0
i = undef`ffffffef
Disassembly — the four red-boxed homing stores are the
"say hello to the shadow store" moment:
A pattern emerges: args 1–4 go in rcx, rdx, r8, r9 and get homed into
the caller's shadow store; arg5 has no register — main stores 55h
straight to [rsp+20h], the first slot past the 0x20 shadow area,
before the call.
After func's sub rsp, 18h, all five params line up as plain displacements:
a @ +20h, b @ +28h, c @ +30h, d @ +38h,
e @ +40h — register-passed or stack-passed, they're indistinguishable in the
body.
main's frame math: shadow 0x20 + 8 for arg5 = 0x28 → rounded up to 0x30 in 16-byte
chunks → + 8 alignment = sub rsp, 38h (that's the extra
undef row at FDF8).
The arithmetic: 0x11 + 0x22 − 0x33 + 0x44 − 0x55 = −0x11 →
i = FFFFFFEF. (Also note this build homes and reads only the low dwords —
r9d, not r9 — everything downstream is int math.)
"The x64 Application Binary Interface (ABI) uses a four-register
fast-call calling convention by default. Space is allocated on the call stack as a
shadow store for callees to save those registers."
"The caller must always allocate sufficient space to store four register
parameters, even if the callee doesn't take that many parameters."
"Any parameters beyond the first four must be stored on the stack
after the shadow store, before the call."
And from the takeaway slide: "The callee has the responsibility
of dumping the register parameters into their shadow space if needed" — the
compiler reserves this space even if no function parameters are passed to another function.
The whole contract, as one table (offsets are from the callee's rsp at entry,
i.e. [rsp] = return address):
parameter
integer / pointer
float / double
home / slot
who writes it
arg1
rcx
xmm0
[rsp+8]
callee (homing, if needed)
arg2
rdx
xmm1
[rsp+10h]
callee (homing, if needed)
arg3
r8
xmm2
[rsp+18h]
callee (homing, if needed)
arg4
r9
xmm3
[rsp+20h]
callee (homing, if needed)
arg5, arg6, …
no register — stack only
[rsp+28h], [rsp+30h], …
caller, before the call
return value
rax
xmm0
—
callee
64-bit calling conventions — MS "x64" vs. System V "x86-64"
Two specs rule the planet. Everything above was the left one; gdb on Linux speaks the right
one. The full documents (they really do have everything):
The TooManyParameters signature again — colors follow the argument:
func(…)
a = 0x11
b = 0x22
c = 0x33
d = 0x44
e = 0x55
MS "x64" 4 register args
RCX
RDX
R8
R9
stack [rsp+20h]
System V 6 register args
RDI
RSI
RDX
RCX
R8
Read the columns and mind the trap:
RCX = arg1 on Windows but arg4 on Linux; RDX = arg2 vs. arg3. Same register,
different meaning — know the ABI before trusting a register. Spill-over args go on the stack,
left-most at the lowest address ("pushed", though really mov, as we saw).
Who saves what — the full team sheet (hover a column)
VS caller-saved ("volatile")
RAX
RCX①
RDX②
R8③
R9④
R10–11
VS callee-saved ("non-volatile")
RBX
RSI
RDI
RBP
R12–15
System V caller-saved (GCC et al.)
RAX
RCX④
RDX③
RSI②
RDI①
R8⑤
R9⑥
R10–11
System V callee-saved
RBX
RBP
R12–15
caller-saved / "volatile" — trashed across callscallee-saved / "non-volatile" — survive callsRSI & RDI — the only two that switch teams① argument order
Caller-save vs. callee-save — who owns the registers
Caller-save — MS: "volatile"
assume they WILL be changed by the callee · the registers "belong" to the callee
assume they will NOT be changed · the registers "belong" to the caller
prologue: push rbx…
body — use them freely
epilogue: pop rbx…
ret
VS: RBX·RBP·RDI·RSI·R12–R15 GCC: RBX·RBP·R12–R15
Balance:
every save is matched by a restore — the caller brackets the call site (save right
before, restore right after), the callee brackets the whole function body (save in the
prologue, restore in the epilogue). Anything pushed must get popped.
Stick it:
System V arg order — "Diane's Silk Dress Costs $8 9" →
RDI · RSI · RDX · RCX · R8 · R9
MS arg order — alphabet, then digits: C · D · 8 · 9 →
RCX · RDX · R8 · R9
And the pink pair: on Windows RSI/RDI are precious (callee-saved) — on Linux they're
the first two arguments, burned on every call.
STACK FRAME TIME OUT — 001's mystery 0x28, finally solved
Back in CallASubroutine1, main() had no
locals and passed no arguments, yet did sub rsp, 28h — and we had no
explanation. Now we do. That frame was pure obligation: 0x20 of shadow store that
main must allocate for any call ("even if the callee doesn't take that many
parameters" — func took zero), plus 8 bytes of 16-byte-alignment padding.
CallASubroutine1's stack, re-labeled with what we know
today:
00000000`0014FE08
return address = 00000001`40001349
00000000`0014FE00
16-byte-stack-alignment padding
00000000`0014FDF8
r9 shadow store (never written)
00000000`0014FDF0
r8 shadow store (never written)
00000000`0014FDE8
rdx shadow store (never written)
00000000`0014FDE0
rcx shadow store (never written)
00000000`0014FDD8
return address = 00000001`40001019
00000000`0014FDD0
undef
00000000`0014FDC8
undef
main's 0x28 = 4 × 8 shadow slots + 8 alignment. Nothing else. main never
touches a byte of it.
func never even makes a stack frame — no locals, no parameters to home, no
calls of its own → no sub rsp at all. Just
mov eax, 0BEEFh / ret.
So the recipe for reading any MSVC x64 frame:
sub rsp, N where N = locals (16-byte chunks) + out-going args area
(0x20 shadow + 8·(extra args), 16-byte rounded) + 8 if needed for alignment.
"Who knows what evil lies in the hearts stacks of men compilers?"