x86-64

registers, the calling convention, and the instructions i actually type. everything is in intel/nasm order, so op dst, src.

jump to registers · calling convention · instructions · addressing · conditions

registers

64     32     16    8      role (sysv)
rax    eax    ax    al     return value
rdi    edi    di    dil    arg 1
rsi    esi    si    sil    arg 2
rdx    edx    dx    dl     arg 3  (also 2nd return)
rcx    ecx    cx    cl     arg 4
r8     r8d    r8w   r8b    arg 5
r9     r9d    r9w   r9b    arg 6
r10    r10d   r10w  r10b   scratch
r11    r11d   r11w  r11b   scratch
rbx    ebx    bx    bl     callee-saved
rbp    ebp    bp    bpl    frame pointer (callee-saved)
rsp    esp    sp    spl    stack pointer
r12    r12d   r12w  r12b   callee-saved
r13    r13d   r13w  r13b   callee-saved
r14    r14d   r14w  r14b   callee-saved
r15    r15d   r15w  r15b   callee-saved
  • rip is the instruction pointer and rflags holds the status flags (zf, sf, cf, of, ...)
  • xmm0 to xmm15 are the 128-bit sse registers for floats and vectors
  • legacy ah, bh, ch, dh alias the high byte of the first four

calling convention (sysv amd64)

linux and macos. windows x64 is different (rcx, rdx, r8, r9).

  • integer and pointer args go in rdi, rsi, rdx, rcx, r8, r9 in that order, and the rest go on the stack, pushed right to left
  • float args go in xmm0 to xmm7
  • the return value goes in rax (rdx:rax for 128-bit), and floats come back in xmm0
  • rbx, rbp, rsp, r12, r13, r14, r15 are callee-saved, so you have to restore them
  • rax, rcx, rdx, rsi, rdi, r8, r9, r10, r11 are caller-saved (scratch)
  • the stack has to be 16-byte aligned at the call, and leaf functions get a 128-byte red zone below rsp
  • syscalls are different. the number goes in rax and the args go in rdi, rsi, rdx, r10, r8, r9 (see interrupts)

instructions

  • cmp a, b sets flags for a − b, and test a, b sets flags for a & b
  • imul/idiv are signed and mul/div are unsigned. sar keeps the sign bit and shr doesn't
  • movzx/movsx zero- or sign-extend, and lea does address math without touching memory or flags
  • writing a 32-bit register zeroes the upper half of the 64-bit one, but writing 8 or 16 bits leaves the rest alone
; int add(int a, int b) { return a + b; }   (sysv)
add:
    lea eax, [rdi + rsi]    ; a in edi, b in esi -> eax
    ret

addressing modes

  • base + index·scale + disp, [rbx + rcx*4 + 8] with scale ∈ {1, 2, 4, 8}
  • rip-relative, [rel label] (nasm) or [rip + label]

conditions (jcc after cmp)

  • signed compares use jg (>), jge (≥), jl (<), and jle (≤)
  • unsigned compares use ja (>), jae (≥), jb (<), and jbe (≤)
  • flag jumps are js/jns (sign), jo/jno (overflow), and jc/jnc (carry)