Skip to content
Functions and the Stack
step 1/5

Reading — step 1 of 5

Learn

~3 min readControl Flow and Memory

Assembly functions follow the System V AMD64 ABI on Linux. Understand it once and you can debug any compiled C code.

Calling convention

Argument registers (first 6 ints/pointers):

  • rdi, rsi, rdx, rcx, r8, r9

Return value: rax (and rdx for 128-bit returns).

Caller-saved (callee may clobber):

  • rax, rcx, rdx, rsi, rdi, r8, r9, r10, r11

Callee-saved (function must preserve):

  • rbx, rsp, rbp, r12, r13, r14, r15

If you use a callee-saved register, push it on entry and pop on exit.

A simple function

Square an integer: int square(int n) { return n * n; }

section .text
    global _start

; int square(int n)  ; n in edi, returns in eax
square:
    mov eax, edi
    imul eax, eax
    ret

_start:
    mov edi, 7
    call square
    ; eax now contains 49
    
    ; Convert and print would go here.
    mov rax, 60
    xor rdi, rdi
    syscall

Notes:

  • call label pushes the return address and jumps
  • ret pops the return address and jumps to it
  • We use edi (32-bit) since n is int (32-bit). Upper 32 bits of rdi are ignored.
  • Result returns in eax. Caller reads it.

With a stack frame

Longer functions need the stack:

; int sum(int a, int b)
sum:
    push rbp                ; save old frame pointer
    mov rbp, rsp            ; new frame
    sub rsp, 16             ; allocate 16 bytes (alignment)
    
    mov [rbp-4], edi        ; save a
    mov [rbp-8], esi        ; save b
    
    mov eax, [rbp-4]
    add eax, [rbp-8]
    
    mov rsp, rbp            ; deallocate
    pop rbp                  ; restore old frame
    ret

Stack alignment

The stack must be 16-byte aligned before a call. Each call pushes 8 bytes (return address); inside the function, rsp is misaligned by 8 until you push rbp (then it's aligned again). This matters for SSE/AVX instructions which fault on misaligned access.

Recursive function example

Factorial:

; int factorial(int n) ; n in edi, returns in eax
factorial:
    cmp edi, 1
    jle .base
    push rdi                ; save n (callee-saved? No — but we need it)
    sub rsp, 8              ; scratch space (NOT alignment — after the push, rsp is already 16-aligned)
    
    dec edi
    call factorial          ; eax = factorial(n-1)
    
    add rsp, 8
    pop rdi                  ; restore n
    
    imul eax, edi            ; eax = n * factorial(n-1)
    ret
.base:
    mov eax, 1
    ret

Common patterns

Save/restore:

push rbx
push r12
; ... use rbx, r12 ...
pop r12
pop rbx
ret

Local variables on stack:

sub rsp, 16        ; 2 qwords of locals
mov [rsp + 0], rax
mov [rsp + 8], rbx
; ...
add rsp, 16        ; deallocate

Functions vs syscalls

  • call func — user-mode function call, jumps directly
  • syscall — kernel transition, much slower (~1μs vs ~1ns)

Use functions for user-space logic; reserve syscalls for I/O, process management, etc.

Learning the ABI lets you call C functions from your assembly (and vice versa). It's how all language runtimes work under the hood.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…