The x86-64 frame concept has a surprise in it, and if you have read that course you already know the shape of this one. On x86-64 a function can put a local in the 128 bytes below the stack pointer without moving the stack pointer at all, because the System V ABI promises that an interrupt will not touch those bytes. The region is called the red zone. A leaf function with three locals on x86-64 is 8 instructions and does not move %rsp once.
AArch64 has no such promise, and the standard does not merely omit it — it says the opposite, in the same section that states the alignment rule:
AAPCS64 5.2.2.1 Universal stack constraints, at all times
during the execution of a thread T:
- No thread is permitted to access (for reading or for
writing) the inactive region of S.
and, two bullets later, the alignment rule that every
other concept in this course depends on:
- SP mod 16 = 0. The stack must be quad-word aligned.
... where the inactive region of T's stack is the area of
memory denoted by the half-open interval [T.limit, T.SP).
Accessing it is forbidden, not merely unwise — and the sentence is in the same list as the alignment rule, which is the section that defines what a stack pointer owes the rest of the system. So there is no region of memory a function may use without first telling the world it is using it, and the way you tell the world is by moving the stack pointer.
That is not a small inconvenience and it is not a style preference. It is the reason the prologue of essentially every AArch64 function that has locals is two instructions longer than the x86-64 equivalent, and it is measurable in a corpus, at four optimisation levels, with no clock involved. This concept is the measurement.
Think of the stack pointer as a published fact. Other code can read it, and an interrupt handler will use it to find your locals. AArch64's rule is: everything at or above SP is yours and is described; everything below SP belongs to somebody else. So to get memory you must move SP down, which is a publication, and to give it back you must move SP up, which is a retraction of the publication.
And the prologue has to be a linked list, because the AAPCS64 mandates it in §5.2.3:
AAPCS64 5.2.3 Conforming code shall construct a linked list of
stack-frames. Each frame shall link to the frame of its
caller by means of a frame record of two 64-bit values
on the stack (independent of the data model). The frame
record for the innermost frame (belonging to the most
recent routine invocation) shall be pointed to by the
frame pointer register (FP). The lowest addressed
double-word shall point to the previous frame record and
the highest addressed double-word shall contain the value
passed in LR on entry to the current function.
... and in the register table, 5.2.3's own FP:
| r30 | LR | The Link Register.
| r29 | FP | The Frame Pointer
| r19...r28 | | Callee-saved
Three claims in one sentence, and each one is a design decision rather than a convenience. The frame is a linked list, so it can be walked by arithmetic instead of being searched. The record is two 64-bit values, ordered — the lowest address holds the previous frame record and the highest holds the return address from LR — so the order of a stp is not arbitrary. And the standard names the register: r29 is FP and r30 is LR, which is why the prologue below can be written without a comment and a reader can look up whether the compiler kept the promise. Here is the whole of it, in the disassembly of one function:
$ llvm-objdump-21 --triple=aarch64 -d abi_O0.o | sed -n '/<nonleaf>/,/ret/p'
sub sp, sp, #48 — the request
stp x29, x30, [sp, #32] — the frame record, two 64-bit values
add x29, sp, #32 — and the link to the caller's frame
stur x0, [x29, #-8]
str x1, [sp, #16]
...
ldp x29, x30, [sp, #32] — the frame record read back
add sp, sp, #48 — the retraction
ret
Read the four marked lines as four obligations, and notice that all four are required and none of them is an optimisation decision:
48 is a multiple of 16 because of concept 1. It is also a multiple of 8, which is what x29 needs to be 8-aligned, and a multiple of 16, which is what the q registers need.x29 and x30, together, in one instruction. This is the AAPCS64's frame record and it is not optional: a function that does not save it cannot be walked.x29 at it.add x29, sp, #32 — x29 is the frame pointer, and this instruction is what makes the frame a record rather than a region.ldp x29, x30, [sp, #32] then add sp, sp, #48. Note the order: the frame record must be read before the retraction, because it lives in the region being retracted.And the part that has no analogue on x86-64: add x29, sp, #32 points into the middle of the frame, not at the top of it. On x86-64 the frame pointer is set to the frame's base and the saved return address is at 0(%rbp) and everything else is at negative offsets. Here the frame record is at +32 inside a 48-byte frame and the locals are at negative offsets from it. Both conventions work; they put the number 32 and the sign the other way round, which is why a reader porting a stack-walking habit between them gets subtly wrong answers for a long time.
The frame census is the count of who asked. Twenty-five functions, four optimisation levels, and the same C compiled for both targets. A pre-indexed [sp, #-N]! counts as an allocation, because it is one — a count of sub sp, sp, #N alone misses every function that fused the allocation into a save.
$ python3 a64abi.py --run | sed -n '/O0 25 of 25/,/Os 14 of 25/p'
O0 25 of 25 functions allocate a frame at all.
O1 14 of 25 functions allocate a frame at all.
O2 14 of 25 functions allocate a frame at all.
Os 14 of 25 functions allocate a frame at all.
And the same number for the same C on x86-64, with the set difference, because a count of two overlapping sets is the least informative thing a reader can be handed:
level a64: with a frame x86: with a frame a64 mean x86 mean
O0 25 of 25 7 of 25 63 bytes 67 bytes
O1 14 of 25 5 of 25 55 bytes 56 bytes
O2 14 of 25 5 of 25 55 bytes 56 bytes
Os 14 of 25 5 of 25 55 bytes 54 bytes
O2 a64 only: big_frame cd9 inner leaf_spill leaf_vec many
nonleaf use_ret use_retd
x86 only: (none) both: 5
“x86 only: (none)” is the finding. There is no function in this corpus that x86-64 frames and AArch64 does not. At -O0 the counts are 25 against 7, and at every level above it 14 against 5 — and the nine extra are exactly the ones whose C asks for memory that does not fit in a register: a volatile local, a volatile array, a value that must survive a call. On x86-64 those go into the 128 bytes below SP and cost nothing at all. Here they cost a sub and an add.
Two rows in the artifact's per-function table are worth reading, because they explain why the -O0 number is the surprising one rather than the -O2 number:
leaf_reg at -O2 is four instructions with no sub sp at all. A leaf that needs no memory frames nothing, on either architecture. The red zone is irrelevant to it because it never wanted anything.leaf_spill at -O2 is a LEAF that has to allocate, because it has three volatile locals and there is nowhere else for them to go. On x86-64 the same function at the same level is ten instructions and allocates NOTHING. This is the entire concept in one pair of rows: a leaf with locals is free on one architecture and costs two instructions on the other, and the difference is a region of memory 128 bytes wide that one ABI promises and the other forbids.Hand-translated to be the same C, so the difference is the architecture and not clang's mood:
AArch64 a64_leaf_noframe 3 instructions, 0 touch SP
mul x8, x1, x0 ; madd x0, x0, x8, x8 ; ret
AArch64 a64_leaf_frame 11 instructions, 8 touch SP
sub sp, sp, #0x10 <-- the request
str x0, [sp, #0x8]
str x1, [sp]
ldr x8, [sp, #0x8]
ldr x9, [sp]
add x8, x8, x9
ldr x0, [sp, #0x8]
ldr x9, [sp]
add x0, x8, x9
add sp, sp, #0x10 <-- the retraction
ret
x86-64 x86_leaf_noframe 3 instructions, 0 touch SP
imulq %rsi, %rax ; addq %rdi, %rax ; retq
x86-64 x86_leaf_frame 8 instructions, 6 touch SP
movq %rdi, -0x8(%rsp) <-- -8(%rsp) is in the red zone
movq %rsi, -0x10(%rsp) <-- -16(%rsp) is too
movq -0x8(%rsp), %rax
addq -0x10(%rsp), %rax
movq -0x8(%rsp), %rcx
addq -0x10(%rsp), %rcx
addq %rcx, %rax
retq <-- %rsp NEVER MOVES
Three instructions with no frame, on both. Eight on x86-64 with a frame and no %rsp movement. Eleven on AArch64 with a frame and a sub and an add. That is the whole cost of having no red zone, and it is two instructions in every function that has locals — which is every function that is not a leaf of pure arithmetic.
Note the six x86-64 instructions that “touch the stack” and the fact that none of them moves the stack pointer. The count of instructions that touch the stack and the count that allocate a frame are different questions, and this corpus contains a row that answers both: AArch64 11 instructions with 8 touching the stack, x86-64 8 instructions with 6 touching it, and the 2-instruction gap is exactly the pair of sp movements.
This is the trap that produced a wrong number in the first version of this course's own census, so it gets its own table. Five forms, and four of them do two jobs in one instruction:
$ python3 a64abi.py --run | sed -n '/THE ONE FORM THAT HIDES/,/misses all four/p'
form reads and writes allocates
stp x29, x30, [sp, #-16]! two registers, and SP 0xa9bf7bfd
str x0, [sp, #-16]! one register, and SP 0xf81f0fe0
ldr x0, [sp], #16 one register, and SP 0xf84107e0
ldp x29, x30, [sp], #16 two registers, and SP 0xa8c17bfd
sub sp, sp, #16 nothing but SP 0xd10043ff
[MEASURED-ON-BYTES] Four of those five are ONE instruction that does
two jobs, and a frame counter that only counts `sub sp` misses all
four. This is x86-64's `leave` in a different costume: x86 needs a
separate `movq %rsp, %rbp; popq %rbp` to undo the frame, and AArch64
folds the deallocation into the last load.
The ! is the pre-index form: writeback, so the address is computed, the access happens, and SP is updated. The [sp], #16 with no ! is the post-index form: the access happens at the current SP, then SP is updated. Between them they cover the entire lifecycle of a frame in four instructions — allocate, save, load-and-retract, pop.
Read the first row carefully, because it is the canonical AArch64 prologue and it looks like a stack-allocated pair: stp x29, x30, [sp, #-16]! is 0xa9bf7bfd, and it does three things at once — allocate 16 bytes, save the old frame pointer, and save the return address. A reader counting “does this function allocate a frame” by looking for sub sp finds nothing here, and a reader counting “how many registers does this function save” finds two and does not notice the allocation.
The mistake this course actually made is worth printing because it is the ordinary kind. The first version of the frame census counted sub sp, sp, #N and add sp, sp, #N only, and reported nine functions with no deallocation at -O2. Three of those nine were deallocating by folding the addition into the last load — ldr x0, [sp], #16. So the table said the compiler had leaked nine frames, which is a spectacular claim to make about a compiler, and the reason it is worth printing is that nine is exactly the number of functions that take a frame at all. When a census produces a number equal to another number in the same file, stop and check the census.
Everything on this page is a count, not a duration. There is no AArch64 machine on the build host, no emulator and no linker, so not one instruction in this course has been run. The frame census is arithmetic over instructions a compiler emitted; the hand-written comparison is a disassembly of code nobody executed. Whether a stp/ldp pair is cheaper than two strs, and whether a real sub sp is cheaper than a fused one, are questions this course cannot answer and does not pretend to.The x86-64 ABI course measured what it could and named the rest; the same discipline, one architecture over, produces a page that has a ret in it and no cycle.
-O2 for aarch64-linux-gnu and look for sp in every instruction, not just sub sp. (Expect the common prologue to be stp x29, x30, [sp, #-N]! — allocate, save the record, in one instruction with a writeback — and the common epilogue to be ldp x29, x30, [sp], #N followed by ret, where the deallocation is inside the load. Then compile the same file for x86-64 and check whether %rsp moves in a leaf with a volatile local. It does not.)volatile local and count the difference yourself.(Expect two extra instructions and a frame on AArch64, zero extra on x86-64. Then make the local non-volatile and watch the AArch64 frame disappear as well — which tells you the frame is a consequence of the register pressure, not of the syntax, and that a red zone would have absorbed the same pressure for free.)sub sp, sp, #32 but never stores x29/x30. (Expect: the function works, and nothing at all happens — no diagnostic, no crash, no warning. Then read it with gdb and with a backtrace and notice that the two disagree, and that the disagreement starts at your frame. That is the whole argument for AAPCS64 §5.2.3 in one experiment, and it is the subject of the last concept.)Backwards, one concept and one tool. Concept 2 is where 31-as-sp was measured, and it is the number this page moves: every sub sp and add sp in this concept is an instruction whose Rd field means the stack pointer rather than a register. And the 16-byte frame size is not a preference — it is concept 1's rule, and this page is where the two meet: a frame size that is not a multiple of 16 is not a smaller frame, it is an ABI violation in the prologue of a function that was called correctly.
Sideways, and this is the sharpest contrast in the whole section: The x86-64 frame has the red zone, measured by clobbering it. That course could deliver a SIGSEGV; this one has no machine to fault on. The stack canary is a value in this frame, and the reason it needs a frame at all is the reason this page exists — on x86-64 a canary could in principle have lived in the red zone. The dynamic-linking course is where the stack is first set up, and the frames you have been reading about are stacked on top of a region it owns.
Forwards. Concept 4 is the other half of the frame's contents: who has to save which registers, and the two counts of zero that are also rules. Concept 5 is what a frame looks like to something that was not there when it ran — and it is the concept where the ratio between the instructions and their description is the measurement, so read this page's four marked prologue lines and then read that page's two columns and notice they are the same four lines.