When reading assembly-level code for any of the AArch32 or AArch64 instruction sets, you may have noticed that the stack pointer has various alignment and usage restrictions. These restrictions are part of the procedure-call standard – the set of common rules that allow functions to call one another. However, some of the rules also apply even if you aren't actually handling function calls. The stack is shared between parts of an application, any libraries that it uses as well as signal handlers, so it is important that these components agree on how the stack should behave.
If you're just writing C code, the compiler will sort this all out for you, but you'll need to understand the rules if you're dealing with any assembly code that needs to interact with the stack.
This article assumes that your platform uses ARM's AAPCS (for AArch32) or AAPCS64 (for AArch64). This is the case on Linux and Android, but other systems may define their own standards.
For both AArch32 and AArch64:
sp
stack_limit < sp <= stack_base
stack_limit
stack_base
For AArch32 (ARM or Thumb), sp must be at least 4-byte aligned at all times. As long as you only push and pop whole registers, this restriction will never be broken.
For AArch64, sp must be 16-byte aligned whenever it is used to access memory. This is enforced by AArch64 hardware.
push
pop
C compilers will typically reserve stack space at the start of the function, then leave sp alone until the end, so the restriction is not as awkward as it first seems. However, you must be aware of it when handling assembly code, and it can be tricky for simple compilers (such as stack-based JIT compilers).
Note that unlike AArch32, arbitrarily-aligned values can be stored in sp, as long as the previously-described rules are followed for memory accesses and public interfaces. This is useful for allocating variable-length arrays of small values, for example:
// Allocate a variable-length array of bytes on the stack. sub sp, sp, x0 // x0 holds the length. and sp, sp, #0xfffffffffffffff0 // Align sp.
The alignment-check-on-memory-access means that AArch64 cannot have general-purpose push- or pop-like operations.
For example:
// Broken AArch64 implementation of `push {x1}; push {x0};`. str x1, [sp, #-8]! // This works, but leaves `sp` with 8-byte alignment ... str x0, [sp, #-8]! // ... so the second `str` will fail.
In this particular case, the stores could be combined:
// AArch64 implementation of `push {x0, x1}`. stp x0, x1, [sp, #-16]!
However, in a simple compiler, it is not always easy to combine instructions in that way.
If you're handling w registers, the problem will be even more apparent: these have to be pushed in sets of four to maintain stack pointer alignment, and since this isn't possible in a single instruction, the code can become difficult to follow. This is what VIXL generates, for example:
w
// AArch64 implementation of `push {w0, w1, w2, w3}`. stp w0, w1, [sp, #-16]! // Allocate four words and store w0 and w1 at the lower addresses. stp w2, w3, [sp, #8] // Store w2 and w3 at the upper addresses.
If you're dealing with hand-written AArch64 assembly code, you'll have to be aware of these patterns.
Many JIT compilers have a tricky situation, though: such compilers are built around a simple stack machine, and expect to be able to push and pop in an ad-hoc fashion. Managing this on AArch64 requires an inventive approach, and I'll describe a few possibilities in a follow-up article.
1Some time ago I was told that the 8-byte alignment restriction exists to allow the use of instructions such as ldrexd and strexd, which require an 8-byte-aligned address. Without a guarantee that a function will be entered with proper alignment, these instructions would be awkward to use on stack variables. There may also be other reasons, but I don't know what they are, and AAPCS doesn't document them.
ldrexd
strexd
I have been asked: What happens if while in the privileged mode we turn off the hardware stack pointer checking and the SP becomes misaligned? In a similar vein: Why would you ever turn off stack alignment checking?