Procedure Call Standard

The Arm architecture places few restrictions on how general purpose registers are used. To recap, integer registers and floating-point registers are general purpose registers.However, if you want your code to interact with code that is written by someone else, or with code that is produced by a compiler, then you need to agree rules for register usage. For the Arm architecture, these rules are called the Procedure Call Standard, or PCS.

The PCS specifies:

  • Which registers are used to pass arguments into the function.
  • Which registers are used to return a value to the function doing the calling, known as the caller.
  • Which registers the function being called, which is known as the callee, can corrupt.
  • Which registers the callee cannot corrupt.

Consider a function foo(), being called from main():

The PCS says that the first argument is passed in X0, the second argument in X1, and so on up to X7. Any further arguments are passed on the stack. Our function, foo(), takes two arguments: a and b. Therefore, a will be in W0 and b will be in W1.

Why W and not X? Because the arguments are a 32-bit type, and therefore we only need a W register.

Note: In C++, X0 is used to pass the implicit this pointer that points to the called function.

Next, the PCS defines which registers can be corrupted, and which registers cannot be corrupted. If a register can be corrupted, then the called function can overwrite without needing to restore, as this table of PCS register rules shows:

X0-X7 X8-X15 X16-X23 X24-X30
Parameter and Result Registers
(X0-X7)
XR (X8) IP0 (X16) Callee-saved Registers
(X24-X28)
Corruptible Registers
(X9-X15)
IP1 (X17)
PR (X18)
Callee-saved Registers
(X19-X23)
FP (X29)
LR (X30)

For example, the function foo() can use registers X0 to X15 without needing to preserve their values. However, if  foo() wants to use X19 to X28 it must save them to stack first, and then restore from the stack before returning.

Some registers have special significance in the PCS:

  • XR - This is an indirect result register. If foo() returned a struct, then the memory for struct would be allocated by the caller, main() in the earlier example. XR is a pointer to the memory allocated by the caller for returning the struct.
  • IP0 and IP1 - These registers are intra-procedure-call corruptible registers. These registers can be corrupted between the time that the function is called and the time that it arrives at the first instruction in the function. These registers are used by linkers to insert veneers between the caller and callee. Veneers are small pieces of code. The most common example is for branch range extension. The branch instruction in A64 has a limited range. If the target is beyond that range, then the linker needs to generate a veneer to extend the range of the branch.
  • FP - Frame pointer.
  • LR - X30 is the link register (LR) for function calls.

Note: We previously introduced the ALU flags, which are used for conditional branches and conditional selects. The PCS says that the ALU flags do not need to be preserved across a function call.

There is a similar set of rules for the floating-point registers:

D0-D7 D8-D15 D16-D23 D24-D31
Parameter and Result Registers
(D0-D7)
Callee-saved Registers
(D8-D15)
Callee-saved Registers
(D16-D31)
Previous Next