The calling convention is the set of rules that lets one function call another when the two were compiled separately: which registers carry the arguments, where the result comes back, which registers a function may overwrite and which it must leave as it found them, and how the stack is laid out. It is the central part of a larger agreement, the application binary interface (ABI), which also fixes the size and alignment of every data type. The hardware enforces none of it; software works only because every compiler, library and hand-written routine obeys the same rules. This article explains the RISC-V ilp32 ABI, the one for 32-bit integer code, using real compiled listings: a small file of test functions, a linked-list program and the library routine __mulsi3.

18 min read | Beginner to Intermediate

What You'll Learn

  • Explain what an ABI is, how it differs from an API, and why separately compiled code needs one
  • Describe how arguments and results travel in registers and on the stack, including 64-bit values and structures
  • Distinguish caller-saved from callee-saved registers and explain the cost each choice avoids
  • State the ilp32 type sizes and the 16-byte stack alignment rule, and what breaks when code ignores them

Why Code Needs a Contract

When a linked-list program calls __umodsi3 to compute a remainder, the two pieces of code have never met. list.c was compiled today; __umodsi3 was compiled years earlier by the people who built the toolchain, and it sits in the library file libgcc.a. The compiler of list.c cannot look inside __umodsi3, and __umodsi3 knows nothing about its callers. Still, the program must put the two numbers where __umodsi3 expects them, and find the remainder where __umodsi3 leaves it, and it must be sure that the call has not destroyed values it still needs.

To the processor, registers x1 to x31 are all the same (Registers and the Register File shows why). Any choice would work, as long as both sides make the same one. The application binary interface (ABI) is that shared choice, written down. For RISC-V it is a public document, the RISC-V ELF psABI (processor-specific ABI), which GCC, LLVM, operating systems and library vendors all follow.

An ABI is different from an API (application programming interface). The API is the source-level contract: a function called __umodsi3 takes two unsigned integers and returns one. The ABI is the binary-level contract: the first integer arrives in register a0, the second in a1, the result goes back in a0, and an unsigned integer is 4 bytes. Two programs can share an API and still be unable to work together if they were built for different ABIs. The ABI covers:

  • the calling convention: argument and result registers, preserved registers, stack layout;
  • the size and alignment of every C type, and therefore the layout of every structure;
  • the object file format (ELF), the relocation types and the names of symbols, covered in Assembling, Linking and Loading.

GCC's option -mabi=ilp32 selects the ABI. The listings in this article come from a small file, abi.c, from that linked-list program, and from the library.

The Register Roles

The calling convention gives each register a role and an ABI name, which disassemblers print instead of the x numbers. The full table of names is in Registers and the Register File; what matters here is the last column, whether a register keeps its value across a call:

ABI nameRegistersRoleKept across a call?
zerox0always zero(constant)
rax1return addressno
spx2stack pointeryes
gp, tpx3, x4global pointer, thread pointer(not allocated by the compiler)
t0-t6x5-x7, x28-x31temporariesno (caller-saved)
s0-s11x8, x9, x18-x27saved registers; s0 is also the frame pointeryes (callee-saved)
a0-a7x10-x17arguments; a0 and a1 also resultsno (caller-saved)

The linked-list program's call to __umodsi3 uses exactly these roles. main copies the random number into a0 and the loop counter into a1, calls, and uses a0 afterwards as the remainder:

  80:  mv    a1,s0            # second argument: i + 1 (kept in s0)
  84:  mv    a0,s2            # first argument: the random number (in s2)
  88:  auipc ra,0x0
  8c:  jalr  348(ra)          # call __umodsi3
  90:  slli  a0,a0,0x2        # the result came back in a0: index x 4

Passing Arguments

The first eight integer or pointer arguments go in a0 to a7, in order. Registers are used because they are fast and because most functions have few arguments; passing them in memory would add a store in the caller and a load in the callee for every argument. When a function has more than eight, the rest go on the stack, in the caller's frame, starting at the address in sp at the moment of the call. abi.c contains a function with ten arguments and a function that calls it:


u32 add10(u32 a, u32 b, u32 c, u32 d, u32 e, u32 f, u32 g, u32 h, u32 i, u32 j);
u32 call10(u32 x) { return add10(x, 2, 3, 4, 5, 6, 7, 8, 9, 10) + 1; }

The compiled call10 builds a 32-byte frame, stores the ninth and tenth arguments at its bottom, loads the other seven constants into a1 to a7 (a0 already holds x, its own first argument), saves ra and calls:

  70:  addi  sp,sp,-32
  74:  li    a5,10
  78:  sw    a5,4(sp)         # argument 10 at 4(sp)
  7c:  li    a5,9
  80:  sw    a5,0(sp)         # argument 9 at 0(sp)
  84:  li    a7,8             # arguments 8 down to 2 in a7 ... a1
  ...
  9c:  li    a1,2
  a0:  sw    ra,28(sp)        # call10 calls, so it must save ra
  a4:  auipc ra,0x0
  a8:  jalr  ra               # call add10
  ac:  lw    ra,28(sp)
  b0:  addi  a0,a0,1          # the result of add10 is in a0
call10 calling add10 with ten arguments: a0 holds x and a1 to a7 hold 2 to 8; arguments 9 and 10 are stored at sp+0 and sp+4 of the 32-byte frame of call10, which also holds ra at sp+28; add10 reads them with lw t1,0(sp) and lw t3,4(sp)

On the other side, add10 finds the two extra arguments at the same addresses. It has no frame of its own, so its sp is still the caller's, and it reads them with lw t1,0(sp) and lw t3,4(sp). Stack arguments cost one store and one load each, which is why APIs that are called often keep their argument lists short.

Quick Check

Q: A function takes 12 integer arguments. Where does the caller put arguments 9 to 12?

Show Answer

The first eight go in a0 to a7. Arguments 9, 10, 11 and 12 go on the stack at 0(sp), 4(sp), 8(sp) and 12(sp), measured from sp at the moment of the call, in the caller's frame. The callee reads them at the same offsets from its own sp on entry, or at larger offsets if it has already made its own frame.

Results, 64-Bit Values and Structures

A result of up to 32 bits comes back in a0. A 64-bit value on a 32-bit machine needs two registers, a register pair: the low half in the lower-numbered register. add64 adds two unsigned long long values, x in a0 (low) and a1 (high), y in a2 and a3, and returns the sum in a0 and a1:

  30:  mv    a5,a0
  34:  add   a0,a0,a2         # low halves
  38:  sltu  a5,a0,a5         # carry = 1 if the low sum wrapped around
  3c:  add   a1,a1,a3         # high halves
  40:  add   a1,a5,a1         # plus the carry
  44:  ret

RISC-V has no carry flag, so the carry is computed with sltu: an unsigned sum that is smaller than one of its inputs has wrapped around. Arithmetic, Logic and Immediates explains this way of working without flags.

Structures follow a size rule. A structure of up to 8 bytes, two registers' worth, is passed and returned in registers. make_pair returns a structure of two 32-bit fields built from its two arguments, and since a and b already sit in a0 and a1, where the result must go, the function has nothing to do but return (GCC also reserves and frees 16 bytes of stack that it never uses, a small missed optimization). A larger structure is returned through memory: the caller reserves space and passes its address as a hidden first argument in a0, so the visible arguments move up by one register. make_big, which returns a 24-byte structure, receives its argument in a1 and writes six words through a0:

  54:  sw    a1,0(a0)         # a0 = where the caller wants the result
  58:  sw    a1,4(a0)         # a1 = the argument a
  ...
  6c:  ret

Who Saves Which Register

A call is a promise in both directions. The caller expects some registers to have the same values after the call; the callee wants registers to work with. The convention splits the registers into two groups so that each side knows its duties:

  • Caller-saved registers (t0-t6, a0-a7 and ra) may be overwritten by any call. A caller that needs a value in one of them after the call must save it itself, before the call, and restore it after.
  • Callee-saved registers (s0-s11 and sp) must have the same value when a function returns as when it was called. A function that wants to use one must save its old value first, normally in its prologue, and restore it in its epilogue.

Neither group is better; they serve different kinds of value. A value that is needed only between calls belongs in a caller-saved register and costs nothing to keep. A value that must survive many calls belongs in a callee-saved register, which costs one save and one restore per call of the function that uses it, however many calls it makes in between.

__mulsi3, the routine that does every multiplication in a program compiled without the M extension, is the first kind. It is a leaf and uses only a0 to a3, all caller-saved, so it needs no stack and saves nothing:

 198:  mv    a2,a0            # a2 = multiplicand
 19c:  li    a0,0             # a0 = product
 1a0:  andi  a3,a1,1          # lowest bit of the multiplier
 1a4:  beqz  a3,1ac
 1a8:  add   a0,a0,a2         # bit set: add the shifted multiplicand
 1ac:  srli  a1,a1,0x1
 1b0:  slli  a2,a2,0x1
 1b4:  bnez  a1,1a0
 1b8:  ret

It is called for each of the 1,728 multiplications of the 12 x 12 matrix product, so any saving and restoring it did would be repeated 1,728 times. Because it touches only caller-saved registers, the convention makes it free.

The main of the linked-list program is the second kind. Its loop counter, the pointers into the arrays and the random seed must survive 511 calls to __umodsi3, so the compiler keeps them in s0 to s4. Its prologue saves the old values of those five registers and ra once, and its epilogue restores them once:

 174:  lw    ra,28(sp)
 178:  lw    s0,24(sp)
 17c:  lw    s1,20(sp)
 180:  lw    s2,16(sp)
 184:  lw    s3,12(sp)
 188:  lw    s4,8(sp)
 18c:  addi  sp,sp,32
 190:  ret

The 32-byte frame these registers occupy is drawn in How a Program Runs: Loops, Calls and the Stack. Had the same five values been in caller-saved registers, main would have to store and reload them around each of the 511 calls; the practice exercise below puts a number on the difference.

Quick Check

Q: In a matrix program, the running sum lives in s2: after each call the code does add s2,s2,a0. Why did the compiler choose s2 rather than a temporary such as t2, which would need no saving in the prologue?

Show Answer

The sum must survive every call to __mulsi3. t2 is caller-saved, so the compiler would have to assume that each call destroys it, and store and reload the sum around every one of the calls. s2 is callee-saved: __mulsi3 promises to keep it (it does not even touch it), so main pays one save and one restore of s2 in total.

Stack Alignment

The convention also says that sp is always a multiple of 16 bytes at a call. GCC records this promise in every assembly file it writes for the target, as the directive .attribute stack_align, 16, and every frame in these listings is a multiple of 16, such as 16, 32 or 48 bytes. call10 needs only 12 bytes (two argument words and ra) but gets 32, because GCC rounded the outgoing-argument area and the save area up to 16 bytes each.

The reason is the largest type. In ilp32, long double is 16 bytes with an alignment of 16, and any function may need to keep one on its stack. If every function keeps sp aligned to 16, each function can place such values at fixed offsets from sp without checking; if one function misaligns sp, every function it calls inherits the problem. The rule costs a few unused bytes per frame, the 8 bytes of padding in the linked-list program's frame for example, and buys code that never has to realign the stack at run time. Hand-written assembly that adjusts sp by 4 or 8 bytes breaks the rule, and the failure appears somewhere else, later, in code that did nothing wrong.

Data Types: What ilp32 Means

The name of the ABI describes its integer types: int, long and pointers are 32 bits. The experiment compiled a file that stores the sizeof and alignment of each C type, once for RISC-V ilp32 and once for the x86-64 host computer, and read the values back from the object files:

C typeRISC-V ilp32: size, alignment (bytes)x86-64 host: size, alignment (bytes)
char1, 11, 1
short2, 22, 2
int4, 44, 4
long4, 48, 8
long long8, 88, 8
void * (any pointer)4, 48, 8
float4, 44, 4
double8, 88, 8
long double16, 1616, 16

The two ABIs agree on everything except long and pointers. That one difference is behind many porting bugs: code that stores a pointer in an int works on ilp32 and fails on a 64-bit system, and a structure containing a long has a different size and different field offsets on the two machines, so a binary file or a network message written by one is misread by the other. Type sizes also decide the layout of structures, with padding inserted so that each field is aligned to its size, as Loads, Stores and Addressing shows. Embedded code often avoids the question by defining its own u32 type for every 32-bit value.

RISC-V defines a family of ABIs, chosen together with the instruction set. ilp32 passes everything in integer registers; ilp32f and ilp32d pass float or double values in floating-point registers and need a core with those registers; lp64 and its variants are the 64-bit ABIs, where long and pointers are 8 bytes. Code compiled for different ABIs cannot be mixed, which is why the toolchain keeps one copy of each library per combination: an rv32i program with the ilp32 ABI links the libgcc.a from the directory rv32i/ilp32 of the installation.

Where Does This Matter?

Every interface between separately built software is an ABI boundary: an application calling the operating system, firmware calling a vendor's driver library delivered only as a binary, Rust or C++ code calling C, a debugger reading a stack. Operating systems promise to keep their ABI stable for years so that old programs keep running, and a silicon vendor that ships a core with a new floating-point unit must decide whether its customers' existing ilp32 libraries still work. Interrupt handlers are the extreme case: an interrupt can arrive between any two instructions, so a handler cannot rely on caller-saved registers being free and must save every register it uses.

Common Mistakes

Mistake #1: Expecting a temporary or argument register to survive a call

t0-t6, a0-a7 and ra are caller-saved: any call may overwrite them. Hand-written assembly that keeps a value in t0 across a call works as long as the callee happens not to use t0, and breaks when the callee changes. Keep such values in s registers, or save them around the call.

Mistake #2: Breaking the callee's promises in hand-written code

An assembly routine that uses s1 without saving it, or moves sp by a multiple of 4 that is not a multiple of 16, satisfies its own tests and corrupts its callers. The failure shows up in the caller, often much later, which makes it one of the hardest bugs to trace. Follow the convention in every routine that is called from or calls compiled code.

Mistake #3: Assuming the sizes of long and pointers

On ilp32 long and pointers are 4 bytes; on the x86-64 host they are 8. Code that mixes them, or data structures shared between a 32-bit core and a 64-bit host, must use fixed-width types such as uint32_t and check the layout, rather than assume the sizes of a different machine.

Practice Exercise

Challenge: What the Convention Costs in list

A run of the linked-list program makes 2,565 stores and 9,732 loads. From the listing, the stores are: 1 per element when order is filled (512 elements); 2 per swap in the shuffle (511 swaps); 1,024 for the next and val fields of the 512 nodes; the prologue's saves; and 1 store of the result by the start-up code. The loads are: 2 per node per walk (512 nodes, 8 walks); 2 per swap; 512 loads of order entries while the nodes are linked; and the epilogue's restores. Check that both totals add up, find how many of the 12,297 memory accesses the calling convention causes, and estimate how many there would be if the five values in s0-s4 had to be stored and reloaded around each of the 511 calls to __umodsi3 instead.

Hint

The prologue and epilogue each touch six registers: ra and s0-s4. "Stored and reloaded around each call" means one store and one load per value per call.

Solution

Stores: 512 + 2 x 511 + 1,024 + 6 + 1 = 512 + 1,022 + 1,031 = 2,565. Loads: 2 x 512 x 8 + 2 x 511 + 512 + 6 = 8,192 + 1,022 + 512 + 6 = 9,732. Together 2,565 + 9,732 = 12,297 accesses, of which only 6 + 6 = 12 come from the calling convention, about 0.1 percent. If the five values lived in caller-saved registers, each of the 511 calls would add 5 stores and 5 loads: 2 x 5 x 511 = 5,110 extra accesses, more than 400 times as many. Keeping long-lived values in callee-saved registers is what makes this program's calls nearly free.

Summary

  • An ABI is the binary-level contract (registers, stack, type sizes, object format) that lets separately compiled code work together; -mabi=ilp32 selects the one used here.
  • The first eight arguments travel in a0-a7 and the result in a0 (a0 and a1 for 64-bit values and small structures); further arguments go on the stack at 0(sp), 4(sp) and so on, as call10 shows.
  • Caller-saved registers (t, a, ra) suit short-lived values, so leaf routines like __mulsi3 need no stack; callee-saved registers (s0-s11) suit values that survive calls, saved once per function.
  • sp stays a multiple of 16, so frames are 16, 32 or 48 bytes even when less is needed; this lets 16-byte types sit on any stack without realignment.
  • In ilp32 int, long and pointers are 4 bytes; on x86-64 long and pointers are 8, the source of many porting and data-layout bugs.
Did you find what you were looking for?

Quiz

No quiz questions available for this article.