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) or xmm0 (float). 128-bit return: rax:rdx or xmm0:xmm1.

  • Caller-saved: rax rcx rdx rsi rdi r8 r9 r10 r11.

  • Callee-saved: rbx rbp r12 r13 r14 r15.

  • Stack alignment: rsp must be 16-byte aligned at every call (which means 8 mod 16 on entry after the call pushes the return address).

  • Variadic functions: rax carries 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) or xmm0 (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, plus x29 (fp) and x30 (lr) by convention.

  • Caller-saved: x0..x18.

  • Stack: sp must be 16-byte aligned at every bl.

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#