call / ret and the stack do their dance| rax | |
| rsp |
sub/add rsp, 28h in main()?0x28 = 40 bytes = 32 bytes of "shadow space" + 8 bytes for alignment.
The Windows x64 ABI requires every caller to reserve 32 bytes of shadow space
(a.k.a. home space) directly above the return address, so the callee can spill its four
register arguments (rcx, rdx, r8, r9) there if it wants to. main() is about to
call func, so it must reserve that space even though func never uses it.
Why the extra 8? Alignment. The ABI says rsp must be 16-byte aligned at the point of a
call. Entering main, the call to main pushed an 8-byte return address, so rsp is at
16n−8. Subtracting 0x28 (16n−8 − 40 = 16m) restores 16-byte alignment before
the next call. The add rsp, 28h in the epilogue is simply the mirror
image, tearing the frame back down.
GCC/Clang at -O0 keep a classic frame pointer: push rbp; mov rbp,rsp
in the prologue and pop rbp in the epilogue — a balanced pair that saves and
restores the caller's rbp.
MSVC here doesn't use rbp as a frame pointer at all (it addresses locals relative to rsp),
and this tiny main() doesn't need to preserve any non-volatile registers. Nothing to save
⇒ nothing to push/pop. The only stack adjustment left is the sub/add rsp, 28h
pair — which is itself "balanced", just done with arithmetic instead of push/pop.