The ARM C calling convention defines how functions should be called in C programs compiled for the ARM architecture. Specifically, it defines which registers need to be preserved across function calls. Understanding these conventions is important for writing efficient and compatible C code for ARM.
In the ARM C calling convention, the following registers need to be preserved by callee functions:
- r4-r8 – General purpose registers
- r10-r11 – General purpose registers
- r13 – Stack pointer (SP)
- r14 – Link register (LR)
This means any C function must save these registers before modifying them and restore them before returning to the caller. The caller function can assume these registers are unchanged after the callee returns.
Why Save Registers?
Saving registers is necessary because C functions will often modify registers as part of their operation. However, the calling function may also be using those registers for its own purposes and expect the values to remain unchanged after the call. By saving registers before modifying them and restoring them after, callee functions can avoid stepping on the toes of their callers.
For example, say function A calls function B. Function A is using r4 to store an important value it needs later. Meanwhile, function B needs to use r4 temporarily as part of its work. By saving r4 before using it and restoring it before returning, function B leaves function A’s r4 untouched.
Which Registers to Save
The ARM C calling convention specifies exactly which registers a callee function must preserve in order to be compatible with caller code. The specific registers are:
- r4-r8 – General purpose registers for caller function
- r10-r11 – General purpose registers for caller function
- r13 – Stack pointer, points to current stack frame
- r14 – Link register, stores return address
Saving these registers allows caller functions to rely on them remaining unchanged across calls. Meanwhile, callee functions are free to use the other general purpose registers r0-r3 and r12 without saving them.
How Registers are Saved
To save the required registers, the callee function will push them onto the stack at the start of the function and pop them back off before returning. This preserves their values in memory during the function’s execution.
A typical function prologue in ARM assembly for saving registers looks like: push {r4-r8, r10-r11, lr} ; Save registers on stack sub sp, sp, #16 ; Allocate stack space for locals
While a typical epilogue for restoring registers is: add sp, sp, #16 ; Deallocate stack space pop {r4-r8, r10-r11, lr} ; Restore registers bx lr ; Return to caller
By bracketing the function this way, the callee can use r4-r8, r10-r11 freely within the function while ensuring the caller’s original values remain untouched.
Caller-Saved vs Callee-Saved Registers
Based on these conventions, ARM general purpose registers can be categorized into two types:
- Caller-saved – r0-r3, r12. The caller must save these if it needs to preserve them.
- Callee-saved – r4-r11, r13-r14. The callee must save these if it modifies them.
This divides the responsibility between caller and callee functions:
- Caller functions must preserve r0-r3, r12 if needed
- Callee functions must preserve r4-r11, r13-r14
These conventions allow both parties to use registers for their own purposes without trampling each other.
Variations Across ARM Architecture Versions
While the main ARM calling convention has remained consistent over time, some variations exist between architecture versions:
- ARMv4 and earlier – r9 is also callee-saved
- ARMv6 and later – r9 is caller-saved
Code written for newer ARM versions can rely on r9 being scratch. But code intending to support older ARMs needs to treat r9 as callee-saved.
Security-Critical Code May Save Additional Registers
For security-critical code, it is sometimes necessary to save additional scratch registers to avoid side channel attacks. For example, cryptographic routines will often save all general purpose registers to clear any registers that may contain sensitive data.
Interoperating with Assembly Code
When writing C functions that need to interface with assembly code, it is essential to follow the standard C calling conventions. Any deviation risks incompatibility with callers or callees.
Likewise, assembly functions meant for C code to call should follow the conventions and save/restore the expected registers. This ensures smooth interoperation between the languages.
Leveraging Conventions for Optimization
Knowing which registers a function can freely modify allows for more efficient register allocation and use. Functions can leverage the caller-saved registers for temporary values and scratch space without needing to preserve them.
Similarly, loops can utilize callee-saved registers for induction variables, counters, etc since their values do not need to be reloaded after each iteration.
Stack Frames and Stack Manipulation
Besides saving registers, functions also manipulate the stack pointer to allow access to stack-allocated local variables. This includes adjusting the stack on function entry, and restoring it on function exit.
The ARM stack grows downwards. A typical function prologue to adjust the stack is: push {…} ; Save registers sub sp, sp, #X ; Allocate stack space
While a typical epilogue is: add sp, sp, #X ; Deallocate stack space pop {…} ; Restore registers
X depends on the required stack space for local variables in each function. Adjusting the stack pointer this way provides each function its own stack frame for automatic variables.
Summary
The ARM C calling convention defines r4-r11, r13-r14 as callee-saved registers which functions must preserve. Knowing which registers need to be saved allows efficient use of the remaining scratch registers.
Following the conventions is vital for compatibility between functions. The conventions create a clear division between caller vs callee responsibility for saving registers.
Consistency with the ARM ABI guarantees C functions in ARM work as expected, provide optimization opportunities, and enable interfacing with assembly code.