C calling convention

From llvm-mos
  • A, X, Y, C, N, V, Z and RS1 to RS9 (RC2 to RC19) are caller-saved. A function may freely overwrite any of these, and the function's callers have to just deal with it.
  • PC, S, D, I, RS0 (RC0 and RC1), and RS10 to RS15 (RC20 to RC31) are callee-saved. A function can use them freely, but before it returns it has to put them back exactly the way it found them, and the function's callers can rely on this behavior.
  • Arguments are assigned from left to right. The return value is assigned in the same manner as the first argument.
  • The bytes composing numeric arguments are passed individually in A, then X, then RC2 to RC15.
  • Pointers are passed through RS1 through RS7.
  • If no registers remain available, values are passed through the soft stack.
  • Aggregate types (structs, arrays, etc.) 4 bytes or smaller are split into their individual value types, and each is passed individually. Such types are also returned by value.
  • Aggregate types larger than 4 bytes are passed by pointer. The pointer is managed entirely by the caller, and may or may not be on the soft stack. The callee is free to write to the memory; the caller must consider the memory overwritten by the call. Such types are returned by a pointer passed as an implicit first argument. The resulting function then returns void.
  • Variable arguments (those within the ellipses of the argument list) are passed through the stack. Named arguments before the variable arguments are passed as usual: first in registers, then stack. Note that the variable argument and regular calling convention differ; thus, variable argument functions must only be called if prototyped. The C standard requires this, but many platforms do not; their variable argument and regular calling conventions are identical. A notable exception is Apple ARM64.

For insight into the design of performant calling conventions, see the following work by Davidson and Whalley. By their convention, this plaftorm uses the "smarter hybrid" method, since LLVM performs both shrink wrapping and caller save-restore placement optimizations, while using both callee-saved and caller-saved registers when appropriate.

Our calling convention is roughly based on RISC-V, suggested after a discussion with one of their working group members.

Methods for Saving and Restoring Register Values across Function Calls: Software--Practice and Experience Vol 21(2), 149-165 (February 1991)

Examples[edit | edit source]

The table below illustrates how function arguments are distributed over the (imaginary) registers.

Function A X rc2 rc3 rc4 rc5 Output Comment
char f(int a) a a A Numeric args passed...
long f(long a, int b) a a a a b b A, X, rc2-3 ...by value. Even very...
void f(int64_t a) a a a a a a ...long ones (incl. rc6-7)
int * f(void *a) a a rc2-3 Pointers only through...
int f(int a, int b, void *c) a a b b c c A, X ...imaginary register pairs
int f(void *a, char b, int c) b c a a c A, X Assign from left to right
void f(struct div_t a) a a a a By members if sizeof ≤ 4
void f(struct ldiv_t a) a a By pointer if sizeof > 4
div_t f(void *a) a a A, X, rc2-3 Return members
ldiv_t f(void *a) 👻 👻 a a Return via write through implicit 1st arg pointer