| rax | |
| rcx | |
| rsp |
Packed layout: foo.a @ rsp+0 · foo.b[i] @ rsp+2+4i ·
foo.c @ rsp+1Ah. Nothing is 4- or 8-byte aligned, so every value spills
across row boundaries.
a, b, c got shuffled to a, c, b. Wrap the same three in a
struct and the C standard pins them: a, then b, then c,
at increasing offsets. Structs are a contract about layout; loose locals are not.Upside down, the declaration reads c → b → a top-to-bottom — exactly the order the stack diagram shows them.
Draw the struct by MS' documented alignment & padding rules and you get the
left diagram. But the disassembly stores c at [rsp+1Ah] and
bases b at +2 — this build used 1-byte structure packing
(#pragma pack(1) / /Zp1), so the real layout is the right diagram:
no padding anywhere, nothing aligned, values straddling the 4-byte rows.
sizeof = 0x28.sizeof = 0x22 (2 + 24 + 8). Members split across rows.b at +4 and c at +20h; this binary uses +2 and +1Ah. Odd/unrounded
struct offsets in disassembly ⇒ somebody set #pragma pack.mov qword ptr [rsp+1Ah], rax writes 8 bytes at an
address ≡ 2 (mod 8), spanning three of our 4-byte rows. x86 shrugs and does it
(slower, possibly split across cache lines, no atomicity guarantee) — plenty of other CPUs
would raise an alignment fault instead.From the x64 software conventions doc: scalar types align to their own size; each member is placed at the next multiple of min(its alignment, the packing level); the whole struct's size is rounded up to a multiple of its strictest member's alignment.
| member | size | natural align | offset @ /Zp8 (default) | offset @ /Zp1 (this binary) |
|---|---|---|---|---|
| short a | 2 | 2 | 0x00 | 0x00 |
| int b[6] | 24 | 4 (per element) | 0x04 (2 pad bytes) | 0x02 |
| long long c | 8 | 8 | 0x20 (4 pad bytes) | 0x1A |
| sizeof(mystruct_t) | 0x28 (40) | 0x22 (34) | ||
Frame-size coincidence: 0x28 and 0x22 both round up to 0x30 in
16-byte chunks, + 8 alignment = sub rsp, 38h either way — you can't tell the
packing from the prologue, only from the member displacements.