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.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchA 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.
#1 Best Overall
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.
// 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.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →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.
Recommended Free Tools
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.
Rank #4
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.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.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →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.
Best Value
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–v15are 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.
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.
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.
Quick Recap
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.

