Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Assembly (x86-64, AArch64, RISC-V)  Module 7 – Functions, the stack and calling conventions

Stack alignment and the crashes it causes

The 16-byte stack rule of System V, Microsoft x64, AAPCS64 and RISC-V at every call, where a misaligned stack faults, and how to keep it aligned.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • State the stack-alignment rule at a call under System V, Microsoft x64, AAPCS64 and RISC-V
  • Diagnose a crash caused by a misaligned stack from the faulting instruction and the value of sp
  • Explain why a misaligned x86-64 stack faults only in instructions that need aligned memory
  • Count the pushes and stack adjustments that keep a hand-written function aligned at its calls
  • Explain how a function realigns the stack for a local that needs 32-byte alignment

Before you start

What you’ll learn · Before you start

What you will learn

  • State the stack-alignment rule at a call under System V, Microsoft x64, AAPCS64 and RISC-V
  • Diagnose a crash caused by a misaligned stack from the faulting instruction and the value of sp
  • Explain why a misaligned x86-64 stack faults only in instructions that need aligned memory
  • Count the pushes and stack adjustments that keep a hand-written function aligned at its calls
  • Explain how a function realigns the stack for a local that needs 32-byte alignment

Before you start

On this page

call_misaligned lowers sp by 8 bytes before it calls show, where call_aligned lowers it by 16, and no instruction in call_misaligned objects. On Apple silicon the program then dies inside show, a correct one-line C function, at its second instruction, with a signal: A notification the operating system delivers to a process, such as SIGFPE after an integer divide fault; unhandled, it ends the process, and the shell reports status 128 plus the signal number. More in “Division: traps, defined results, remainders” whose code means “invalid address alignment”.

Every application binary interface (ABI): The rules that let separately compiled machine code work together on one platform, from the size of each C type and the calling convention to the format of object files. More in “Calling conventions: arguments and returns” fixes where the stack pointer: The register that addresses the top of the stack: rsp on x86-64, sp on AArch64 and sp (x2) on RISC-V. Push, pop and function prologues move it; losing it loses the stack. More in “Registers on x86-64, AArch64 and RISC-V” must point at a call, and every processor punishes a break of that rule in its own way: AArch64 checks sp at each memory access through it, while x86-64 faults only when code uses an instruction that needs aligned memory. Here you state the rule for System V, Microsoft x64, AAPCS64 and RISC-V, find both the instruction that faulted and the one at fault, see why an x86-64 program can pass its tests with a misaligned stack, count the bytes that keep a function aligned, and read how compilers realign the stack for a local that needs 32 bytes.

Every ABI fixes the stack’s alignment at a call, and all four ask for 16 bytes

one_call(x) returns g(x) + 1. No value in it has to survive the call, so on x86-64 it saves no register at all, and on AArch64 and RISC-V it only has to keep its return address: The address a call records so that the callee can come back: the address of the instruction right after the call. x86-64's call pushes it onto the stack; AArch64's bl and blr and RISC-V's jal and call write it to x30 or ra. More in “Calls, returns and the link register”, which arrives in x30 or ra and which the call to g will overwrite. Whatever else its code does to the stack pointer is there for the call alone. listing.sh compiles one file at -O2 for one target with clang -S and prints only the instructions:

one_call.c, and listing.sh, which prints clang's instructions for one target

one_call.c

// one_call keeps nothing across its call to g, so whatever its prologue does to the stack pointer is there for the
// return address and for the alignment the call needs, not for saved values.
long g(long);

long one_call(long x) { return g(x) + 1; }

listing.sh

#!/bin/sh
# listing.sh TARGET FILE.c [FLAGS...]
# Compiles FILE.c at -O2 for TARGET with clang -S and prints only the instructions and labels:
# assembler directives, comments and blank lines are dropped, and runs of tabs become one space.
target=$1
file=$2
shift 2
case $target in
    riscv64-*) set -- -march=rv64gc -mabi=lp64d "$@"; cc=clang-23 ;;  # Apple clang has no RISC-V target
    x86_64-*) set -- -masm=intel "$@"; cc=clang ;;
    *) cc=clang ;;
esac
"$cc" --target="$target" -O2 -S -fno-asynchronous-unwind-tables "$@" -o - "$file" |
    grep -v -E '^[[:space:]]*([.#;/]|$)|^@feat' |
    sed -E 's/[[:space:]]+(;|\/\/|#) .*$//' |
    tr -s '\t' ' '
one_call at -O2 on five targets Python · one_call_session.py
"""Compiles one_call at -O2 for five targets with clang -S and prints the instructions of each."""

import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session  # noqa: E402

session.run("""
# x86-64 Linux, System V ABI
sh listing.sh x86_64-linux-gnu one_call.c
# x86-64 Windows, Microsoft x64 convention (compiler output only: nothing is linked or run)
sh listing.sh x86_64-pc-windows-msvc one_call.c
# AArch64 on macOS, Apple's arm64 ABI
sh listing.sh arm64-apple-macos one_call.c
# AArch64 Linux, AAPCS64
sh listing.sh aarch64-linux-gnu one_call.c
# 64-bit RISC-V Linux, LP64D (Homebrew's clang-23: Apple's clang has no RISC-V target)
sh listing.sh riscv64-linux-gnu one_call.c
""")

Output

# x86-64 Linux, System V ABI
$ sh listing.sh x86_64-linux-gnu one_call.c
one_call:
 push rax
 call g@PLT
 inc rax
 pop rcx
 ret
# x86-64 Windows, Microsoft x64 convention (compiler output only: nothing is linked or run)
$ sh listing.sh x86_64-pc-windows-msvc one_call.c
one_call:
 sub rsp, 40
 call g
 inc eax
 add rsp, 40
 ret
# AArch64 on macOS, Apple's arm64 ABI
$ sh listing.sh arm64-apple-macos one_call.c
_one_call:
 stp x29, x30, [sp, #-16]!
 mov x29, sp
 bl _g
 add x0, x0, #1
 ldp x29, x30, [sp], #16
 ret
# AArch64 Linux, AAPCS64
$ sh listing.sh aarch64-linux-gnu one_call.c
one_call:
 stp x29, x30, [sp, #-16]!
 mov x29, sp
 bl g
 add x0, x0, #1
 ldp x29, x30, [sp], #16
 ret
# 64-bit RISC-V Linux, LP64D (Homebrew's clang-23: Apple's clang has no RISC-V target)
$ sh listing.sh riscv64-linux-gnu one_call.c
one_call:
 addi sp, sp, -16
 sd ra, 8(sp)
 call g
 addi a0, a0, 1
 ld ra, 8(sp)
 addi sp, sp, 16
 ret

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 one_call_session.py

The Linux x86-64 build pushes rax, whose value nobody needs, and pops the same 8 bytes into rcx after the call. The Windows build subtracts 40 from rsp: 32 bytes of shadow space: The 32 bytes a Microsoft x64 caller reserves on the stack just above the return address on every call, so that the callee can store rcx, rdx, r8 and r9 there if it needs to. More in “Calling conventions: arguments and returns”, which every Microsoft x64 call needs, and 8 more. Both AArch64 builds store their frame record, 16 bytes, and the RISC-V build moves sp by 16 to store ra, a single 8-byte register. None of these choices is about one_call itself. Each convention states its stack alignment: The rule an ABI sets for the address in the stack pointer, such as a multiple of 16 at every call. Compilers rely on it to keep values that need aligned memory at fixed offsets from the stack pointer., the rule its build keeps:

ABI The rule for the stack pointer When it must hold
System V AMD64 a multiple of 16 just before call, so rsp + 8 is one at a function’s first instruction; 32 or 64 when an __m256 or __m512 argument goes on the stack at every call
Microsoft x64 16-byte aligned everywhere except inside a prolog or an epilog, and in leaf functions
AAPCS64 sp mod 16 = 0 at every public interface, and at every memory access through sp
RISC-V psABI aligned to 128 bits, which is 16 bytes on entry, and throughout the procedure

Apple’s arm64 platforms follow AAPCS64 here: their stack pointer needs 16-byte alignment too. The difference between the two x86-64 builds and the others comes from the call instruction. x86-64’s call pushes an 8-byte return address, so every function starts 8 bytes below the boundary its caller kept, and before it calls anything it must move rsp by another 8, or 24, or any odd multiple of 8. One push of any register does that, which is all push rax is for: 8 for the return address and 8 for rax make 16. Windows gets there with arithmetic, since 8 and 40 make 48. bl and RISC-V’s call push nothing, so on those two a function starts aligned and keeps every frame a multiple of 16; the RISC-V build of one_call leaves 8 of its 16 bytes unused.

Why 16 bytes, when no register here is wider than 8? The largest values that commonly live on the stack are 16 bytes wide: an SSE register on x86-64, a q register or a pair of x registers on AArch64, a long double on Linux, or an __int128. Microsoft gives its reason for the stack’s 16-byte alignment in those terms: 16 bytes is a common alignment for XMM operations. With every frame starting on such a boundary, a compiler can give those values fixed offsets and use the instructions that need aligned addresses, without testing anything at run time.

Key idea

At a call the stack pointer holds a multiple of 16 under all four ABIs. On x86-64 the call itself pushes 8 bytes, so every function starts 8 bytes off and must move rsp by an odd multiple of 8 before it calls again.

Checkpoint

Check: The alignment rule at a call, per ABI

  1. Question 1 of 2 A System V caller has rsp at a multiple of 16 and executes call. What is rsp modulo 16 at the first instruction of the callee?

    Type a number.

    A hint

    What does call put on the stack, and how big is it?

    Show the answer: Question 1 of 2

    Answer: 8

    call pushes the 8-byte return address, so the callee starts 8 bytes below the boundary its caller kept: rsp + 8 is the multiple of 16, as the psABI words it. That is why one_call's Linux build pushes rax before its own call.

  2. Question 2 of 2 The Windows build of one_call starts with sub rsp, 40, although it keeps nothing on the stack. What are the 40 bytes for?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 32 bytes of shadow space for g, and 8 bytes that make the call aligned

    Windows needs 32 bytes of shadow space at every call and a 16-byte aligned rsp outside prologs and epilogs. With the 8-byte return address, 32 alone would leave rsp 8 bytes off, so the build subtracts 40.

On AArch64 the fault comes at the first access through sp, often in the callee

call_aligned and call_misaligned differ in one immediate: the first makes room for a local with sub sp, sp, #16 and the second with sub sp, sp, #8. The C driver calls one of them and catches SIGBUS and SIGSEGV with a handler that prints what a crash report would show: the signal, its code, the function and offset of the faulting instruction found with dladdr(), and sp modulo 16 at that moment. The handler runs on a stack of its own, set up with sigaltstack(), because the thread’s stack pointer is the one thing in doubt:

Two callers of show, and a driver that reports the faulting instruction

calls_aarch64.s

// Two callers of the C function show(long), for AArch64 on Apple platforms. Both build a frame record and then
// make room for a local before the call. call_aligned reserves 16 bytes, so sp stays a multiple of 16;
// call_misaligned reserves 8, which no instruction here objects to.
    .text
    .p2align 2

    .globl _call_aligned
_call_aligned:
    stp x29, x30, [sp, #-16]!   // sp = S - 16
    mov x29, sp
    sub sp, sp, #16             // sp = S - 32: a multiple of 16
    bl _show                    // x0 still holds the argument
    add sp, sp, #16
    ldp x29, x30, [sp], #16
    ret

    .globl _call_misaligned
_call_misaligned:
    stp x29, x30, [sp, #-16]!   // sp = S - 16
    mov x29, sp
    sub sp, sp, #8              // sp = S - 24: 8 bytes off a multiple of 16
    bl _show                    // show starts with sp 8 bytes off
    add sp, sp, #8
    ldp x29, x30, [sp], #16
    ret

align_main.c

// Driver for calls_aarch64.s. It calls call_aligned, or call_misaligned when run as "./align bad". A handler for
// SIGBUS and SIGSEGV prints what a crash report would show: the signal, its code, the function and offset of the
// instruction that faulted, and sp modulo 16 at that moment. It runs on its own stack (sigaltstack), because the
// thread's stack pointer is the thing in doubt.
#include <dlfcn.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/ucontext.h>
#include <unistd.h>

void call_aligned(long v);
void call_misaligned(long v);

void show(long v) { printf("show(%ld) printed this line\n", v); }

static void on_fault(int sig, siginfo_t *info, void *context) {
  ucontext_t *uc = context;
  uint64_t pc = uc->uc_mcontext->__ss.__pc;
  uint64_t sp = uc->uc_mcontext->__ss.__sp;
  const char *code = (sig == SIGBUS && info->si_code == BUS_ADRALN) ? "BUS_ADRALN" : "other";
  Dl_info where;
  char line[200];
  int n;
  if (dladdr((void *)pc, &where) && where.dli_sname)
    n = snprintf(line, sizeof line, "%s, code %s, at %s+%llu, sp %% 16 = %llu\n", sig == SIGBUS ? "SIGBUS" : "SIGSEGV",
                 code, where.dli_sname, (unsigned long long)(pc - (uint64_t)where.dli_saddr),
                 (unsigned long long)(sp % 16));
  else
    n = snprintf(line, sizeof line, "signal %d, code %s, sp %% 16 = %llu\n", sig, code, (unsigned long long)(sp % 16));
  write(1, line, n);
  _exit(1);
}

int main(int argc, char **argv) {
  static char spare[65536];
  stack_t alt = {.ss_sp = spare, .ss_size = sizeof spare, .ss_flags = 0};
  sigaltstack(&alt, NULL);
  struct sigaction action;
  memset(&action, 0, sizeof action);
  action.sa_sigaction = on_fault;
  action.sa_flags = SA_SIGINFO | SA_ONSTACK;
  sigaction(SIGBUS, &action, NULL);
  sigaction(SIGSEGV, &action, NULL);
  setvbuf(stdout, NULL, _IONBF, 0);
  if (argc > 1 && strcmp(argv[1], "bad") == 0)
    call_misaligned(7);
  else
    call_aligned(7);
  printf("back in main\n");
  return 0;
}
Both callers on Apple silicon Python · misaligned_session.py
"""Builds the two callers of show() with the C driver, runs each once, and disassembles the start of show."""

import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session  # noqa: E402

session.run("""
# One program: the C driver, show() and the two assembly callers
clang -O1 -o align align_main.c calls_aarch64.s
# sp a multiple of 16 at the call
./align; echo "exit=$?"
# sp 8 bytes off at the call
./align bad; echo "exit=$?"
# the first three instructions of show, the function that faulted
llvm-objdump -d --no-show-raw-insn --no-leading-addr --disassemble-symbols=_show align | grep -v -E '^$|file format|Disassembly' | head -4 | tr -s ' \t' ' '
""")

Output

# One program: the C driver, show() and the two assembly callers
$ clang -O1 -o align align_main.c calls_aarch64.s
# sp a multiple of 16 at the call
$ ./align; echo "exit=$?"
show(7) printed this line
back in main
exit=0
# sp 8 bytes off at the call
$ ./align bad; echo "exit=$?"
SIGBUS, code BUS_ADRALN, at show+4, sp % 16 = 8
exit=1
# the first three instructions of show, the function that faulted
$ llvm-objdump -d --no-show-raw-insn --no-leading-addr --disassemble-symbols=_show align | grep -v -E '^$|file format|Disassembly' | head -4 | tr -s ' 	' ' '
<_show>:
 sub sp, sp, #0x20
 stp x29, x30, [sp, #0x10]
 add x29, sp, #0x10

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 misaligned_session.py

The aligned call printed its line. The misaligned one never reached printf: the fault came at show+4, the stp x29, x30, [sp, #0x10] with which show stores its own frame record: Two 64-bit values a function stores in its stack frame: the address of its caller's frame record and its own return address. Linked by the frame pointer, the records let a debugger or profiler walk every active call. More in “Callee-saved registers and stack frames”, while sp was 8 bytes off a multiple of 16. Every instruction before it got through. sub sp, sp, #8 is arithmetic, bl writes x30 and touches no memory, and show’s own sub sp, sp, #0x20 is arithmetic again. Arm’s architecture checks sp only when a load or a store uses it as its base register, and only where the operating system has turned that check on: the SA0 bit of the system register SCTLR_EL1 enables this SP alignment check: An AArch64 check, turned on by the operating system (the SA0 bit for user code), that makes any load or store whose base register is sp fault when sp is not a multiple of 16. Arithmetic on sp and branches are never checked. for user code, and Linux sets that bit in the value it gives SCTLR_EL1 at boot. Step through the run, one value of sp at a time:

Interactive · step by step call_misaligned and show, one value of sp at a time Step through the run of ./align bad. S is sp on entry to call_misaligned, a multiple of 16; lower slots are lower addresses.
call_misaligned and show, one value of sp at a time, step 7 of 7. Showing sp (S - 56), sp mod 16 (8), running (show), S - 16: x29, x30, S - 24: 8 bytes, S - 56: 32 bytes, SIGBUS. In focus: S - 24: 8 bytes.first storespS - 56sp mod 168runningshowS - 16: x29, x30the frame recordS - 24: 8 bytesthe localS - 56: 32 bytesshow's frameSIGBUScode BUS_ADRALN

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. On entry. main has called call_misaligned with bl. sp holds S, a multiple of 16, as AAPCS64 requires at a public interface.
  2. stp x29, x30, [sp, #-16]!. This store uses sp as its base register, so the hardware checks sp first. S passes; the pair goes to S - 16, and sp becomes S - 16, still a multiple of 16.
  3. sub sp, sp, #8. Plain arithmetic: nothing checks its result. sp is now S - 24, which is 8 bytes off a multiple of 16.
  4. bl _show. bl writes the return address into x30 and branches. It reads and writes no memory, so nothing is checked, and show starts with sp still 8 bytes off.
  5. show+0: sub sp, sp, #0x20. show makes its 32-byte frame with arithmetic again. sp becomes S - 56, still 8 bytes off.
  6. show+4: stp x29, x30. The first store through sp. The SP alignment check fails before anything is written, macOS delivers SIGBUS with code BUS_ADRALN, and the handler prints show+4 and sp % 16 = 8.
  7. Where the bug is. show did nothing wrong. The 8 in call_misaligned is the bug, one frame up: with 16 there, sp is S - 32 at the call and the same run prints the line from show.

So the report names a function that did nothing wrong. The rule broke one frame up, in the caller, and any function call_misaligned called would have died at its first store through sp, whether show, memcpy or printf.

Key idea

On AArch64 a misaligned sp is caught at the first load or store that uses it as a base, which is often the callee’s first frame store; the arithmetic instruction that misaligned it passes unnoticed.

BUS_ADRALN is the POSIX code for an invalid address alignment, and Linux reports the same fault with the same code from its handler for what it calls the “SP/PC alignment exception”. Only the number differs. SIGBUS is signal 10 on macOS, so a shell reports a process it ended with status 138, and signal 7 on Linux, which makes the status 135; the stack shows where 128 plus the signal’s number comes from.

Debugging the function named in the report

The function that faults is usually innocent: its first store through sp checks a stack pointer its caller handed over. Look at the frames above it instead, for an adjustment of sp that is not a multiple of 16, such as sub sp, sp, #8 or str x19, [sp, #-8]!, on the path that ran.

Checkpoint

Check: Where AArch64 catches a misaligned sp

  1. Question 1 of 2 After call_misaligned's sub sp, sp, #8, the run executes bl _show, then sub sp, sp, #0x20 and stp x29, x30, [sp, #0x10] in show. Which instruction does the SP alignment check stop?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The stp at show+4

    AArch64 checks sp when a load or store uses it as a base, never when an instruction computes it. The check fired at show+4, two instructions into a correct function, one frame below the bug.

  2. Question 2 of 2 The same misaligned program, built and run on AArch64 Linux, is killed by the same fault without a handler. What exit status does the shell report?

    Type a number.

    A hint

    Linux numbers SIGBUS differently from macOS.

    Show the answer: Question 2 of 2

    Answer: 135

    SIGBUS is signal 7 on Linux, so the shell reports 128 + 7 = 135. On macOS SIGBUS is signal 10, which gives 138.

On x86-64 only aligned vector accesses fault, so a misaligned stack can pass tests

x86-64 has no check like SA0: push, pop, mov and call work at any address. The rule matters because compiled code depends on it. clear_and_use zeroes a 32-byte array before it passes the array on, and total is a variadic function: A function that takes a variable number of arguments after its named parameters, such as printf, whose prototype ends in an ellipsis (...). Each ABI says where the extra arguments travel: on Apple silicon in 8-byte stack slots, on AArch64 and x86-64 Linux in the usual argument registers. More in “Calling C library functions from assembly”, like printf, that adds n doubles:

vector_slots.c: two functions that depend on the alignment of rsp C · vector_slots.c
// Two functions whose x86-64 code depends on rsp being where the System V ABI says it is. clear_and_use zeroes a
// 32-byte local array; total is variadic, like printf, and adds n doubles.
#include <stdarg.h>

void use(char *);

void clear_and_use(void) {
  char buf[32] = {0};
  use(buf);
}

double total(int n, ...) {
  va_list ap;
  va_start(ap, n);
  double sum = 0;
  for (int i = 0; i < n; i++) sum += va_arg(ap, double);
  va_end(ap);
  return sum;
}
clear_and_use and the start of total for x86-64 Linux Python · vector_slots_session.py
"""Compiles clear_and_use and total at -O2 for x86-64 Linux and prints their first instructions."""

import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session  # noqa: E402

session.run("""
# x86-64 Linux, System V ABI: all of clear_and_use and the first 17 instructions of total
sh listing.sh x86_64-linux-gnu vector_slots.c | head -27
""")

Output

# x86-64 Linux, System V ABI: all of clear_and_use and the first 17 instructions of total
$ sh listing.sh x86_64-linux-gnu vector_slots.c | head -27
clear_and_use:
 sub rsp, 40
 xorps xmm0, xmm0
 movaps xmmword ptr [rsp + 16], xmm0
 movaps xmmword ptr [rsp], xmm0
 mov rdi, rsp
 call use@PLT
 add rsp, 40
 ret
total:
 sub rsp, 88
 mov qword ptr [rsp - 88], rsi
 mov qword ptr [rsp - 80], rdx
 mov qword ptr [rsp - 72], rcx
 mov qword ptr [rsp - 64], r8
 mov qword ptr [rsp - 56], r9
 test al, al
 je .LBB1_19
 movapd xmmword ptr [rsp - 48], xmm0
 movaps xmmword ptr [rsp - 32], xmm1
 movaps xmmword ptr [rsp - 16], xmm2
 movaps xmmword ptr [rsp], xmm3
 movaps xmmword ptr [rsp + 16], xmm4
 movaps xmmword ptr [rsp + 32], xmm5
 movaps xmmword ptr [rsp + 48], xmm6
 movaps xmmword ptr [rsp + 64], xmm7
 lea rax, [rsp - 96]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 vector_slots_session.py

clear_and_use subtracts 40 from an rsp that arrived 8 bytes off, which leaves rsp aligned, and clears the array with two movaps stores of 16 zero bytes each. Intel’s manual requires the memory operand of movaps, movapd and movdqa to sit on a 16-byte boundary, 32 bytes for their 256-bit forms, and raises a general-protection exception: The x86 exception #GP, raised for a protection violation such as an aligned vector instruction like movaps whose memory operand is not on a 16-byte boundary. Linux delivers it to a user program as SIGSEGV. otherwise; for unaligned data it points to the movups form. Linux turns a general-protection fault in user code into SIGSEGV, status 139. Had the caller left rsp 8 bytes off, both stores would have faulted, a crash in clear_and_use for its caller’s mistake.

total shows how such a mistake can hide. A variadic function cannot know which registers carry its arguments, so its prologue stores all of them in a register save area: Memory in a variadic function's own frame where its prologue stores the argument registers that may carry variadic arguments, so that va_arg can read them in order: up to rdi to r9 and xmm0 to xmm7 under System V, x0 to x7 and q0 to q7 under AAPCS64 on Linux. More in “Structs, variadic calls and large values”: the five integer argument registers after rdi with plain mov, and xmm0 to xmm7 with movapd and movaps. Under System V the caller’s al gives the count of vector registers that carry arguments, never below the true count and never above 8, and test al, al with je skips all eight vector stores when al is 0. A misaligned call that passes only integers to such a function sets al to 0, jumps over every aligned store and works; the same call with one double sets al to 1, reaches movapd xmmword ptr [rsp - 48], xmm0 and faults.

Instruction on a stack address Alignment it needs With rsp 8 bytes off
push, pop, call, mov none works
movups, movdqu none works
movaps, movapd, movdqa 16 bytes general-protection fault, SIGSEGV
vmovaps with a 256-bit ymm register 32 bytes general-protection fault, SIGSEGV

GCC’s manual describes the same trap from the other side: code built with 16-byte stack alignment, a standard library for instance, called with a misaligned stack may meet misaligned-access traps in SSE instructions. A test suite that never passes a double through the faulty call never runs those instructions.

Trusting a call because it worked once

A misaligned call into printf with "%d" prints correctly, because al is 0 and the vector saves are skipped. It proves nothing about the same call with "%f". Check rsp modulo 16 at the call instead of the output.

Checkpoint

Check: Why x86-64 faults only in aligned instructions

  1. Question 1 of 2 A hand-written x86-64 Linux routine calls a variadic C function with rsp 8 bytes off. With two int arguments it works; with one double it crashes. Why the difference?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: With a double the caller sets al to 1, so the callee's prologue runs its aligned vector saves, which fault

    The variadic prologue stores the vector argument registers with aligned instructions, guarded by al. The misalignment is there on both calls; only the call with a double reaches an instruction that checks it.

  2. Question 2 of 2 Which of these instructions fault when their stack address is 8 bytes off a multiple of 16? Choose every one that applies.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • movaps xmmword ptr [rsp], xmm0
    • movdqa xmmword ptr [rsp + 16], xmm1

    x86-64 punishes a misaligned stack only through instructions that need aligned memory: movaps, movapd and movdqa need 16 bytes and their 256-bit forms 32. Everything else runs.

Counting every push and every sub keeps a hand-written function aligned

Compilers get alignment right by counting bytes, and hand-written code can count the same way. keep_one keeps x across its call to g, and keep_two keeps x and y, so they need one and two callee-saved register: A register whose value a called function must hand back unchanged: if it writes the register, it saves the old value first and restores it before it returns. rbx on x86-64, x19 on AArch64 and s0 on RISC-V are callee-saved. More in “Callee-saved registers and stack frames”:

pad.c: one value and two values kept across a call C · pad.c
// Two functions that keep values across one call to g: keep_one needs one callee-saved register, keep_two needs
// two. Count what each prologue does to the stack pointer before the call.
long g(long);

long keep_one(long x) { return g(x) + x; }

long keep_two(long x, long y) { return g(x) + x * y; }
keep_one and keep_two at -O2 on four targets Python · pad_session.py
"""Compiles keep_one and keep_two at -O2 for four targets and prints the instructions of each."""

import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session  # noqa: E402

session.run("""
# x86-64 Linux, System V ABI
sh listing.sh x86_64-linux-gnu pad.c
# x86-64 Windows, Microsoft x64 convention (compiler output only)
sh listing.sh x86_64-pc-windows-msvc pad.c
# AArch64 on macOS
sh listing.sh arm64-apple-macos pad.c
# 64-bit RISC-V Linux, LP64D
sh listing.sh riscv64-linux-gnu pad.c
""")

Output

# x86-64 Linux, System V ABI
$ sh listing.sh x86_64-linux-gnu pad.c
keep_one:
 push rbx
 mov rbx, rdi
 call g@PLT
 add rax, rbx
 pop rbx
 ret
keep_two:
 push r14
 push rbx
 push rax
 mov rbx, rsi
 mov r14, rdi
 call g@PLT
 imul rbx, r14
 add rax, rbx
 add rsp, 8
 pop rbx
 pop r14
 ret
# x86-64 Windows, Microsoft x64 convention (compiler output only)
$ sh listing.sh x86_64-pc-windows-msvc pad.c
keep_one:
 push rsi
 sub rsp, 32
 mov esi, ecx
 call g
 add eax, esi
 add rsp, 32
 pop rsi
 ret
keep_two:
 push rsi
 push rdi
 sub rsp, 40
 mov esi, edx
 mov edi, ecx
 call g
 imul esi, edi
 add eax, esi
 add rsp, 40
 pop rdi
 pop rsi
 ret
# AArch64 on macOS
$ sh listing.sh arm64-apple-macos pad.c
_keep_one:
 stp x20, x19, [sp, #-32]!
 stp x29, x30, [sp, #16]
 add x29, sp, #16
 mov x19, x0
 bl _g
 add x0, x0, x19
 ldp x29, x30, [sp, #16]
 ldp x20, x19, [sp], #32
 ret
_keep_two:
 stp x20, x19, [sp, #-32]!
 stp x29, x30, [sp, #16]
 add x29, sp, #16
 mov x19, x1
 mov x20, x0
 bl _g
 madd x0, x19, x20, x0
 ldp x29, x30, [sp, #16]
 ldp x20, x19, [sp], #32
 ret
# 64-bit RISC-V Linux, LP64D
$ sh listing.sh riscv64-linux-gnu pad.c
keep_one:
 addi sp, sp, -16
 sd ra, 8(sp)
 sd s0, 0(sp)
 mv s0, a0
 call g
 add a0, a0, s0
 ld ra, 8(sp)
 ld s0, 0(sp)
 addi sp, sp, 16
 ret
keep_two:
 addi sp, sp, -32
 sd ra, 24(sp)
 sd s0, 16(sp)
 sd s1, 8(sp)
 mv s0, a1
 mv s1, a0
 call g
 mul a1, s0, s1
 add a0, a0, a1
 ld ra, 24(sp)
 ld s0, 16(sp)
 ld s1, 8(sp)
 addi sp, sp, 32
 ret

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pad_session.py

On x86-64 Linux the count starts at 8, the return address. keep_one pushes rbx to save it, which makes 16, and calls g with nothing more. keep_two must save r14 and rbx, which makes 24, so it adds push rax, a third push for the count alone, and gives those 8 bytes back with add rsp, 8 instead of a pop. Under System V the rule is parity: before a call, a function’s pushes and subtractions together must come to an odd multiple of 8. Windows adds its 32 bytes of shadow space to the same sum. The bytes moved before each call, counted from the caller’s aligned rsp:

Target keep_one keep_two
x86-64 Linux 8 + push rbx = 16 8 + three pushes = 32
x86-64 Windows 8 + push rsi + 32 = 48 8 + two pushes + 40 = 64
AArch64 macOS a 32-byte frame: x20, x19 and the frame record the same 32 bytes
RISC-V Linux a 16-byte frame: ra and s0 a 32-byte frame: ra, s0, s1 and 8 spare

AArch64 and RISC-V have no return address on the stack to count, so they round every frame up to a multiple of 16 instead. Apple’s keep_one saves x20 beside x19 because a pair takes the same 16 bytes as one register, and the RISC-V keep_two leaves the slot at 0(sp) unused, since 24 bytes of saves need a 32-byte frame. Step through the count for keep_two on x86-64 Linux:

Interactive · step by step keep_two on x86-64 Linux, counted in bytes from its caller Step through the prologue, the call and the epilogue. The count is how far rsp is below the aligned value the caller had before its call.
keep_two on x86-64 Linux, counted in bytes from its caller, step 8 of 8. Showing bytes moved (0), rsp mod 16 (0), g (returned). In focus: bytes moved.bytes moved0rsp mod 160greturned

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. On entry. The caller's call pushed the return address, so rsp is 8 bytes below the multiple of 16 the caller kept. keep_two must call g with rsp aligned again.
  2. push r14. keep_two keeps x in r14 across the call, so it saves the caller's r14 first. 8 + 8 = 16 bytes: rsp is aligned right now.
  3. push rbx. y lives in rbx across the call, so rbx is saved too. 24 bytes: 8 off again. Two saves alone would make the call misaligned.
  4. push rax. rax holds nothing keep_two needs; this push exists only for the count. 32 bytes: rsp is a multiple of 16 again.
  5. call g@PLT. The call pushes keep_two's own return address, so g starts 8 bytes below a multiple of 16, exactly as keep_two did. The rule holds one level down.
  6. add rsp, 8. Back from g, the padding is given back without loading it: the epilogue adds 8 instead of popping into a register.
  7. pop rbx, pop r14. The two saved values come back in the reverse order of their pushes, and the count falls to 8, the return address alone.
  8. ret. ret pops the return address. rsp is the caller's again, a multiple of 16, as it was before the caller's call.

At its call, g starts exactly as keep_two did: 8 bytes below a multiple of 16.

Key idea

Count from 8 on x86-64, because of the return address, and make every call see a multiple of 16: an odd number of 8-byte steps under System V, the same with 32 bytes of shadow space under Windows. AArch64 and RISC-V round every frame up to a multiple of 16.

The count works the same way in a hand-written routine: write down the 8 of the return address, add 8 for each push and the operand of each sub rsp, and check that the sum is a multiple of 16 at every call, on every path. A routine that pushes on one path and not on another needs the check on both.

Counting from zero on x86-64

A count that forgets the return address adds a padding push where none is needed and leaves it out where it is: a function that saves only rbx and pushes rax as well calls g with rsp 8 bytes off. Start every count at 8.

Checkpoint

Check: Counting bytes to keep a function aligned

  1. Question 1 of 2 A System V function saves rbx, r12 and r13 with three pushes and then calls printf. How many bytes of padding must it add before the call?

    Type a number.

    A hint

    Start the count at 8.

    Show the answer: Question 1 of 2

    Answer: 0 bytes

    8 for the return address and 24 for three pushes make 32, a multiple of 16, so the call is aligned as it is. With two pushes it would need 8 bytes more, as keep_two's push rax shows.

  2. Question 2 of 2 An AArch64 function stores a frame record and saves x19, x20 and x21 before it calls. What is the smallest frame, in bytes, that keeps sp a multiple of 16?

    Type a number.

    A hint

    A frame record is two 8-byte words.

    Show the answer: Question 2 of 2

    Answer: 48 bytes

    16 bytes of record and 24 of saves make 40, which is not a multiple of 16, so the frame rounds up to 48 and 8 bytes stay unused, as in RISC-V's 32-byte frame for 24 bytes of saves.

A local that needs 32-byte alignment makes the function realign the stack

_Alignas(32) asks for more than any of the four ABIs promises. A caller hands over a 16-byte boundary, and nothing says whether that is also a 32-byte one. No constant subtraction can fix that, because the constant that works depends on where the stack pointer happens to be, so the function rounds the stack pointer down instead:

wide.c: a 64-byte local aligned to 32 bytes C · wide.c
// buf must start at a multiple of 32, twice the alignment any of these ABIs promises for the stack pointer, so the
// function cannot simply subtract a constant from it.
void use(char *);

void wide_local(void) {
  _Alignas(32) char buf[64] = {0};
  use(buf);
}
wide_local at -O2 on three targets Python · wide_session.py
"""Compiles wide_local at -O2 for three targets and prints the instructions of each."""

import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session  # noqa: E402

session.run("""
# -fno-stack-protector leaves out a canary check that has nothing to do with alignment
# x86-64 Linux, System V ABI
sh listing.sh x86_64-linux-gnu wide.c -fno-stack-protector
# AArch64 on macOS
sh listing.sh arm64-apple-macos wide.c -fno-stack-protector
# 64-bit RISC-V Linux, LP64D
sh listing.sh riscv64-linux-gnu wide.c -fno-stack-protector
""")

Output

# -fno-stack-protector leaves out a canary check that has nothing to do with alignment
# x86-64 Linux, System V ABI
$ sh listing.sh x86_64-linux-gnu wide.c -fno-stack-protector
wide_local:
 push rbp
 mov rbp, rsp
 and rsp, -32
 sub rsp, 96
 xorps xmm0, xmm0
 movaps xmmword ptr [rsp + 48], xmm0
 movaps xmmword ptr [rsp + 32], xmm0
 movaps xmmword ptr [rsp + 16], xmm0
 movaps xmmword ptr [rsp], xmm0
 mov rdi, rsp
 call use@PLT
 mov rsp, rbp
 pop rbp
 ret
# AArch64 on macOS
$ sh listing.sh arm64-apple-macos wide.c -fno-stack-protector
_wide_local:
 stp x29, x30, [sp, #-16]!
 mov x29, sp
 sub x9, sp, #80
 and sp, x9, #0xffffffffffffffe0
 movi.2d v0, #0000000000000000
 stp q0, q0, [sp, #32]
 stp q0, q0, [sp]
 mov x0, sp
 bl _use
 mov sp, x29
 ldp x29, x30, [sp], #16
 ret
# 64-bit RISC-V Linux, LP64D
$ sh listing.sh riscv64-linux-gnu wide.c -fno-stack-protector
wide_local:
 addi sp, sp, -96
 sd ra, 88(sp)
 sd s0, 80(sp)
 addi s0, sp, 96
 andi sp, sp, -32
 sd zero, 32(sp)
 sd zero, 40(sp)
 sd zero, 48(sp)
 sd zero, 56(sp)
 sd zero, 0(sp)
 sd zero, 8(sp)
 sd zero, 16(sp)
 sd zero, 24(sp)
 mv a0, sp
 call use
 addi sp, s0, -96
 ld ra, 88(sp)
 ld s0, 80(sp)
 addi sp, sp, 96
 ret

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 wide_session.py

After push rbp, the x86-64 build’s rsp is a multiple of 16. and rsp, -32 clears its low five bits, which lowers it by 0 or 16 bytes to a multiple of 32, and sub rsp, 96 keeps that, since 96 is 3 × 32; four movaps stores then clear the 64 bytes at [rsp]. The AArch64 build computes x9 = sp - 80 and writes the rounded value straight into sp with and sp, x9, #0xffffffffffffffe0, and the RISC-V build uses andi sp, sp, -32. This is stack realignment: Rounding the stack pointer down with an and, such as and rsp, -32, so that a local can get more alignment than the ABI promises. The rounding loses the old value, so the function keeps a copy in a frame pointer and restores it from there., and it has a price. Rounding throws away the old value of the stack pointer, and only a register can bring it back, so every build first sets a frame pointer: A register that locates the current function's frame record, so that each frame links to its caller's: x29 on AArch64 and rbp on x86-64 address the record, and s0 on RISC-V points just above it. Compilers may omit it, and Apple requires x29 to address a valid frame record. More in “Callee-saved registers and stack frames” and its epilogue restores the stack pointer from it:

ISA Rounds down with Restores the stack pointer with
x86-64 and rsp, -32 mov rsp, rbp, then pop rbp
AArch64 and sp, x9, #0xffffffffffffffe0 mov sp, x29, then ldp x29, x30
RISC-V andi sp, sp, -32 addi sp, s0, -96, then the loads

Microsoft’s convention puts the same point in one line: alignment above 16 bytes must be done by hand. The System V rule of the first section needs 32 or 64 bytes only from a caller that passes an __m256 or __m512 argument on the stack; a local that needs that much is the callee’s own business, as in wide_local. GCC’s -mstackrealign uses the same rounding at entry, for another reason: it lets code that keeps 16-byte alignment be called from older code that keeps only 4.

Checkpoint

Check: Realigning the stack for a 32-byte local

  1. Question 1 of 2 Why does every build of wide_local set a frame pointer, although the function calls only one other function?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Rounding the stack pointer down loses its old value, and the epilogue restores it from the frame pointer

    A fixed subtraction can be undone with a fixed addition; a rounding cannot. So a function that realigns its stack keeps the old stack pointer in a frame pointer and restores it from there.

  2. Question 2 of 2 rsp holds 1,040, a multiple of 16 but not of 32, when and rsp, -32 runs. What does rsp hold afterwards?

    Type a number.

    A hint

    -32 has its low five bits clear and every other bit set.

    Show the answer: Question 2 of 2

    Answer: 1024

    and with -32 clears the low five bits, which rounds down to a multiple of 32: 1,040 = 32 × 32 + 16, so rsp falls by 16 to 1,024. A value that is already a multiple of 32 does not move.

Practise

Three changes to copies of the lesson’s files. Build and list them with the commands the sessions above print.

  1. In a copy of calls_aarch64.s, write call_twice(v), which calls show(v) and then show(v + 1), keeping v in x19 across the first call. Decide the frame’s size before you write the prologue, with the record and x19 in it. Add call_twice to a copy of align_main.c and check that both lines print. Then change only the frame size, so that one of your instructions leaves sp 8 bytes off, and predict which instruction the handler names before you run it.
  2. Write keep_three(x, y, z), which returns g(x) + x * y + z, by hand for x86-64 System V, in a GNU as file in Intel syntax, keeping its three values in rbx, r12 and r13 across the call. Count from 8 and decide whether it needs padding. Assemble it with clang --target=x86_64-linux-gnu -c and list it with llvm-objdump -d -M intel. Then add a fourth saved register and say what changes.
  3. Change wide.c to _Alignas(64) and predict the mask and the subtraction in the x86-64 build before you run listing.sh on it. Then run it for arm64-apple-macos and find the new mask.

Practice that adapts to you

Practise this lesson

15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 15 Which statement about the stack pointer holds for System V, Microsoft x64, AAPCS64 and RISC-V alike?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: It is a multiple of 16 when a call executes

    The four ABIs agree on 16 bytes at a call. They differ in where else the rule holds, and x86-64 differs in where the function starts, because its call instruction pushes the return address.

  2. Question 2 of 15 A RISC-V function saves only ra before it calls another function. By how many bytes does it lower sp?

    Type a number.

    Show the answer: Question 2 of 15

    Answer: 16 bytes

    ra takes 8 bytes, but the psABI keeps sp aligned to 128 bits throughout, so the frame is 16 bytes and 8 of them stay unused, as in one_call's RISC-V build: addi sp, sp, -16, then sd ra, 8(sp).

  3. Question 3 of 15 A System V caller passes an __m256 argument on the stack. What must be true of rsp just before the call?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: It is a multiple of 32

    System V's rule scales with the widest value on the stack: 16 bytes normally, 32 or 64 when __m256 or __m512 arguments are passed there.

  4. Question 4 of 15 What turns on the AArch64 check that stopped show?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: The SA0 bit of SCTLR_EL1, which the operating system sets for user code

    The SP alignment check is a feature of the processor that the operating system switches on with SA0 for EL0 code. With it on, a load or store through a misaligned sp raises a fault, which Linux and macOS deliver as SIGBUS.

  5. Question 5 of 15 A report from AArch64 says SIGBUS, BUS_ADRALN, at memcpy+12, with sp % 16 = 8, and memcpy was called from your routine parse_block. Where do you look first?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: At every change parse_block makes to sp on the path that called memcpy

    A misaligned sp faults in the first function that stores through it, which is often a correct callee. The bug is in the frame above, as call_misaligned's sub sp, sp, #8 was.

  6. Question 6 of 15 A function on AArch64 Linux is entered with sp 8 bytes off, and its first instruction is stp x29, x30, [sp, #-16]!. What happens?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: That very stp faults, because the check reads sp as the base before the offset is applied

    In show the first access through sp came at its second instruction; a function whose first instruction stores through sp faults at once.

  7. Question 7 of 15 A movaps in user code on x86-64 Linux stores to a stack address that is 8 bytes off a multiple of 16. Which signal does the program get?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: SIGSEGV

    The two architectures report a misaligned stack differently: AArch64 with an SP alignment fault, SIGBUS, and x86-64 with a general-protection exception from an aligned vector instruction, SIGSEGV on Linux.

  8. Question 8 of 15 Why can a misaligned call into a variadic System V function pass every test that passes only integers?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: al is 0 for those calls, so the callee's prologue skips its aligned stores of xmm0 to xmm7

    A misaligned stack on x86-64 waits for the first instruction that needs alignment. In a variadic callee those instructions run only when al says vector registers were used.

  9. Question 9 of 15 clear_and_use could zero its array with movups instead of movaps. Would that make it safe to call with a misaligned stack?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: Only its own two stores; use and every function it calls may still rely on an aligned rsp

    The fix for a misaligned stack is in the caller that misaligned it. Making one callee tolerant moves the crash one call deeper, to the next function that relies on the rule.

  10. Question 10 of 15 Under System V, a function moves rsp only with 8-byte pushes before its call. Which numbers of pushes keep the call aligned?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: An odd number: 1, 3, 5 and so on

    Count from 8 and add 8 per push: 1, 3 or 5 pushes give 16, 32 or 48. keep_one used one push, keep_two three.

  11. Question 11 of 15 A Windows x64 function pushes rsi and rdi and then calls another function. What is the smallest value of n in its sub rsp, n?

    Type a number.

    Show the answer: Question 11 of 15

    Answer: 40 bytes

    It needs 32 bytes of shadow space, and 8 + 16 + 32 = 56 is 8 bytes off, so n = 40 and the total is 64, exactly the sub rsp, 40 of keep_two's Windows build.

  12. Question 12 of 15 A System V routine pushes rbx at entry. Only on its error path does it also push r12 before calling a logging function. What is wrong?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: The error path calls with rsp 8 bytes off: 8 + 8 + 8 = 24

    Alignment is a property of each call site. A count that is right on the main path says nothing about a path that pushes once more, which is the kind of bug that crashes only when an error is logged.

  13. Question 13 of 15 Why can wide_local not reach a 32-byte boundary with a fixed sub rsp?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: The amount needed depends on where rsp is, and a caller promises only 16 bytes

    A 16-byte promise leaves two cases for a 32-byte boundary, so the function rounds with an and instead of subtracting a constant.

  14. Question 14 of 15 Which instruction gives wide_local's AArch64 build its stack pointer back before ldp x29, x30, [sp], #16?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: mov sp, x29

    Realignment is undone from a saved copy of the stack pointer. Each build keeps one: rbp on x86-64, x29 on AArch64, s0 on RISC-V.

  15. Question 15 of 15 For a local declared _Alignas(64), an x86-64 build rounds with and rsp, m. What is m, as a signed number like the -32 of wide_local?

    Type a number.

    Show the answer: Question 15 of 15

    Answer: -64

    -64 has its low six bits clear, so and rsp, -64 rounds rsp down to a multiple of 64, by 0, 16, 32 or 48 bytes from an rsp that was a multiple of 16.

Interview questions

What must the stack pointer satisfy at a call on x86-64 Linux and on AArch64, and why does a System V function that pushes exactly one register need no padding?

On x86-64 Linux, under System V, rsp must be a multiple of 16 just before a call executes. The call then pushes an 8-byte return address, so at a function’s first instruction rsp + 8 is the multiple of 16. On AArch64, AAPCS64 requires sp to be a multiple of 16 at every public interface and at every memory access through sp, and the hardware checks the second rule when the operating system enables it. A System V function that pushes one register has moved rsp by 8 bytes after the return address’s 8, which makes 16, so it can call at once. With zero or two pushes it needs 8 more bytes, and compilers often add push rax or sub rsp, 8 for that. AArch64 has no pushed return address, so its frames are simply rounded up to a multiple of 16.

Two longer cases in the module’s bank start from the crashes on this page: one is a printf that fails only when it prints a double, the other a bus error reported in a library function that is not at fault.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    An x86-64 Linux assembly routine pushes rbx and r12 at entry and calls printf to log a count with "%ld". It has worked for months. A colleague adds the mean as a double, "%ld items, mean %.2f", sets al to 1, and the routine now dies with SIGSEGV inside printf. Explain the crash, prove it, and fix it.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    After a change to a hand-written AArch64 routine that now keeps a third local, a Linux service dies with SIGBUS, si_code BUS_ADRALN, and the top frame is format_line, a logging-library C function untouched for years. The routine addresses its locals through x29. Find the fault, explain why format_line is named, and fix it.

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What alignment must the stack pointer have at a call, and why is an x86-64 function's entry 8 bytes off?

    A multiple of 16 under System V, Microsoft x64, AAPCS64 and RISC-V. On x86-64 the call itself pushes an 8-byte return address, so a function must move rsp by an odd multiple of 8 before it calls again.

  2. Why can a System V function that pushes exactly one register call another at once?

    The return address took 8 bytes and the push another 8, which makes 16, so rsp is a multiple of 16 again. With zero or two pushes the function needs 8 more bytes, often a push rax or a sub rsp, 8.

  3. On AArch64, where does a misaligned stack pointer fault?

    At the first memory access whose base register is sp, often the callee's first frame store, with SIGBUS and BUS_ADRALN. The arithmetic that misaligned sp is never checked, so the crash points at correct code.

  4. Why can a misaligned stack pass tests on x86-64 and then crash later?

    x86-64 faults only on instructions that need aligned memory, such as movaps. Integer code runs fine until a callee, typically a variadic one saving its vector registers, uses an aligned vector store.

  5. How do you keep hand-written functions aligned?

    Count: start at 8 on x86-64 for the return address, add every push and every subtraction, include Windows' 32 bytes of shadow space, and make each call see a multiple of 16. On AArch64 and RISC-V round every frame up to 16.

  6. How does a function get 32-byte alignment for a local when the ABI only promises 16?

    It rounds the stack pointer down with an and, such as and rsp, -32, and keeps the old value in a frame pointer to restore it, because the rounding loses it.

Key takeaways

  • System V, Microsoft x64, AAPCS64 and RISC-V all keep the stack pointer a multiple of 16 at a call; on x86-64 the return address that call pushes leaves each function’s entry 8 bytes off, so the function moves rsp by an odd multiple of 8 before it calls again.
  • AArch64 checks sp at every load or store that uses it as a base, so a misaligned stack faults with SIGBUS and BUS_ADRALN, often in a correct callee, while the arithmetic that misaligned it passes.
  • x86-64 faults only in instructions that need aligned memory, such as movaps, so a misaligned call can work for integer arguments and fail as soon as a variadic callee saves its vector registers.
  • Counting keeps hand-written code aligned: start at 8 on x86-64, add every push and subtraction, include Windows’ 32 bytes of shadow space, and round every AArch64 and RISC-V frame up to a multiple of 16.
  • A local that needs more alignment than 16 bytes makes the function round the stack pointer down with an and, and restore it from a frame pointer, because the rounding loses its old value.

Terms in this lesson

general-protection exception
The x86 exception #GP, raised for a protection violation such as an aligned vector instruction like movaps whose memory operand is not on a 16-byte boundary. Linux delivers it to a user program as SIGSEGV.
SP alignment check
An AArch64 check, turned on by the operating system (the SA0 bit for user code), that makes any load or store whose base register is sp fault when sp is not a multiple of 16. Arithmetic on sp and branches are never checked.
stack alignment
The rule an ABI sets for the address in the stack pointer, such as a multiple of 16 at every call. Compilers rely on it to keep values that need aligned memory at fixed offsets from the stack pointer.
stack realignment
Rounding the stack pointer down with an and, such as and rsp, -32, so that a local can get more alignment than the ABI promises. The rounding loses the old value, so the function keeps a copy in a frame pointer and restores it from there.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.