| rax | |
| rsp |
"The stack will always be maintained 16-byte aligned, except within the prolog (for example, after the return address is pushed) …"
sub rsp, 38h — and why is j at [rsp+20h] instead of [rsp]?func2 is the first function we've seen that is both a caller and has a local. Its 0x38 (56) bytes decompose exactly like the annotated course diagram:
0x20 shadow space at [rsp+0..1F] (bottom, for the upcoming
call func3) + 0x10 local chunk at [rsp+20h..2Fh]
(j in the low half) + 0x8 alignment padding on top = 0x38.
The shadow space must sit directly above the return address the call will push —
that's why it claims the bottom of the frame and evicts j up to
[rsp+20h]. In SingleLocalVariable.c, func called nobody, so i got
to live right at [rsp].
func3 — 0x18: leaf with a local → 0x10 local chunk + 0x8 padding. No shadow space (it calls nobody).
func / main — 0x28: caller with no locals → 0x20 shadow space + 0x8 padding.
func2 — 0x38: caller with a local → 0x20 shadow + 0x10 local chunk + 0x8 padding.
The +8 in every case exists because call pushed an 8-byte return address,
leaving rsp at 16n−8; the sub amounts are all ≡ 8 (mod 16), restoring 16-byte alignment —
exactly the ABI rule quoted above.
func3 puts 0x7A11 ("tall") in eax; then func2's, func's, and main's ret/
epilogues never touch eax, so the same value rides all the way out — the process exit code
is 0x7A11.
j = 0x7a1e ("tale") is written to memory and then… never read. A dead store,
kept only because this is an unoptimized build. Its corpse stays at 0014FD90 forever
(well, until someone else's frame reuses that memory).
1) The lecture's disassembly slide shows the stores as 0BEEFh /
7EEFh — copy-paste leftovers from earlier examples. The C source and the stack
diagrams use 0x7a11 / 0x7a1e, so this page follows those.
2) The CRT return address here is 00000001`40001389, not the
…1349 from the earlier examples — more code in the binary shifts the C runtime,
so the address of "the instruction after call main" moves too.
long long locals in one frame. Same machinery, three new tricks to spot before the next lecture:int func() { long long i = 0xf01dab1ef007ba11; long long j = 0x0b57ac1e5; return i+j; } int main() { return func(); }
func: 0000000140001000 sub rsp,18h 0000000140001004 mov rax,0F01DAB1EF007BA11h 000000014000100E mov qword ptr [rsp+8],rax 0000000140001013 mov eax,0B57AC1E5h 0000000140001018 mov qword ptr [rsp],rax 000000014000101C mov rax,qword ptr [rsp] 0000000140001020 mov rcx,qword ptr [rsp+8] 0000000140001025 add rcx,rax 0000000140001028 mov rax,rcx 000000014000102B add rsp,18h 000000014000102F ret main: 0000000140001060 sub rsp,28h 0000000140001064 call func (0140001000h) 0000000140001069 add rsp,28h 000000014000106D ret
sub rsp,18h: 0x10 of locals fits one
16-byte chunk exactly — a single padding row.long long k = 0x57ABBADABAD00;)
→ sub rsp,28h: 24 bytes of locals round up to two 16-byte chunks —
now two padding rows. That's Microsoft's 16-byte allocation granularity at work.sub rsp,18h with
zero wasted local space. i lands at [rsp+8], j at
[rsp] — note memory order flips declaration order.mov qword ptr [mem], imm64.
The big constant must detour through a register: mov rax, imm64, then store rax.j's value fits in 32 bits, so the compiler
uses mov eax, 0B57AC1E5h — the automatic zero-extension fills upper rax for free,
saving instruction bytes versus a full mov rax, imm64.j's slot really reads 00000000`B57AC1E5,
zeros included, exactly as the frame diagram shows.f01dab1e f007ba11 = "foldable football",
0b57ac1e5 = "obstacles", and the third local's
57abba dabad00 ≈ "yabba dabba doo". The sum rax =
F01DAB1F`A5827BF6, and main returns it truncated to int:
eax = A5827BF6.call 0000000140001040 —
a leftover from SingleLocalVariable2 (where func lived at 1040). func is at
…1000 here.