Part IIInstruction Set Architectures

Procedure Calling Conventions

August 3, 2026·24 min read·intermediate

When one function calls another, the two functions must agree on where arguments live, where the return value goes, and which registers the called function may destroy. Without such an agreement, a compiler…

When one function calls another, the two functions must agree on where arguments live, where the return value goes, and which registers the called function may destroy. Without such an agreement, a compiler could place the first argument in register X3 while the called function expects it in X0, and the program would silently produce wrong results. The agreement is the calling convention, and it is recorded in the platform’s ABI specification, not in the ISA manual.

This chapter examines four calling conventions in use today: the RISC-V standard (“ilp32d” and “lp64d” ABIs), the ARM AArch64 Procedure Call Standard (AAPCS64), the System V AMD64 ABI (used on Linux, macOS, and most Unix-like systems on x86-64), and the Microsoft x64 ABI (used on Windows). For each convention, the chapter describes the register partition into caller-saved and callee-saved sets, the argument-passing rules, the stack-frame layout, and the mechanics of variadic functions. A side-by-side comparison table at the end draws the four conventions together.

01.Why Conventions Exist

An ISA defines what a processor can do. A calling convention defines what the software agrees to do. The distinction is important. Nothing in the RISC-V hardware prevents a function from using register a0 as a scratch register and returning a value in t3. But if every function follows the convention that arguments arrive in a0a7 and the return value goes back in a0, then any function compiled by any conforming compiler can call any other function, including functions in libraries compiled years earlier.

Calling conventions also interact tightly with the hardware. The partition of registers into caller-saved (scratch) and callee-saved (preserved) sets determines how much spill traffic the prologue and epilogue generate. A convention that marks too many registers as callee-saved forces every function to save and restore registers it may not use. A convention that marks too many as caller-saved forces the caller to save live values before every call. The balance is an empirical optimization that ABI designers tune using instruction-count measurements on representative workloads.

02.RISC-V Calling Convention

The RISC-V calling convention is defined by the RISC-V ELF psABI specification [1]. Two ABIs are common: ilp32d for RV32 with hardware double-precision float, and lp64d for RV64 with hardware double-precision float. The register assignments are the same in both.

Register assignments

RISC-V names its 32 integer registers with both a numeric name (x0–x31) and an ABI name that indicates purpose:

Table 1. RISC-V integer register assignments (lp64d ABI)

RegisterABI nameRoleSaver
x0zeroHardwired zero
x1raReturn addressCaller
x2spStack pointerCallee
x3gpGlobal pointer
x4tpThread pointer
x5–x7t0–t2TemporariesCaller
x8s0/fpSaved / frame pointerCallee
x9s1Saved registerCallee
x10–x11a0–a1Args / return valuesCaller
x12–x17a2–a7ArgumentsCaller
x18–x27s2–s11Saved registersCallee
x28–x31t3–t6TemporariesCaller

The 12 callee-saved registers (s0–s11) give functions generous preserved storage, and sp is preserved across calls as well. The 16 caller-saved registers (ra, t0–t6, a0–a7) give the compiler plenty of scratch space for expression evaluation and argument staging.

Floating-point registers follow a parallel split: fa0–fa7 for arguments and return values, ft0–ft11 for temporaries, and fs0–fs11 for callee-saved state.

Argument passing

Integer and pointer arguments are passed in a0 through a7 (eight registers). Floating-point arguments go in fa0 through fa7. Arguments that do not fit in registers spill to the stack in memory order, starting at the stack pointer. The return value goes in a0 (and a1 for a 128-bit return value on RV64).

Structures of up to two machine words (16 bytes on RV64) are passed in register pairs. Larger structures are passed by reference: the caller allocates space and passes a pointer.

Stack frame layout

The stack grows downward (toward lower addresses). The frame pointer (s0) points to the top of the current frame, and the stack pointer (sp) points to the bottom. A typical prologue saves ra and s0, adjusts sp, and sets s0 to the new sp value:

RISC-V function prologue saving the return address and frame pointer.

Riscv
addi sp, sp, -32 # allocate 32 bytes
sd ra, 24(sp) # save return address
sd s0, 16(sp) # save old frame pointer
addi s0, sp, 32 # set frame pointer

The epilogue reverses the sequence. RISC-V defines no red zone: a signal handler or interrupt may clobber memory below sp at any time.

Variadic functions

For variadic functions (functions declared with ... in C), the convention requires all variadic arguments to be passed in the integer registers a0–a7, even if some arguments are floating-point. The callee discovers argument types through a format string or a count argument, not through the ABI itself. If more than eight arguments are passed, the extras spill to the stack in the usual way.

03.AArch64 Procedure Call Standard (AAPCS64)

The AArch64 Procedure Call Standard is maintained by ARM and defined in the “Procedure Call Standard for the Arm 64-bit Architecture” document [2]. It is used on Linux, Android, macOS on Apple Silicon, and all other AArch64 operating systems except Windows on ARM (which uses its own variant).

Register assignments

Table 2. AArch64 integer register assignments (AAPCS64)

RegisterRoleSaver
X0–X7Arguments / return valuesCaller
X8Indirect result locationCaller
X9–X15TemporariesCaller
X16–X17Intra-procedure-call scratch (IP0, IP1)Caller
X18Platform register (reserved by OS)Special
X19–X28Callee-saved registersCallee
X29Frame pointer (FP)Callee
X30Link register (LR)Caller
SPStack pointerCallee

The 11 callee-saved registers (X19–X28 plus FP) are fewer than RISC-V’s 12, and the 19 caller-saved registers (X0–X17 plus LR) are more. The asymmetry reflects ARM’s observation that most functions are short and do not need many preserved registers.

The SIMD and floating-point registers V0–V31 are split similarly: V0–V7 for arguments and return values, V8–V15 callee-saved (only the lower 64 bits, D8–D15, must be preserved), and V16–V31 caller-saved.

Argument passing

Arguments are classified into “Next General-purpose Register Number” (NGRN) and “Next SIMD and Floating-point Register Number” (NSRN) categories. Integer and pointer arguments fill X0–X7. Floating-point and SIMD arguments fill V0–V7. When registers are exhausted, arguments spill to the stack in natural alignment order.

Structures up to 16 bytes are passed in one or two registers. Larger structures are passed by reference (the caller copies the structure to a temporary location and passes a pointer in X8 if the structure is returned, or on the stack if it is an argument).

Stack frame layout

The stack grows downward, and the stack pointer must be 16-byte aligned at all times. AArch64 hardware enforces this alignment: a misaligned SP triggers an alignment fault. The frame pointer (X29) and link register (X30) are typically saved as a pair at the top of the frame using the STP (store pair) instruction:

AArch64 function prologue using STP to save frame pointer and link register.

Code
STP X29, X30, [SP, #-32]! // push FP and LR, allocate 32 bytes
MOV X29, SP // set new frame pointer

The pre-indexed writeback (!) adjusts SP in the same instruction as the store. The epilogue uses LDP with post-indexed writeback.

Variadic functions

AAPCS64 requires variadic arguments to be passed in the same registers as non-variadic arguments (X0–X7 for integer, V0–V7 for floating-point). The callee typically saves all argument registers to the stack at the start of the function and then walks the argument list through the saved region. The va_list structure records the current position in both the general-purpose and SIMD register save areas.

04.System V AMD64 ABI

The System V AMD64 ABI is the standard calling convention on Linux, macOS, FreeBSD, and most other Unix-like systems running on x86-64. It was designed by the x86-64 ABI working group and is maintained as a supplement to the System V ABI [3].

Register assignments

Table 3. System V AMD64 ABI integer register assignments

RegisterRoleSaver
RAXReturn value, syscall numberCaller
RBXCallee-savedCallee
RCX4th integer argumentCaller
RDX3rd integer argument, 2nd returnCaller
RSI2nd integer argumentCaller
RDI1st integer argumentCaller
RBPFrame pointer (optional)Callee
RSPStack pointerCallee
R85th integer argumentCaller
R96th integer argumentCaller
R10Temporary, static chain pointerCaller
R11TemporaryCaller
R12–R15Callee-savedCallee

The convention passes the first six integer arguments in registers (RDI, RSI, RDX, RCX, R8, R9), a design driven by the observation that the vast majority of C functions take six or fewer arguments. Floating-point arguments go in XMM0–XMM7 (eight registers). The return value goes in RAX (and RDX for 128-bit returns). Floating-point returns use XMM0 (and XMM1 for 128-bit floating-point returns).

Only six integer registers are callee-saved (RBX, RBP, R12–R15), the fewest of any of the four conventions discussed in this chapter. The aggressive caller-saved policy minimizes prologue and epilogue cost for short functions at the expense of more spills around call sites in long functions with many live variables.

The red zone

The System V AMD64 ABI defines a 128-byte red zone below the current stack pointer. A leaf function (one that makes no function calls) may use this region for local variables without adjusting RSP. The red zone saves the sub rsp, N / add rsp, N pair in the prologue and epilogue of leaf functions, which are the majority of functions in a typical program.

The red zone is safe because signal handlers and interrupts on Unix-like systems adjust the stack pointer before writing below it. Kernel code and interrupt handlers must not use the red zone because a nested interrupt could clobber it.

Stack alignment

The stack must be 16-byte aligned at the point of a CALL instruction. Because CALL pushes an 8-byte return address, the stack is 16-byte aligned minus 8 at function entry. The prologue must adjust the stack to restore 16-byte alignment before any SSE memory operations, which fault on misaligned 128-bit accesses.

Variadic functions

The System V ABI passes variadic arguments in the same registers as fixed arguments (RDI, RSI, RDX, RCX, R8, R9 for integer, XMM0– XMM7 for floating-point). The callee must save all potentially used argument registers to a “register save area” on the stack at function entry. The register AL (the low byte of RAX) is set by the caller to the number of XMM registers used for variadic floating-point arguments, allowing the callee to skip saving unused XMM registers.

05.Microsoft x64 ABI

The Microsoft x64 ABI is the calling convention on 64-bit Windows. It differs from the System V AMD64 ABI in several significant ways, and code compiled for one convention cannot directly call code compiled for the other without an explicit thunk or wrapper.

Register assignments

Table 4. Microsoft x64 ABI integer register assignments

RegisterRoleSaver
RAXReturn valueCaller
RBXCallee-savedCallee
RCX1st integer argumentCaller
RDX2nd integer argumentCaller
RSICallee-savedCallee
RDICallee-savedCallee
RBPFrame pointer (optional)Callee
RSPStack pointerCallee
R83rd integer argumentCaller
R94th integer argumentCaller
R10–R11TemporariesCaller
R12–R15Callee-savedCallee

The first four integer arguments go in RCX, RDX, R8, and R9 (not RDI, RSI, RDX, RCX as in System V). Only four register arguments are provided, compared to six on System V. Floating-point arguments go in XMM0–XMM3 (four registers, compared to eight on System V). The 5th and subsequent arguments go on the stack.

The Microsoft convention preserves RSI and RDI as callee-saved, while System V treats them as caller-saved argument registers. This difference means that a function compiled for one convention cannot safely call a function compiled for the other without saving and restoring the disputed registers.

Shadow space

The most distinctive feature of the Microsoft x64 ABI is the shadow space (also called “home space” or “register parameter area”). The caller must allocate 32 bytes (four 8-byte slots) above the return address on the stack before every call, regardless of the number of arguments. The callee may use this space to spill the four register arguments without allocating additional stack space.

The shadow space simplifies the implementation of variadic functions and structured exception handling on Windows. It also provides a uniform location for debuggers to find argument values. The cost is 32 bytes of stack consumed by every function call, even for functions that take zero arguments.

Stack alignment

The stack must be 16-byte aligned before the CALL instruction, identical to the System V requirement. After the call, the 8-byte return address plus the 32-byte shadow space puts the stack at RSP + 40 at function entry, which is 16-byte aligned minus 8. The function prologue must adjust to restore alignment.

Variadic functions

The Microsoft x64 ABI passes variadic arguments in the same four registers as fixed arguments (RCX, RDX, R8, and R9), with the fifth and later arguments on the stack. One rule is specific to variadic calls. A floating-point argument passed to a variadic function must be placed in both the XMM register and the corresponding integer register, because the callee has no prototype for the variadic part of the list and cannot know which register class to read. A function such as printf therefore finds a double third argument in R8 as well as in XMM2.

The callee implements va_start by spilling RCX, RDX, R8, and R9 into the shadow space that its caller already allocated. Because the shadow space sits immediately below the stack arguments, the spilled registers and the stack arguments form one contiguous array, and va_list is a single pointer that walks it. That is simpler than the System V arrangement, which needs a dedicated register save area and a va_list that tracks the general-purpose and SSE save areas separately.

Exception handling and unwind tables

Windows structured exception handling (SEH) requires every non-leaf function to have an entry in the .pdata section that describes its prologue and stack frame layout. The unwind information in the .xdata section allows the exception dispatcher to walk the stack without frame pointers. The Microsoft ABI therefore makes frame pointers optional: if the function has unwind data, the debugger and exception handler can reconstruct the stack without RBP.

On Unix, the equivalent mechanism is the DWARF .eh_frame section, used by both System V AMD64 and the RISC-V and AArch64 ABIs.

06.Side-by-Side Comparison

Table 5. Calling convention comparison across four ABIs

FeatureRISC-VAAPCS64SysV AMD64MS x64
Int arg regsa0–a7 (8)X0–X7 (8)6 regs4 regs
FP arg regsfa0–fa7 (8)V0–V7 (8)XMM0–7 (8)XMM0–3 (4)
Int returna0–a1X0–X1RAX, RDXRAX
Callee-saved int11+FP10+FP5+RBP7+RBP
Red zoneNoneNone128 bytesNone
Shadow spaceNoneNoneNone32 bytes
Stack alignment16 bytes16 bytes16 bytes16 bytes
Link registerra (x1)LR (X30)StackStack
Frame pointers0 (x8)X29RBPRBP

Several patterns emerge from the table. All four conventions align the stack to 16 bytes, reflecting the needs of SIMD instructions that require aligned memory operands. RISC-V and AArch64 use a dedicated link register to hold the return address, avoiding the stack push that x86-64 requires on every CALL. The Microsoft x64 ABI is the most conservative, with only four register arguments and mandatory shadow space, but this conservatism simplifies Windows exception handling.

07.Concrete Assembly Examples

To make the conventions tangible, consider the C function:

A simple function that adds two integers and returns the result.

C
long add_two(long a, long b) {
return a + b;
}

The following subsections show the compiled assembly for each ISA.

RISC-V

RISC-V assembly for add_two. Arguments arrive in a0 and a1, result returns in a0. A leaf function with no stack frame.

Riscv
add_two:
add a0, a0, a1
ret # pseudo-instruction for jalr x0, ra, 0

Two instructions, no stack manipulation. The function is a leaf, so no prologue or epilogue is needed.

AArch64

AArch64 assembly for add_two. Arguments in X0 and X1, result in X0.

Code
add_two:
ADD X0, X0, X1
RET // branches to LR (X30)

Identical structure to the RISC-V version. The link register holds the return address.

x86-64 (System V)

x86-64 assembly for add_two under the System V ABI. First argument in RDI, second in RSI, result in RAX.

Code
add_two:
lea rax, [rdi + rsi]
ret

The LEA (load effective address) instruction computes the sum without touching the flags register, a common idiom when the result must go into a register different from either source. The RET instruction pops the return address from the stack and jumps to it.

x86-64 (Microsoft)

x86-64 assembly for add_two under the Microsoft x64 ABI. First argument in RCX, second in RDX, result in RAX.

Code
add_two:
lea rax, [rcx + rdx]
ret

The structure is the same as the System V version, but the argument registers differ. RCX holds the first argument instead of RDI, and RDX holds the second instead of RSI.

08.A Non-Leaf Function Example

A non-leaf function must save the return address and any callee-saved registers it uses. Consider:

A non-leaf function that calls add_two and multiplies the result by a saved value.

C
long scale(long a, long b, long factor) {
long sum = add_two(a, b);
return sum * factor;
}

RISC-V version

RISC-V assembly for scale. Must save ra and s0 because the function calls add_two.

Riscv
scale: addi sp, sp, -16 # allocate stack frame sd ra, 8(sp) # save return address sd s0, 0(sp) # save s0 mv s0, a2 # factor -> s0 (callee-saved) call add_two # sum = add_two(a, b) mul a0, a0, s0 # a0 = sum * factor ld ra, 8(sp) # restore return address ld s0, 0(sp) # restore s0 addi sp, sp, 16 # deallocate frame ret

The function saves ra because the call to add_two will overwrite it. It saves s0 because it uses s0 to hold factor across the call. The third argument (a2) must be moved to a callee-saved register because add_two is free to clobber a2.

AArch64 version

AArch64 assembly for scale. STP saves the frame pointer and link register in a single instruction.

Code
scale:
STP X29, X30, [SP, #-32]! // save FP and LR
MOV X29, SP
STR X19, [SP, #16] // save callee-saved X19
MOV X19, X2 // factor -> X19 (callee-saved)
BL add_two // sum = add_two(X0, X1)
MUL X0, X0, X19 // X0 = sum * factor
LDR X19, [SP, #16] // restore X19
LDP X29, X30, [SP], #32 // restore FP, LR; deallocate
RET

The structure mirrors the RISC-V version. STP and LDP save and restore two registers in a single instruction, which is more compact than the two separate stores on RISC-V.

x86-64 System V version

x86-64 System V assembly for scale. RBX is used as the callee-saved register for factor.

Code
scale:
push rbx ; save callee-saved rbx
mov rbx, rdx ; factor -> rbx
call add_two ; sum = add_two(rdi, rsi)
imul rax, rbx ; rax = sum * factor
pop rbx ; restore rbx
ret

The PUSH and POP instructions handle the callee-saved register. The return address is pushed by CALL and popped by RET, both implicitly.

09.Stack Unwinding and Exception Handling

When a C++ exception is thrown, the runtime must walk backward through the call stack, invoking destructors for local objects in each frame, until it finds a matching catch handler. This process is stack unwinding, and it requires knowing the layout of each frame on the stack.

Frame pointers vs. unwind tables

The traditional approach is a frame-pointer chain: each function saves the caller’s frame pointer and sets its own frame pointer to the current stack position. The debugger or exception handler follows the chain of saved frame pointers to reconstruct the call stack.

Modern compilers often omit the frame pointer (-fomit-frame-pointer on GCC and Clang) because it frees one register for general use (RBP on x86-64, X29 on AArch64, s0 on RISC-V). Without frame pointers, the runtime relies on unwind tables stored in the binary’s metadata sections.

On Unix-like systems, the DWARF .eh_frame section describes each function’s prologue instructions and the location of each saved register relative to the canonical frame address (CFA). On Windows, the .pdata and .xdata sections serve the same purpose for SEH.

The cost of unwinding

Stack unwinding is rare (exceptions are, by design, exceptional), so the cost matters only when it happens. The unwind tables add to the binary size, typically 5–10 percent of the text section. On x86-64, the variable-length instruction encoding makes unwind-table generation more complex than on fixed-length ISAs because the table must track the stack-pointer adjustments precisely across instructions of different lengths. AArch64 and RISC-V benefit from fixed-length instructions: the unwind machinery can step through the prologue instruction by instruction with a uniform stride.

10.Worked Examples

11.Exercises

References

  1. [1](2024). “RISC-V.”
  2. [2](2024). “Procedure Call Standard for the Arm.”
  3. [3]Matz, Michael and Hubi\v c (2024). “System V.”
Book mode
computer-architectureinstruction-set-architectures
Was this helpful?