Driver FixRecommendedSound, Wi-Fi or graphics acting up? Check drivers firstFind missing or outdated drivers fast.Check DriversHalloween DealsAmazon USHalloween week deals: useful picks before month-endAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan Now×
FREEDOM251

Freedom251 › Blog › AAPCS64

AArch64 Register Rules: Caller- vs Callee-Saved Explained

By Freedom251 Team · · 9 min read

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Under the base Arm AAPCS64, a called function must return with the incoming values of x19–x29 and SP intact. Registers x0–x17 are generally caller-saved, although x16, x17 and x18 have special roles that require extra care. For SIMD and floating-point state, only the low 64 bits of v8–v15 are preserved; v0–v7 and v16–v31 are caller-saved.

That division is a contract at every function-call boundary. If your value is in a caller-saved register and must remain live after a call, your code must move or save it first. If a callee changes a callee-saved register, the callee must restore the original value before returning.

As an Amazon Associate I earn from qualifying purchases.

The core rule: caller-saved versus callee-saved

AArch64 function calls follow the Arm Architecture Procedure Call Standard for 64-bit systems (AAPCS64). The convention allows separately compiled functions to interoperate because both sides know who owns each register across a call.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A caller-saved register is scratch space from the callee’s point of view. The called function may overwrite it, so the caller must save any value that it still needs. A callee-saved register has the opposite contract: if the callee uses it, the callee saves the incoming value and restores it before returning.

The obligation is conditional. A function does not have to push every callee-saved register in its prologue. It saves only the registers it actually changes. Likewise, a caller need not save every caller-saved register—only values that remain live across the particular call.

Register-by-register map

Registers Base AAPCS64 purpose Value guaranteed after a call?
x0–x7 Integer, pointer and ordinary result registers No; caller-saved
x8 Indirect-result location register No; caller-saved
x9–x15 General temporary registers No; caller-saved
x16 (IP0), x17 (IP1) Intra-procedure-call temporaries; available to linker veneers and PLT code No; do not keep live values across calls
x18 Platform-defined register, or an ordinary temporary where the platform assigns no dedicated role Platform-dependent; consult the target ABI
x19–x28 General callee-saved registers Yes; the full 64-bit value
x29 (FP) Frame pointer and callee-saved register Yes; subject to the platform’s frame-record rules
x30 (LR) Link register containing a return address after BL Not as an ordinary callee-saved register
SP Stack pointer Yes; it must meet stack-alignment and validity constraints
v0–v7 SIMD and floating-point arguments and results No; caller-saved
v8–v15 SIMD and floating-point registers Only the low 64 bits of each register
v16–v31 SIMD and floating-point temporaries No; caller-saved

“Preserved” means the value visible to the caller after return, not that the physical register is untouched during execution. A callee can use x19, for example, provided it saves the incoming 64-bit value and restores it before executing RET.

What the caller must do before a call

Suppose a caller computes a pointer in x9, then calls another function and uses that pointer afterward. The called function is free to overwrite x9. The caller must move the pointer into a callee-saved register, spill it to the stack, or otherwise arrange to reconstruct it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
// Conceptual sequence, not a complete function
// x9 contains a value that is still needed
mov     x19, x9          // keep it in a callee-saved register
bl      work             // work may overwrite x9
mov     x9, x19          // value is available again

Using x19 creates a new obligation for the current function: if it changes x19, it must preserve the value that its own caller supplied. A compiler may instead spill the value to a stack slot, especially when register pressure or an existing stack frame makes that cheaper.

The same reasoning applies to arguments and results. x0–x7 carry integer or pointer arguments and return values, but they are not a place to keep long-lived state across another call. A function returning a value in x0 must place the final result there after its last call, not assume an earlier value survived.

What the callee must preserve

General-purpose callee-saved registers

Under base AAPCS64, x19–x29 are callee-saved, and all 64 bits of each register are covered. If a function uses only x20, it can save and restore only x20; saving the whole range is unnecessary unless the implementation actually modifies those registers.

A typical non-leaf function creates a stack frame, stores the incoming values of the callee-saved registers it will modify, performs its work, reloads them, and then returns. The exact instruction sequence is an implementation choice, but the observable result must satisfy the ABI.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The stack pointer

SP is also preserved across a public call. A function may adjust it to allocate locals or spill slots, but it must restore the caller’s stack position before returning. AAPCS64 also imposes stack constraints, including required alignment at public call boundaries. Code that temporarily misaligns the stack must repair it before making a call or exposing that state to code that relies on the public interface.

The link register is different

BL target writes the return address into x30, the link register. A leaf function that makes no further call can often return directly with RET, which branches through x30. A non-leaf function calls another function with BL, and that nested call overwrites x30. To return to its own caller later, the non-leaf function normally saves its original link value in the stack frame or another safe location.

This is why x30 should not be described simply as another callee-saved register. The ABI’s preserved general-purpose set is x19–x29; preserving a return address is a control-flow requirement of any function that needs to survive a nested call.

Why x16 and x17 are unsafe for live values

x16 (IP0) and x17 (IP1) are intra-procedure-call temporaries. Linker-generated veneers, procedure linkage table code and related call-sequence machinery may use them while transferring control to a destination. Even if a direct call appears not to touch them in one build, separately linked code can legally do so.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Therefore, do not place a value in x16 or x17 and expect it to survive a call. Treat them as caller-saved scratch registers, particularly around indirect calls, shared-library calls and calls whose final placement is decided by the linker.

x18 depends on the operating system ABI

The base AAPCS64 leaves x18 to the platform. A target ABI may dedicate it to inter-procedural state, thread-related data or another system purpose. If the platform has no dedicated role, it may treat x18 as an additional caller-saved register.

That means “x18 is caller-saved” is not a portable AArch64 rule. Arm advises platform-independent hand-written assembly to avoid x18 where practical. Apple platforms reserve x18, and their frame-record rules also require x29 to address a valid frame record. Other operating systems can make different choices. Always read the ABI for the exact target rather than transferring a rule from one operating system to another.

SIMD and floating-point registers: the 64-bit boundary

The SIMD register convention has an easily missed qualification. v0–v7 and v16–v31 are caller-saved. The low 64 bits of v8–v15 are callee-saved, but the upper 64 bits are not guaranteed by the base convention.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Consequently, a 64-bit floating-point value in the low half of v8 can survive a call if the callee follows AAPCS64. A 128-bit vector occupying the whole register cannot be assumed to survive merely because it is in v8–v15. The caller must save the complete value or use another strategy before the call.

Value held across a call Safe base-PCS approach
64-bit integer or pointer Use a callee-saved GPR such as x19–x28, or spill it in the caller
128-bit integer or vector Save the full value; a single v8–v15 register is not sufficient protection
Short-lived temporary Use a caller-saved GPR or SIMD register and consume it before the call

SVE and other extended procedure-call rules

Scalable Vector Extension (SVE) introduces conditional preservation rules that depend on the interface. When a routine passes or returns scalable-vector or predicate arguments, AAPCS64 specifies preservation for z8–z23 and p4–p15 in the applicable procedure-call standard. Those rules are not a blanket replacement for the base SIMD table.

Code using SVE, Scalable Matrix Extension (SME) or another extended PCS must consult the matching current ABI sections. First determine which procedure-call standard the function uses, then apply that standard’s register and state-preservation rules. Applying only the base GPR summary to an SVE or SME interface can produce an apparently correct function that corrupts vector or predicate state.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Leaf, non-leaf and hand-written assembly decisions

Leaf functions

A leaf function makes no calls. It can freely use caller-saved registers for temporaries and may not need a stack frame at all. It still must preserve any callee-saved register that it changes and must leave SP valid on return.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Non-leaf functions

A non-leaf function must protect its return address because a nested BL overwrites x30. It must also preserve every callee-saved register it modifies. A common prologue saves the frame pointer and link register together, allocates local storage, and then saves any additional callee-saved registers used by the body. The epilogue reverses those actions before RET.

Inline and stand-alone assembly

In compiler inline assembly, declare every register and memory effect in the constraints or clobber list. The compiler may keep a live value in a caller-saved register across your assembly block only if the block accurately reports that register as clobbered. In stand-alone assembly, follow the target platform’s ABI, not just the architectural instruction set. An instruction can be architecturally legal while still violating the calling convention expected by the surrounding program.

A practical checklist for debugging a call boundary

  • Identify the exact ABI: base AAPCS64, an operating-system variant, or an extended SVE/SME procedure-call standard.
  • List every value that must remain live after the call.
  • Mark whether each value is in a caller-saved register, a callee-saved register, memory, or a special register such as x18.
  • For each caller-saved value, save it before the call or recompute it afterward.
  • For each callee-saved register your function modifies, save the incoming 64-bit value and restore it before return.
  • Check that a nested call cannot destroy the function’s return address in x30.
  • Verify stack alignment and restoration at every public call boundary.
  • For SIMD values, check the width: only the low 64 bits of v8–v15 are covered by the base guarantee.
  • Check platform documentation before using x18.

Why the split improves performance

Caller- and callee-saved sets distribute save/restore costs according to value lifetime. A temporary used only before a call can live in a caller-saved register with no preservation overhead. A value that remains live through many calls can occupy a callee-saved register, so the caller does not spill it at every call site; a callee pays the save/restore cost only if it actually uses that register.

This is an efficiency trade-off, not a promise about a particular compiler’s register allocation. Compilers may choose a stack slot, a caller-saved register, a callee-saved register or recomputation based on register pressure, code size and optimization goals. Hand-written assembly must honor the ABI contract regardless of which choice a compiler would make.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Common mistakes and their fixes

Assuming x0–x18 are all equivalent

x0–x17 are generally caller-saved, but x16 and x17 have linker-sensitive roles, while x18 is platform-defined. Treating all 19 registers as interchangeable can break only after linking, on a different operating system or in a shared-library build.

Saving every callee-saved register unconditionally

The callee is required to preserve a register only when it changes that register. Saving all of x19–x29 is correct but often wasteful and complicates stack-frame sizing. Save the subset your function actually modifies.

Calling v8–v15 fully preserved

The base rule preserves only each register’s low 64 bits. Save the entire 128-bit value when the upper half is live.

Forgetting that BL overwrites x30

A function that calls another function and then executes RET without first protecting its own link value will usually return to the wrong address. Preserve the link register as part of the non-leaf frame.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Bottom line for AArch64 calls

At a base AAPCS64 call boundary, rely on x19–x29 and SP surviving, provided the callee follows the convention. Assume x0–x17 can change, treat x18 as platform-specific, and never use x16 or x17 for values that must survive a call. For SIMD state, preserve the caller’s full value yourself unless you know that only the low 64-bit portion of v8–v15 is live. Finally, protect x30 whenever your function makes a nested call.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

AAPCS64AArch64Arm assemblycallee-saved registerscaller-saved registerscalling convention

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.