Every call on x86-64 changes the stack pointer by 8 bytes and then requires the stack pointer to be in a particular state. That is the whole of the frame. There is no frame descriptor, no link register, no callee-saved stack pointer, and no implicit “push the arguments” pass — all of which is exactly the machinery the 32-bit x86 conventions had, and all of which a reader who learned calling conventions on another architecture will be looking for and not finding.
Two rules live in those 8 bytes, and both of them are narrower than the slogans usually attached to them.
call and a tail jmp want OPPOSITE adjustments. Not a subtlety: the same jmp with the canonical subq $8, %rsp in front of it is not a misaligned tail call, it is a dead process.And the reason for the alignment rule is not the one most people carry. It is not a penalty. The vector course's width concept measured unaligned accesses on this machine and found them free, so a rule that exists for a penalty cannot be the rule on this silicon. The reason is a fault, and it is three instructions to demonstrate.
Start from the only fact. A call decrements %rsp by 8 and writes the return address at the new %rsp. Everything in this concept is a consequence of that one behaviour, and the consequence you must hold on to is:
That is why the idiom you see in every prologue looks the way it does. A function with a frame needs a 16-byte-aligned outgoing argument area, and the area is at %rsp, so it must move %rsp by a multiple of 16 — and the canonical sequence for an 8-byte outgoing argument is the one line that looks like a mistake:
subq $8, %rsp # the ABI's own idiom, for EIGHT bytes
movq %rdi, (%rsp) # of outgoing argument area
call some_function
addq $8, %rsp
Eight bytes of outgoing area requires eight bytes of stack, and eight bytes of stack is not 16-byte aligned unless the %rsp was 8 (mod 16) beforehand — which it is, because that is where a function's %rsp sits. The subq $8, %rsp is not padding. It is the difference between the callee's first instruction seeing a 16-byte-aligned address and not seeing one.
And here is the measurement, which is the only way to know that any of this matters:
$ ./abidump | sed -n '/THE ALIGNMENT FAULT/,/^ THIS IS NOT/p'
%rsp mod 16 at a callee's first instruction, four ways in:
a `call`, %rsp 16-aligned at the call (the rule) 8
a `call`, the `subq $8,%rsp` deleted 0
a TAIL `jmp`, no adjustment at all 8
a tail `jmp` after `subq $8,%rsp` 0
and that adjustment, left in place, kills the process: KILLED by a signal, because the return address is not where `ret` looks for it
one callee, three arms. It is EXACTLY the prologue gcc -O0 emits:
`pushq %rbp; movq %rsp,%rbp; subq $32,%rsp` and a 16-byte
vector local at -16(%rbp), which is 16-byte aligned iff %rbp is,
and %rbp is the CALLER's %rsp minus 8.
movaps 16-byte store, CONFORMING call -> returned, and the value read back was 0x3ff8000000000000
movaps 16-byte store, VIOLATING call -> KILLED by a signal
signal 11, and 11 is SIGSEGV
movups 16-byte store, VIOLATING call -> returned, and the value read back was 0x3ff8000000000000
Three arms, one callee, and the callee is not a contrivance: it is the prologue gcc -O0 emits, with a 16-byte vector local at -16(%rbp). Read the chain: %rbp is the caller's %rsp minus 8, so %rbp is 16-byte aligned exactly when the caller's %rsp was, so -16(%rbp) is 16-byte aligned exactly when the caller kept the rule. Delete one subq $8, %rsp in the caller and the callee dies on a legal instruction.
And the third arm is the one that makes it a fault rather than a penalty. movups is the same store, at the same address, through the same misaligned call, and it returns having read back the value that was stored. The difference between the two arms is one letter in a mnemonic and the difference between the two outcomes is a signal.
The red zone is the 128 bytes immediately below %rsp, which a leaf function may use as locals without adjusting %rsp at all. A function with no calls therefore needs no frame, and the 128 bytes are the reason: they are already there.
What the 128 bytes are for is a separate question, and the answer is in the specification rather than in the measurement: they exist so that a leaf can be interrupted by a signal handler that uses the stack without the leaf having had to build a frame. That is a kernel-behaviour claim and this course cannot test it, so the artifact says so in its limits block rather than paraphrasing the specification as though it had measured it. What can be measured is the restriction that comes with the 128 bytes, and the restriction is not about tidiness.
$ ./abidump | sed -n '/THE RED ZONE, MEASURED/,/^ That is the whole/p'
a leaf that writes 8 bytes at -8(%rsp) and 8 at -128(%rsp) and
never touches %rsp at all. The sum below is 0 only if BOTH
words came back unchanged, so it is the checksum and the result
in one number.
leaf, no call, 2 slots written every slot intact: YES -- both survived
the same leaf, plus ONE `call` every slot intact: NO -- at least one destroyed
the same, plus a FRAMED callee every slot intact: NO -- and this arm writes FOUR slots, not two
A leaf that uses the red zone and never calls keeps both words. Add onecall and it loses them. That is not a slow path, it is not a compiler optimisation, and it is not “undefined behaviour” in the C sense — the assembly is perfectly well-defined and does exactly what it says. It is a violation of the convention, and the hardware helps you find out which convention you violated:
$ ./abidump | sed -n '/AND THE MECHANISM/,/writes them/p' \
| sed 's/0x0000[0-9a-f]\{12\}/<per-run address>/'
AND THE MECHANISM, PROVED RATHER THAN ASSERTED. The leaf stores a
magic word at its own -8(%rsp) and then calls a function that
reports two numbers: its own %rsp, and 0(%rsp).
the callee saw %rsp = <per-run address>
the callee's 0(%rsp) = <per-run address>
The callee reports the same address the first concept read the return RIP from. The word that replaced the leaf's variable is the callee's own return address, and the leaf's variable and the callee's return-address slot are the same eight bytes of memory.
So the leaf restriction is arithmetic about where a push lands, and the 128 is just how much the architecture gives you before the next thing you do stops being yours. A useful way to hold it: the red zone is only yours until your next call, and call is one of the eight bytes.
One more thing this concept owes the reference, because it is the piece people expect and do not get. %rbp is a callee-saved general register, and that is all it is on x86-64. There is nothing in the hardware that requires a function to establish a frame, and push %rbp; mov %rsp,%rbp is a gcc convention for making the frame walkable, not a requirement. That gcc can and does use %rbp as an ordinary ninth general register at -O2 is direct evidence, and you can see it in the corpus this course audits: mx at -O2 moves its four integer arguments into %r13, %r12, %rbp and %rbx — and pushes all four of those registers to do it, because every one of the four is callee-saved and the values sitting in them belong to its caller.
Why the convention exists is the subject of the unwinding concept: a frame pointer is a way of making the frame addressable by arithmetic instead of by a table, and a table is what a profiler wants and arithmetic is what a debugger on a leaf with no description gets.
The four-way table at the top of this concept is the part worth re-reading, because the two bottom rows are a trap that a great deal of documentation walks into. The claim that “the stack must be 16-byte aligned at every control transfer” is false, and it is false in the most expensive way available: not slower, but dead.
$ ./abidump | sed -n '/THE RULE IS TWO RULES/,/^$/p' THE RULE IS TWO RULES AND NOT ONE. A `call` pushes 8, so the caller's %rsp must be 16-ALIGNED at the call. A tail `jmp` pushes nothing, so the jmpping function's %rsp must be 8 mod 16, which for a function entered at 8 mod 16 means NO ADJUSTMENT AT ALL. A PLT stub is `endbr64; jmp *GOT(%rip)` and nothing else, and it is right for the same reason.
Both rules are about the same thing — the callee's %rsp is 8 (mod 16) at its first instruction — and they differ because a call pushes 8 and a jmp does not. The proof that the second rule is real and not a nicety is the best evidence in this section, because it is already in every dynamically linked program on the machine:
$ objdump -d --no-show-raw-insn -M intel /usr/lib/x86_64-linux-gnu/libc.so.6 \
| sed -n '/<free@plt>:/,/^$/p'
00000000000283d0 <free@plt>:
283d0: endbr64
283d4: jmp QWORD PTR [rip+0x1e9a2e] # 211e08 <free@@GLIBC_2.2.5+0x15caa8>
283da: nop WORD PTR [rax+rax*1+0x0]
No subq $8, %rsp. No frame. No push. Two instructions, and the second one is the whole of the stub. If the canonical alignment adjustment were required before a tail transfer, every PLT entry in every shared library would be misaligned by 8 bytes, and the first 16-byte vector store in memcpy would fault on every call from every program. It does not, because the stub is right.
What the movaps arm then shows is the reason the rule about callis real, and it is worth being precise about what kind of evidence a fault is. It is not a faster path and a slower path. It is a process that exists and a process that does not, and the difference is one letter of one mnemonic in the callee. That is a much stronger kind of evidence than a ratio, and it is also the only kind available here: there is no performance counter on this machine to tell you what the misaligned store would have cost, so the honest statement is the one about the fault and the misaligned-but-not-faulting case is in the limits block.
abis.S, remove the subq $8, %rsp from abi_call_conforming and rename it. (Expect the movaps arm to change from “returned” to “KILLED by a signal” while the movups arm does not move at all. That is the whole argument for the rule in one edit, and it takes ten seconds.)abi_tail_adjusted to be reported as killed rather than as misaligned, and read the reason the artifact gives: the adjustment does not merely shift the alignment, it puts 8 bytes between the callee and the return address its ret will pop. The failure mode of the wrong tail-call fix is a jump into the middle of the stack, not a slow call.)-8(%rsp) through an inline asm, then calls printf. (Expect the local to come back as a code address, or as something that looks like one. Then move the local to -128(%rsp) and confirm that the other end of the red zone is the one that survives a no-frame callee and dies against a framed one — and read the psABI's note that the area is reserved from the 128th byte down, which is the reason a green zone exists at all.)-O2. (Expect the difference to be small: two instructions of prologue and two of epilogue, and no sub at all in the red-zone version. The interesting part is what a compiler does not do, and the fact that this course does not measure the resulting speed-up — the red zone's cost is a plausible story and a plausible story is not a number, so the artifact declines to make one and says so.)%rsp is not 16-aligned. (Expect to find several, and then expect to spend the next hour discovering that your tracker loses track of %rsp at pushfq, at enter, and at any write it does not model. That is not a failure of diligence; it is the seventh time in this collection that a checker which cannot account for a mismatch is not evidence of a mismatch, and the last concept's retraction R6 is about exactly this experiment.)Backwards, the calling convention is where stack=0x10 came from: the seventh argument sits 8 bytes past the return address, and this concept is about what that address has to be aligned to. The memory course's translation concept is the reason a misaligned 16-byte store can fault at all — a page is 4 KiB and a 16-byte-aligned object never straddles one, and the fault is a page fault rather than an alignment check. The privilege course's canonical-address concept is the one place a similar-sounding word means something quite different, and the two are worth keeping apart.
Forwards, the red zone is the reason the callee-saved set is the size it is, and that is the next concept's opening move: a caller cannot keep anything live in a caller-saved register across a call, so anything it wants to keep has to go somewhere that survives, and on x86-64 the places that survive are six registers and the stack. The saved-register concept opens with that argument and then checks it by experiment rather than by table. And the %rbp paragraph at the end of the reality section is the forward reference to unwinding: a frame pointer exists so a frame can be walked by arithmetic, and that matters exactly when there is no table.
Outward, one neighbour owns the why of the frame's contents and this course deliberately does not re-teach it. The security course's stack-protector concept is where a canary is placed between the return address and the locals and why a smash is detected rather than exploited. That is a property of a hardening tool layered on the frame, and this course owes the exhaustive x86-64 reference for the frame itself. Reading both is the cheapest way to stop confusing the two: the canary is a convention adopted by compilers, and the alignment rule is a convention adopted by the hardware's instruction set.