Functions#
A function in assembly is a label you can call,
followed by code that ends with ret. The contract between
caller and callee, the calling convention, fixes which
registers carry arguments, which carries the return value, and
which the callee must preserve. Three conventions cover almost
every modern host: System V AMD64 (Linux / macOS / BSD on
x86-64), **Microsoft x64* (Windows on x86-64), and AAPCS64
(everything on ARM64).
/variables`. For
call / ret flow, see Control flow.
SysV AMD64#
The Linux / macOS / BSD calling convention on x86-64.
Integer / pointer args 1-6:
rdi rsi rdx rcx r8 r9.Floating args 1-8:
xmm0..xmm7.Return:
rax(integer / pointer) orxmm0(float). 128-bit return:rax:rdxorxmm0:xmm1.Caller-saved:
rax rcx rdx rsi rdi r8 r9 r10 r11.Callee-saved:
rbx rbp r12 r13 r14 r15.Stack alignment:
rspmust be16-byte aligned at everycall(which means8mod16on entry after thecallpushes the return address).Variadic functions:
raxcarries the count of vector registers used (zero if none).
Microsoft x64#
The Windows calling convention on x86-64.
Args 1-4:
rcx rdx r8 r9.Floating args 1-4:
xmm0..xmm3.Return:
rax(integer) orxmm0(float).Caller-saved:
rax rcx rdx r8 r9 r10 r11.Callee-saved:
rbx rbp rdi rsi r12 r13 r14 r15.Shadow space: caller reserves 32 bytes of stack the callee may freely use for its first four args (even when passed in registers).
AAPCS64 (ARM64)#
The ARM64 calling convention.
Args 1-8:
x0..x7(also for returns).Floating args 1-8:
v0..v7.Indirect result:
x8.Callee-saved:
x19..x28, plusx29(fp) andx30(lr) by convention.Caller-saved:
x0..x18.Stack:
spmust be16-byte aligned at everybl.
Prologue and epilogue#
x86-64.
; A function that uses one local variable (8 bytes) and
; needs the standard frame pointer.
my_fn:
push rbp ; save caller's frame
mov rbp, rsp ; new frame
sub rsp, 16 ; locals + alignment
; rsp is now 16-byte aligned
; ... body, using [rbp - 8] for the local ...
leave ; mov rsp, rbp; pop rbp
ret
A leaf function (no calls inside) can omit the frame pointer
and skip the push rbp / mov rbp, rsp pair when it
fits its locals in the red zone (the 128 bytes below
rsp that SysV reserves for the leaf).
; leaf: red-zone usage, no stack adjustment
my_leaf:
mov qword [rsp - 8], rdi ; below rsp, but the OS won't clobber
; ... rest of body ...
ret
ARM64.
my_fn:
stp x29, x30, [sp, #-16]! // save fp, lr
mov x29, sp // new frame
sub sp, sp, #16 // 16 bytes of locals
// ... body ...
add sp, sp, #16
ldp x29, x30, [sp], #16 // restore fp, lr
ret
Argument access#
x86-64 SysV.
; void f(long a, long b, long c)
f:
; rdi = a, rsi = b, rdx = c
add rdi, rsi
add rdi, rdx
mov rax, rdi
ret
When more than six integer args are passed, args 7+ live on the
stack, just above the return address ([rsp + 8], [rsp +
16], …).
ARM64.
f:
// x0 = a, x1 = b, x2 = c
add x0, x0, x1
add x0, x0, x2
ret
Return values#
Integer / pointer in rax (x86-64) or x0 (ARM64). 128-bit
return uses rax:rdx on x86-64 or x0:x1 on ARM64.
For struct returns up to 16 bytes (or two registers’ worth), they ride in registers; larger struct returns use a hidden first argument (a pointer the caller supplies) and the function writes through it.
Calling another function#
The operator preserves caller-saved registers around a call by pushing them; saves arguments into callee-saved slots first if they are needed after the call.
; foo(x); bar(x); both clobber rdi
; preserve x across the calls
call_both:
push rbx ; callee-saved, available to us
mov rbx, rdi ; stash x
mov rdi, rbx
call foo
mov rdi, rbx
call bar
pop rbx
ret
Variadic#
x86-64 SysV variadic: rax must contain the count of vector
registers used by the call (zero if no floats). Reading the
varargs uses va_list (a layered structure pointing into the
register-save area + overflow area on the stack).
For the operator hand-rolling assembly, the simplest path is to
call into a C wrapper that handles the va_list mechanics.
Inline assembly#
GCC / Clang inline asm lets the operator embed assembly inside
C functions. The volatile keyword prevents the optimiser
from reordering or dropping the block.
static inline uint64_t rdtsc(void) {
uint32_t lo, hi;
__asm__ volatile ("rdtsc" : "=a"(lo), "=d"(hi));
return ((uint64_t)hi << 32) | lo;
}
The constraint string tells the compiler which registers carry which operands.
Tail calls#
A tail call is a call followed immediately by a ret;
the operator collapses it into a jmp to avoid stack growth.
; not a tail call
call helper
ret
; tail call (same effect, no extra frame)
jmp helper
Compilers do this automatically when they can; hand-written
assembly does it by writing jmp.
References#
Registers for register inventory and stack model.
Control flow for
call/retand branch mechanics.Runtime for ELF entry points, dynamic loading, and the ABI in full.