DevTools

Cheatsheet Assembly

Linguagem de baixo nível com acesso direto ao hardware (x86/NASM)

Back to languages
Assembly
80 cards found
Categories:
Versions:

Basic


10 cards
x86-64 Registers
; 64-bit registers (GPR):
; RAX RBX RCX RDX RSI RDI RSP RBP
; R8  R9  R10 R11 R12 R13 R14 R15

; Sub-registers:
; EAX (32b) | AX (16b) | AH/AL (8b)

; Purpose:
; RAX - function return
; RCX - counter (loops)
; RSP - stack pointer
; RBP - base pointer (frame)
; RSI/RDI - source/destination

Registers are the fastest memory in the CPU. RAX is the accumulator (return). RSP points to the top of the stack. R8-R15 are exclusive to 64-bit. Accessing EAX zeroes the upper 32 bits.

NASM directives
; Define data:
db  0x41            ; 1 byte ('A')
dw  1000            ; 2 bytes (word)
dd  3.14            ; 4 bytes (float)
dq  123456789       ; 8 bytes (qword)

; Reserve space:
resb 16             ; 16 bytes (bss)
resw 8              ; 8 words
resq 1              ; 1 qword

; Constants:
MAX: equ 100
SIZE: equ $ - start

; Alignment:
align 16

db/dw/dd/dq define 1/2/4/8-byte data. resb/resw/resq reserve space in BSS. equ creates constants. align aligns for performance.

Flags (EFLAGS/RFLAGS)
; Main flags:
; ZF - Zero Flag (result = 0)
; CF - Carry Flag (unsigned overflow)
; SF - Sign Flag (sign bit)
; OF - Overflow Flag (signed overflow)
; PF - Parity Flag (low-byte parity)

; Instructions that affect flags:
add rax, rbx    ; ZF, CF, SF, OF
sub rax, rbx    ; ZF, CF, SF, OF
cmp rax, rbx    ; like sub, discards result
test rax, rax   ; like and, discards (ZF/SF)

; View flags:
pushfq          ; push flags to stack
pop rax         ; flags in rax

Flags are bits that indicate results. ZF=1 if zero. CF=1 on carry/borrow. SF=1 if negative. OF=1 on signed overflow. CMP and TEST only affect flags.

NASM structure
; file.asm — basic NASM structure
section .data       ; initialized data
    msg: db "Hello", 10
    len: equ $ - msg

section .bss        ; uninitialized data
    buffer: resb 64

section .text       ; code
    global _start

_start:
    ; instructions here
    mov rax, 60     ; sys_exit
    xor rdi, rdi    ; code 0
    syscall

.data holds data with an initial value. .bss reserves space (zero). .text contains the code. global _start exports the entry point. section organizes the binary.

Addressing modes
mov rax, [rbx]          ; indirect (contents of rbx)
mov rax, [rbx + 8]      ; displacement
mov rax, [rbx + rcx*4]  ; base + index*scale
mov rax, [rbx + rcx*4 + 16] ; full

; RIP-relative (64-bit):
mov rax, [rel variable]

; Immediate:
mov rax, 42             ; direct value

; Register:
mov rax, rbx            ; register → register

Addressing: [reg] = indirect, [reg+N] = displacement, [base+idx*scale] = arrays. RIP-relative is the standard in 64-bit for global data. Scales: 1, 2, 4, 8.

Comments and conventions
; Line comment (semicolon)

/* Multi-line comment
   (supported by NASM) */

; Naming conventions:
; _start      → entry point (Linux)
; main        → entry point (C)
; snake_case  → functions and labels
; CONSTANTS   → equ in uppercase

; Labels:
loop_start:     ; local label
.end:           ; local label (dot)
@@:             ; anonymous label
jmp @B          ; jumps to previous @@

; starts a comment. Labels end with :. .name are local (scope between labels). @@ is anonymous (@F = forward, @B = backward). Convention: snake_case for symbols.

Compile and run (NASM)
; Assemble (assembly → object):
nasm -f elf64 program.asm -o program.o

; Link (object → executable):
ld program.o -o program

; Run:
./program

; With GCC (if using C functions):
nasm -f elf64 -g -F dwarf main.asm
gcc main.o -o app -no-pie

; View machine code:
objdump -d program

nasm -f elf64 generates a 64-bit Linux object. ld links without libc. Use gcc if you need printf/malloc. -g -F dwarf adds debug info. objdump -d disassembles.

MOV and data transfer
mov rax, 42         ; immediate → register
mov rax, rbx        ; register → register
mov rax, [mem]      ; memory → register
mov [mem], rax      ; register → memory

; Extension variants:
movzx eax, byte [x]  ; zero-extend (8→32)
movsx eax, byte [x]  ; sign-extend (8→32)
movsxd rax, eax      ; sign-extend (32→64)

; LEA (address, not contents):
lea rax, [rbx + rcx*4 + 10]

MOV copies data (it does not move!). movzx extends with zeros. movsx extends with sign. LEA computes an address without accessing memory — useful for fast arithmetic.

Hello World
section .data
    msg: db "Hello, World!", 10
    len: equ $ - msg

section .text
    global _start

_start:
    mov rax, 1          ; sys_write
    mov rdi, 1          ; stdout (fd=1)
    mov rsi, msg        ; pointer to string
    mov rdx, len        ; length
    syscall

    mov rax, 60         ; sys_exit
    xor rdi, rdi        ; exit code 0
    syscall

sys_write (rax=1) prints. rdi=fd (1=stdout), rsi=buffer, rdx=size. syscall invokes the kernel. sys_exit (rax=60) terminates. $ - msg computes the length.

Sizes and suffixes
; Explicit sizes:
mov byte [x], 0x41     ; 1 byte
mov word [x], 0x1234   ; 2 bytes
mov dword [x], 0xABCD  ; 4 bytes
mov qword [x], 0xFF    ; 8 bytes

; In a register (implicit):
mov al, 1     ; 8-bit
mov ax, 1     ; 16-bit
mov eax, 1    ; 32-bit (zeroes upper 32!)
mov rax, 1    ; 64-bit

; NASM uses byte/word/dword/qword ptr
; MASM uses PTR: mov [x], DWORD PTR 0

Suffixes: byte=1, word=2, dword=4, qword=8 bytes. Writing to EAX automatically zeroes the upper 32 bits of RAX. In NASM, the size comes from the register or is explicit.

Data and Memory


10 cards
Defining variables
section .data
    ; Strings:
    name:   db "Anna", 0        ; null-terminated
    msg:    db "Hello", 10     ; with newline

    ; Numeric:
    age:    db 30              ; byte
    year:   dw 2024            ; word (2 bytes)
    pop:    dd 7000000         ; dword (4 bytes)
    big:    dq 9999999999      ; qword (8 bytes)

    ; Float:
    pi:     dd 3.14159         ; float 32-bit
    e:      dq 2.718281828     ; double 64-bit

    ; Arrays:
    vector: dd 1, 2, 3, 4, 5

db = byte/string, dw = word, dd = dword/float, dq = qword/double. Strings are byte arrays. 0 at the end = null-terminated. Arrays are contiguous data.

Structs in Assembly
; Simulate a struct with offsets:
; struct Person { char name[32]; int age; }
STRUC Person
    .name:   resb 32
    .age:    resd 1
    .size:
ENDSTRUC

section .bss
    p: resb Person.size

section .text
    ; Access:
    mov dword [p + Person.age], 30
    lea rdi, [p + Person.name]

    ; Manual offset:
    ; name = offset 0
    ; age  = offset 32

STRUC/ENDSTRUC define structs in NASM. Fields have automatic offsets (Person.age). Person.size gives the total size. Access via [base + offset]. Equivalent to structs in C.

Pointer arithmetic
; Pointer in rdi, iterate array:
mov rdi, arr        ; rdi = &arr[0]
mov rcx, 5          ; 5 elements

.loop:
    mov eax, [rdi]  ; read current element
    add eax, 1      ; process
    mov [rdi], eax  ; write back
    add rdi, 4      ; next (dword = +4)
    dec rcx
    jnz .loop

; Alternative with index:
xor rcx, rcx
.next:
    mov eax, [arr + rcx*4]
    inc rcx
    cmp rcx, 5
    jl .next

Pointers advance by the type size: +4 for dword, +8 for qword. Alternative: index with [base + idx*scale]. Both are equivalent. A pointer is more natural in Assembly.

BSS (reserve space)
section .bss
    ; Reserve without initializing (zero):
    buffer:   resb 256        ; 256 bytes
    counter:  resd 1          ; 1 dword (4 bytes)
    matrix:   resq 100        ; 100 qwords (800 bytes)
    array:    resw 10         ; 10 words

; Advantages of BSS:
; • Takes no space in the binary
; • Zero-initialized by the OS
; • Faster than .data for buffers

; Access:
mov byte [buffer], 'A'
mov dword [counter], 0

BSS reserves space with no initial value (zero by the OS). resb/resw/resd/resq reserve 1/2/4/8 bytes per element. It does not grow the binary. Ideal for large buffers and arrays.

Stack (push/pop)
; PUSH: decrements RSP, writes
push rax         ; RSP -= 8; [RSP] = RAX
push rbx
push rcx

; POP: reads, increments RSP
pop rcx          ; RCX = [RSP]; RSP += 8
pop rbx
pop rax

; Peek top without removing:
mov rax, [rsp]   ; peek

; Reserve local space:
sub rsp, 32      ; 32 bytes for locals
; ... use [rsp], [rsp+8], etc.
add rsp, 32      ; release (epilogue)

PUSH places on the stack (RSP -= 8). POP removes (RSP += 8). LIFO: last in, first out. The stack grows downward (lower addresses). sub rsp, N reserves space for local variables.

Aligned data
section .data
    align 16            ; align to 16 bytes
    vec4: dd 1.0, 2.0, 3.0, 4.0

    align 32            ; for AVX
    vec8: dd 1.0, 2.0, 3.0, 4.0
          dd 5.0, 6.0, 7.0, 8.0

section .bss
    alignb 16
    buffer: resb 128

; Align the stack (required on calls):
and rsp, -16        ; align RSP to 16 bytes
; ... call functions ...

; Why: SIMD requires alignment
; movaps fails if not aligned!

align 16 aligns data to 16 bytes (SSE). align 32 for AVX. and rsp, -16 aligns the stack before calls. movaps fails with a segfault if misaligned. Performance: 64-byte cache lines.

Arrays and indexing
section .data
    arr: dd 10, 20, 30, 40, 50

section .text
    ; Access by index:
    mov rax, [arr]            ; arr[0] = 10
    mov rax, [arr + 4]        ; arr[1] = 20
    mov rax, [arr + 8]        ; arr[2] = 30

    ; With a variable index (i in rcx):
    mov rcx, 2
    mov eax, [arr + rcx*4]   ; arr[2] = 30

    ; Write:
    mov dword [arr + rcx*4], 99  ; arr[2] = 99

    ; Size:
    len: equ ($ - arr) / 4   ; 5 elements

Arrays are contiguous memory. [arr + idx*4] for dword (4 bytes per element). Scale: *1 (byte), *2 (word), *4 (dword), *8 (qword). $ - arr gives the size in bytes.

Memory access
; Read from memory:
mov rax, [variable]        ; 8 bytes
mov eax, [variable]        ; 4 bytes
mov al, byte [variable]    ; 1 byte

; Write to memory:
mov [variable], rax
mov byte [buffer + 5], 'X'

; RIP-relative addressing (64-bit):
mov rax, [rel counter]     ; position-independent

; With negative displacement:
mov rax, [rbp - 8]        ; local variable
mov rax, [rbp + 16]       ; argument

Brackets [] = memory access. Without brackets = immediate value or register. [rel var] is RIP-relative (position-independent code). [rbp-N] for locals, [rbp+N] for arguments.

LEA (load address)
; LEA computes an address WITHOUT accessing memory:
lea rax, [rbx + rcx*4 + 8]

; Use for arithmetic (fast!):
lea rax, [rax + rax*4]    ; rax = rax * 5
lea rax, [rax*8 + rax]    ; rax = rax * 9
lea rax, [rax + rax*2]    ; rax = rax * 3

; vs MOV (accesses memory):
mov rax, [rbx + rcx*4]    ; loads VALUE
lea rax, [rbx + rcx*4]    ; loads ADDRESS

; Pointer to a variable:
lea rdi, [rel msg]        ; address of msg

LEA computes an address without reading memory. Faster than MOV for arithmetic (1 cycle). Multiplies by constants: [rax+rax*4] = ×5. Does not affect flags. Widely used for &variable.

Constants and EQU
; Constants (take no memory):
MAX:     equ 100
NEWLINE: equ 10
NULL:    equ 0
STDIN:   equ 0
STDOUT:  equ 1
STDERR:  equ 2

; Compile-time computation:
SIZE:    equ 1024 * 4
OFFSET:  equ 8 + 16

; Usage:
mov rcx, MAX
cmp rax, SIZE

; %define (macro, redefinable):
%define VERSION 2

equ defines constants at assembly time (no memory). Replaced textually. %define is redefinable (macro). Ideal for syscalls, sizes and offsets. Do not confuse with variables in .data.

Arithmetic and Logic


10 cards
ADD and SUB
add rax, 10         ; rax += 10
add rax, rbx        ; rax += rbx
sub rax, 5          ; rax -= 5
sub rax, rcx        ; rax -= rcx

; Flags affected: ZF, CF, SF, OF
add rax, rbx
; ZF=1 if result=0
; CF=1 on unsigned overflow
; SF=1 if bit 63=1 (negative)
; OF=1 on signed overflow

; ADD with memory:
add dword [counter], 1
sub qword [balance], rax

ADD adds, SUB subtracts. Both affect flags: ZF (zero), CF (carry), SF (sign), OF (overflow). ADD reg, mem and ADD mem, reg are valid.

AND, OR, XOR, NOT
and rax, 0xFF       ; mask (keep 8 bits)
and rax, -16        ; align to 16 (clear 4 bits)

or rax, 0x01        ; set bit 0
or rax, rbx         ; combine bits

xor rax, rax        ; zero (faster than mov!)
xor eax, eax        ; zeroes the whole RAX (32→64)

not rax             ; invert all bits

; Test a bit:
test rax, 0x04      ; is bit 2 set?
jnz .bit_set        ; ZF=0 if bit=1

AND = mask, OR = set bits, XOR = toggle/zero, NOT = invert. xor rax,rax is the fastest way to zero (1 byte, 1 cycle). TEST = AND without storing the result.

ADC and SBB (carry)
; ADC: add + carry flag
; For 128-bit addition:
mov rax, [num1]
add rax, [num2]       ; add low
mov rdx, [num1+8]
adc rdx, [num2+8]     ; add high + carry

; SBB: subtract + borrow
mov rax, [num1]
sub rax, [num2]       ; sub low
mov rdx, [num1+8]
sbb rdx, [num2+8]     ; sub high - borrow

; Usage: arbitrary-precision arithmetic
; (numbers larger than 64 bits)

ADC = ADD + Carry Flag. SBB = SUB + Borrow. Used for multi-precision arithmetic (128+ bits). Pattern: ADD on the low, ADC on the high. Essential for cryptography and big numbers.

MUL and IMUL
; MUL (unsigned): result in RDX:RAX
mov rax, 100
mov rbx, 200
mul rbx         ; RDX:RAX = RAX * RBX

; IMUL (signed, more versatile):
imul rax, rbx           ; rax *= rbx
imul rax, rbx, 10       ; rax = rbx * 10
imul rax, rcx           ; rax = rax * rcx

; 32-bit: result in EDX:EAX
mov eax, 50000
mov ebx, 60000
mul ebx         ; EDX:EAX = EAX * EBX

; Overflow: CF=OF=1 if result does not fit

MUL = unsigned, result in RDX:RAX (128 bits). IMUL = signed, more flexible (2 or 3 operands). To multiply by powers of 2, use SHL (faster).

Shifts (SHL, SHR, SAR)
shl rax, 1      ; rax *= 2 (shift left)
shl rax, 4      ; rax *= 16
shr rax, 1      ; rax /= 2 (unsigned)
shr rax, 3      ; rax /= 8

sar rax, 1      ; arithmetic shift (preserves sign)
; sar -8, 1 = -4 (correct for negatives)
; shr -8, 1 = huge value (wrong!)

; Rotation:
rol rax, 1      ; rotate left (bit exits → enters)
ror rax, 4      ; rotate right

; SHLD/SHRD: double shift
shld rax, rbx, 4  ; shift left with bits from rbx

SHL = ×2^n, SHR = ÷2^n (unsigned). SAR preserves sign (for negatives). ROL/ROR rotate bits. Shifts are much faster than MUL/DIV for powers of 2.

BSR, BSF, POPCNT
; Bit Scan:
bsf rcx, rax      ; index of first bit 1 (low)
bsr rcx, rax      ; index of last bit 1 (high)
; ZF=1 if input=0

; Popcount (count 1 bits):
popcnt rcx, rax   ; rcx = number of bits set to 1
; Requires: -mpopcnt or CPUID check

; Count zeros:
lzcnt rcx, rax    ; leading zeros
tzcnt rcx, rax    ; trailing zeros (= bsf)

; Extract a bit:
bt rax, 5         ; CF = bit 5 of rax
bts rax, 5        ; set bit 5
btr rax, 5        ; clear bit 5
btc rax, 5        ; toggle bit 5

BSF/BSR find the first/last bit 1. POPCNT counts active bits. LZCNT/TZCNT count zeros. BT/BTS/BTR manipulate individual bits. Useful for bitmaps and flags.

DIV and IDIV
; DIV (unsigned):
; Divides RDX:RAX by the operand
; Quotient → RAX, Remainder → RDX
mov rax, 100
xor rdx, rdx       ; ZERO RDX first!
mov rbx, 7
div rbx            ; rax=14, rdx=2

; IDIV (signed):
mov rax, -100
cqo                ; sign-extend RAX → RDX:RAX
mov rbx, 7
idiv rbx           ; rax=-14, rdx=-2

; 32-bit: cdq before div
; Careful: div by 0 = #DE exception!

DIV divides RDX:RAX by the operand. Quotient in RAX, remainder in RDX. Always zero RDX (xor rdx,rdx) or sign-extend (cqo). Division by 0 raises an exception.

CMP and TEST
; CMP: subtracts without storing (flags only)
cmp rax, 10
; ZF=1 if rax==10
; CF=1 if rax<10 (unsigned)
; SF≠OF if rax<10 (signed)

cmp rax, rbx
je  .equal      ; ZF=1
jl  .less       ; SF≠OF (signed)
ja  .greater    ; CF=0 and ZF=0 (unsigned)

; TEST: AND without storing (flags only)
test rax, rax   ; is rax zero?
jz  .null       ; ZF=1 if rax=0

test rax, 1     ; is it odd?
jnz .odd        ; bit 0 = 1

CMP compares (does SUB without storing). TEST checks bits (does AND without storing). Both affect flags only. Followed by conditional jumps. test rax,rax + jz = check null/zero.

INC, DEC and NEG
inc rax         ; rax++ (does not affect CF!)
dec rcx         ; rcx-- (does not affect CF!)

neg rax         ; rax = -rax (two's complement)
; neg: CF=0 if input=0, CF=1 otherwise

; Alternatives that affect CF:
add rax, 1      ; like inc, but affects CF
sub rax, 1      ; like dec, but affects CF

; Typical usage:
mov rcx, 10
.loop:
    ; process
    dec rcx
    jnz .loop   ; ZF=1 when rcx=0

INC/DEC increment/decrement without affecting CF (useful after ADC/SBB). NEG negates (two's complement). In loops, DEC + JNZ is the pattern. ADD/SUB 1 if you need CF.

Type conversions
; Sign extension:
movsx eax, byte [x]    ; int8 → int32
movsx rax, dword [y]   ; int32 → int64
movsxd rax, eax        ; int32 → int64
cwde                   ; AX → EAX (sign)
cdqe                   ; EAX → RAX (sign)

; Zero extension:
movzx eax, byte [x]    ; uint8 → uint32
movzx rax, word [y]    ; uint16 → uint64

; Prepare division:
xor rdx, rdx           ; zero RDX (unsigned)
cqo                    ; RDX:RAX = sign-extend (signed)
cdq                    ; EDX:EAX = sign-extend (32-bit)

MOVSX extends with sign (correct for negatives). MOVZX extends with zeros (unsigned). CQO/CDQ prepare RDX for division. Always use the correct extension to avoid bugs.

Flow Control


10 cards
JMP (unconditional)
; Unconditional jump:
jmp .end            ; local label
jmp start           ; global label
jmp rax             ; indirect (address in rax)
jmp [rax]           ; indirect via memory

; Labels:
start:
    ; code
.end:
    ; code

; Short vs near:
jmp short .close    ; -128 to +127 bytes (2 bytes)
jmp near .far       ; ±2GB (5 bytes)
; NASM chooses automatically

JMP jumps unconditionally (like goto). jmp label = direct. jmp reg = indirect (jump tables, vtables). short = 2 bytes (limited). Local labels with a . prefix.

LOOP instruction
; LOOP: decrements RCX, jumps if != 0
mov rcx, 5
.loop:
    ; body (runs 5 times)
    mov rax, rcx
    loop .loop      ; rcx--; if rcx!=0 jmp

; Variants:
loope .loop     ; loop if RCX!=0 AND ZF=1
loopz .loop     ; synonym of LOOPE
loopne .loop    ; loop if RCX!=0 AND ZF=0
loopnz .loop    ; synonym of LOOPNE

; Note: LOOP is slow on modern CPUs!
; Prefer: dec rcx / jnz .loop

LOOP = DEC RCX + implicit JNZ. LOOPE/LOOPNE also check ZF. On modern CPUs, LOOP is slower than DEC+JNZ (microcode). Use only for readability in educational code.

Ternary (CMOV)
; rax = (rbx > 10) ? 1 : 0

; With jumps:
    cmp rbx, 10
    jle .zero
    mov rax, 1
    jmp .end
.zero:
    xor rax, rax
.end:

; With CMOV (no branch, faster):
    xor rax, rax        ; rax = 0 (default)
    mov rcx, 1
    cmp rbx, 10
    cmovg rax, rcx      ; if >, rax = 1

; CMOVcc: move if condition (no jump!)
; cmovl, cmovg, cmove, cmovne, cmova...

CMOVcc = conditional move without a branch. Avoids branch predictor misprediction. cmovg = move if greater. Ideal for simple ternaries. No branch = the pipeline does not stall. Prefer it for simple expressions.

Conditional jumps (equality)
cmp rax, 10

je  .equal      ; jump if equal (ZF=1)
jne .different  ; jump if not equal (ZF=0)

; Synonyms:
jz  .zero       ; jump if zero (ZF=1) = JE
jnz .not_zero   ; jump if not zero (ZF=0) = JNE

; After TEST:
test rax, rax
jz  .null       ; rax == 0?
jnz .valid      ; rax != 0?

; Check a bit:
test rax, 0x04
jnz .bit2_set   ; bit 2 set?

JE/JNE = equal/not equal (after CMP). JZ/JNZ = zero/not-zero (after TEST). They are synonyms (same opcode, ZF). Use the name that makes most sense in context.

IF/ELSE in Assembly
; if (rax > 10) { rbx = 1 } else { rbx = 0 }

    cmp rax, 10
    jle .else       ; if rax <= 10, go to else

.then:
    mov rbx, 1
    jmp .end        ; skip the else!

.else:
    mov rbx, 0

.end:
    ; continues here

; Pattern: the NEGATED condition jumps to else
; jmp .end in then avoids running else

IF/ELSE: negate the condition and jump to .else. In the then block, JMP .end so you do not fall into the else. Labels with . are local. Equivalent to if (cond) {...} else {...} in C.

SETcc (conditional byte)
; SETcc: sets a byte to 0 or 1
cmp rax, rbx
sete al         ; al = 1 if equal, 0 otherwise
movzx rax, al   ; extend to 64 bits

setl cl         ; cl = 1 if signed less
setg dl         ; dl = 1 if signed greater
arrow bl         ; bl = 1 if unsigned above
setne sil       ; sil = 1 if not equal

; Boolean → integer:
xor eax, eax
cmp rcx, 0
setnz al        ; al = (rcx != 0) ? 1 : 0

; Combine with arithmetic:
; rax += (rbx > 5)
cmp rbx, 5
setg cl
movzx rcx, cl
add rax, rcx

SETcc sets a byte (AL, CL, etc.) to 0 or 1 according to the flags. Followed by MOVZX to use the an integer. No branches. Useful for counting conditions and booleans. Variants: SETE, SETL, SETG, SETA.

Conditional jumps (comparison)
; SIGNED (after CMP):
jl  .less       ; jump if less (SF≠OF)
jle .less_equal ; less or equal (ZF=1 or SF≠OF)
jg  .greater    ; jump if greater (ZF=0 and SF=OF)
jge .greater_equal ; greater or equal (SF=OF)

; UNSIGNED (after CMP):
jb  .below      ; jump if below (CF=1)
jbe .below_equal ; below or equal (CF=1 or ZF=1)
ja  .above      ; jump if above (CF=0 and ZF=0)
jae .above_equal ; above or equal (CF=0)

; Mnemonics: L/G = signed, B/A = unsigned

Signed: JL/JG/JLE/JGE. Unsigned: JB/JA/JBE/JAE. L/G for correct negatives. B/A for addresses/sizes. Always after CMP.

Switch/Case (jump table)
section .data
    jump_table: dq .case0, .case1, .case2, .case3

section .text
    ; rax = index (0-3)
    cmp rax, 3
    ja  .default        ; out of range

    jmp [jump_table + rax*8]  ; indirect jump

.case0:
    mov rbx, 10
    jmp .end
.case1:
    mov rbx, 20
    jmp .end
.case2:
    mov rbx, 30
    jmp .end
.default:
    mov rbx, 0
.end:

Switch uses an address table + JMP [table + idx*8]. Check the range first (CMP + JA). Each case ends with JMP .end (avoids fall-through). O(1) for any case. Like a vtable.

Loops with DEC/JNZ
; Loop N times (pattern):
mov rcx, 10         ; counter
.loop:
    ; loop body
    mov rax, rcx    ; use counter
    dec rcx         ; rcx--
    jnz .loop       ; repeat if rcx != 0

; Loop with increasing index:
xor rcx, rcx        ; i = 0
.next:
    mov rax, [arr + rcx*4]
    ; process
    inc rcx
    cmp rcx, 10
    jl .next        ; while i < 10

; Infinite loop:
.inf:
    jmp .inf        ; or: jmp $

Pattern: DEC + JNZ (countdown). Alternative: INC + CMP + JL (countup). JMP $ = infinite loop (jumps to itself). Countdown is more efficient (fewer instructions).

Short-circuit (&amp;&amp;, ||)
; if (a > 0 && b > 0) { ... }
    cmp rax, 0
    jle .end        ; if a<=0, FALSE (short-circuit)
    cmp rbx, 0
    jle .end        ; if b<=0, FALSE
    ; both true:
    ; if body
.end:

; if (a == 1 || b == 2) { ... }
    cmp rax, 1
    je  .true        ; if a==1, TRUE (short-circuit)
    cmp rbx, 2
    je  .true        ; if b==2, TRUE
    jmp .end
.true:
    ; if body
.end:

&&: jumps to the end on the first false condition. ||: jumps to the body on the first true one. Short-circuit: the second condition is only evaluated if needed. Same pattern that C compilers generate.

Functions and Stack


10 cards
CALL and RET
; CALL: push RIP, jump to function
call my_function      ; call
; ... continues here after RET

; RET: pop RIP, return to caller
my_function:
    mov rax, 42       ; return value
    ret               ; return

; Indirect CALL:
call rax              ; address in register
call [rax]            ; address in memory

; RET with cleanup (stdcall, rare in x64):
ret 8                 ; pop + remove 8 bytes

CALL saves the return address on the stack and jumps. RET pops that address and returns. RAX is the return register. In x86-64, CALL is always relative (±2GB).

Local variables
func:
    push rbp
    mov rbp, rsp
    sub rsp, 48       ; 6 local qwords

    ; Locals (negative offsets from RBP):
    mov [rbp-8], rdi      ; local1 = arg1
    mov [rbp-16], 0       ; local2 = 0
    mov qword [rbp-24], 100 ; local3 = 100

    ; Local array:
    ; [rbp-32] to [rbp-48] = 2 qwords

    ; Access:
    mov rax, [rbp-8]      ; read local1
    add rax, [rbp-24]     ; local1 + local3

    leave
    ret

Local variables live at [rbp-N] (negative offsets). Reserve with sub rsp, N (a multiple of 16!). The compiler allocates everything at once. Access is relative to RBP (stable even with push/pop).

Variadic functions
; printf(fmt, ...) — variable args
; RAX = number of XMM registers used (0-8)

section .data
    fmt: db "x=%d, y=%f", 10, 0

section .text
    extern printf

    mov rdi, fmt        ; format
    mov esi, 42         ; int arg
    movq xmm0, [pi]    ; double arg
    mov eax, 1          ; 1 XMM register used!
    call printf

; AL (RAX low) = count of XMM args
; If you forget → crash or garbage!
; For pure integers: xor eax, eax

Variadic functions (printf, scanf) require AL = the number of args in XMM. Forgetting causes a crash. Integers in RDI, RSI, RDX... Floats in XMM0-7. Always xor eax,eax if there are no floats.

Stack frame (prologue/epilogue)
my_function:
    ; Prologue:
    push rbp          ; save previous frame
    mov rbp, rsp      ; new frame pointer
    sub rsp, 32       ; space for locals

    ; Body:
    mov [rbp-8], rdi   ; local 1 (arg1)
    mov [rbp-16], rsi  ; local 2 (arg2)

    ; Epilogue:
    mov rsp, rbp      ; release locals
    pop rbp           ; restore frame
    ret

; Alternative (leave = mov rsp,rbp + pop rbp):
    leave
    ret

Prologue: push rbp + mov rbp, rsp + sub rsp, N. Epilogue: leave + ret. RBP is stable during AS function. Locals at [rbp-N], args at [rbp+16] and above.

Recursion
; factorial(n): if n<=1 return 1, else n*factorial(n-1)
factorial:
    cmp rdi, 1
    jle .base           ; base case

    push rdi            ; save n (caller-saved!)
    dec rdi             ; n - 1
    call factorial      ; recursion
    pop rdi             ; restore n

    imul rax, rdi       ; n * factorial(n-1)
    ret

.base:
    mov rax, 1
    ret

; Careful: stack overflow with large N!
; Each call uses ~16+ bytes of stack

Recursion: save the needed registers with PUSH before the CALL (they are caller-saved!). Restore with POP afterward. The base case avoids infinite recursion. Each call consumes stack — practical limit ~8MB.

Calling C functions (extern)
; Declare an external function:
extern printf
extern malloc
extern strlen

section .data
    msg: db "Value: %d", 10, 0

section .text
    global main

main:
    push rbp
    mov rbp, rsp

    ; printf("Value: %d", 42)
    mov rdi, msg        ; arg1: format
    mov esi, 42         ; arg2: value
    xor eax, eax        ; 0 xmm args
    call printf wrt ..plt  ; PIC (Linux)

    ; return 0
    xor eax, eax
    pop rbp
    ret

; Link: gcc main.o -o app -no-pie

extern declares C functions. Call with arguments in RDI, RSI, RDX... wrt ..plt for PIC code (position-independent). Link with gcc (not ld). xor eax,eax before variadics.

Arguments (System V AMD64)
; Linux/macOS convention (System V):
; Integers/pointers: RDI, RSI, RDX, RCX, R8, R9
; Floats: XMM0-XMM7
; Return: RAX (int), XMM0 (float)
; Extras: on the stack (right → left)

; Example: func(a, b, c, d, e, f, g)
; a=RDI, b=RSI, c=RDX, d=RCX, e=R8, f=R9
; g → [rsp] (on the stack)

; Windows (Microsoft x64):
; RCX, RDX, R8, R9 (only 4 in registers!)
; Shadow space: 32 mandatory bytes

Linux: 6 integer args in RDI, RSI, RDX, RCX, R8, R9. Return in RAX. Windows uses RCX, RDX, R8, R9 + shadow space. Floats in XMM0-7.

Leaf function (no CALL)
; Leaf = does not call other functions
; Does not need a full prologue!

; Minimal version:
sum:
    lea rax, [rdi + rsi]  ; rax = a + b
    ret

; With red zone (128 free bytes):
; Below RSP, no push, safe in a leaf
leaf_func:
    mov [rsp-8], rdi    ; red zone!
    mov [rsp-16], rsi
    ; process without sub rsp
    ret

; Red zone: 128 bytes below RSP
; Only safe if it does NOT call functions
; (signal handlers may corrupt it)

A leaf function makes no CALL — it can omit the prologue. Red zone: 128 bytes below RSP are safe in a leaf (System V). No push/pop = faster. Ideal for simple functions (getters, calculations).

Callee-saved vs Caller-saved
; Callee-saved (preserve if you use them!):
; RBX, RBP, R12, R13, R14, R15
; (also RSP implicitly)

; Caller-saved (may be changed):
; RAX, RCX, RDX, RSI, RDI, R8-R11

; Example: function that uses RBX
func:
    push rbx          ; preserve!
    mov rbx, rdi      ; use rbx
    call other_func   ; rbx is safe
    mov rax, rbx      ; still valid
    pop rbx           ; restore
    ret

; If you do not preserve → intermittent bug!

Callee-saved: RBX, RBP, R12-R15 — the called function MUST preserve them. Caller-saved: RAX, RCX, RDX, RSI, RDI, R8-R11 — may be destroyed by a CALL. Always push/pop the callee-saved ones.

Stack alignment
; The ABI requires RSP aligned to 16 bytes
; BEFORE a CALL (after CALL, RSP%16 = 8)

; Check:
; rsp % 16 == 0 before call → correct

; If misaligned:
sub rsp, 8          ; align (if needed)
call func
add rsp, 8          ; restore

; On entry to the function (after CALL):
; RSP % 16 == 8 (the CALL pushed 8)
; push rbp → RSP % 16 == 0 ✓

; Why: movaps/movdqa require alignment
; SSE/AVX fail with a misaligned stack

The stack MUST be 16-byte aligned before a CALL. After CALL, RSP%16=8. The prologue (push rbp) realigns it. If you do sub rsp, N, N must be a multiple of 16. movaps causes a segfault if misaligned.

System Calls


10 cards
Syscall (overview)
; Linux x86-64 syscalls:
; RAX = syscall number
; RDI, RSI, RDX, R10, R8, R9 = arguments
; syscall → invokes the kernel
; Return in RAX (negative = error)

; Common syscalls:
; 0  = read
; 1  = write
; 2  = open
; 3  = close
; 39 = getpid
; 60 = exit
; 63 = uname

; Full table:
; /usr/include/asm/unistd_64.h

syscall invokes the Linux kernel. Number in RAX, args in RDI, RSI, RDX, R10, R8, R9. Return in RAX (negative = errno). Destroys RCX and R11.

sys_open and sys_close
section .data
    filename: db "/tmp/test.txt", 0

section .text
    ; open(filename, O_WRONLY|O_CREAT, 0644)
    mov rax, 2              ; sys_open
    lea rdi, [rel filename]
    mov rsi, 0x241          ; O_WRONLY|O_CREAT|O_TRUNC
    mov rdx, 0644o          ; permissions (octal)
    syscall
    ; rax = file descriptor (or -errno)

    mov rdi, rax            ; save the fd

    ; ... write to the file ...

    ; close(fd)
    mov rax, 3              ; sys_close
    ; rdi already has the fd
    syscall

; Flags: O_RDONLY=0, O_WRONLY=1, O_RDWR=2
; O_CREAT=0100, O_TRUNC=01000, O_APPEND=02000

sys_open (rax=2): path, flags, mode. Returns fd (≥0) or -errno. sys_close (rax=3): fd in rdi. Flags: O_RDONLY=0, O_WRONLY=1, O_CREAT=0100. Always close fds!

sys_execve
section .data
    path: db "/bin/echo", 0
    arg0: db "echo", 0
    arg1: db "Hello!", 0

section .text
    ; execve(path, argv, envp)
    mov rax, 59             ; sys_execve
    lea rdi, [rel path]     ; filename

    ; argv = {"echo", "Hello!", NULL}
    lea rsi, [rel argv]     ; pointer to the array

    ; envp = NULL (no environment)
    xor rdx, rdx

    syscall
    ; If it returns → ERROR! (execve does not return)

section .data
    argv: dq arg0, arg1, 0  ; array of pointers

sys_execve (59) replaces the process with another. rdi=path, rsi=argv (array of pointers, NULL-terminated), rdx=envp. If it returns, it is an error. Never returns on success. Combine with fork.

sys_write (print)
section .data
    msg: db "Hello, World!", 10
    len: equ $ - msg

section .text
    global _start
_start:
    ; write(1, msg, len)
    mov rax, 1          ; sys_write
    mov rdi, 1          ; fd = stdout
    lea rsi, [rel msg]  ; buffer
    mov rdx, len        ; size
    syscall

    ; rax = bytes written (or -errno)
    cmp rax, 0
    jl  .error          ; check for error

    mov rax, 60         ; sys_exit
    xor rdi, rdi
    syscall
.error:
    mov rax, 60
    mov rdi, 1          ; exit code 1
    syscall

sys_write (rax=1): rdi=fd (1=stdout, 2=stderr), rsi=buffer, rdx=size. Returns bytes written. Check rax < 0 for errors. Does not add a newline automatically.

sys_mmap (allocate memory)
; mmap(NULL, size, PROT, FLAGS, fd, offset)
mov rax, 9              ; sys_mmap
xor rdi, rdi            ; addr = NULL (kernel chooses)
mov rsi, 4096           ; size = 4KB
mov rdx, 3              ; PROT_READ|PROT_WRITE
mov r10, 0x22           ; MAP_PRIVATE|MAP_ANONYMOUS
mov r8, -1              ; fd = -1 (anonymous)
xor r9, r9              ; offset = 0
syscall
; rax = memory address (or -errno)

; Use:
mov [rax], byte 'A'    ; write

; Free: munmap(addr, size)
mov rax, 11             ; sys_munmap
mov rsi, 4096
syscall

; PROT: READ=1, WRITE=2, EXEC=4
; MAP: SHARED=0x01, PRIVATE=0x02, ANON=0x20

sys_mmap (rax=9) allocates memory. MAP_ANONYMOUS = no file. Returns an address or -errno. sys_munmap (rax=11) frees it. Equivalent to malloc (which uses mmap internally). 4096-byte pages.

Error handling
; A syscall returns -errno in RAX
; (negative values: -1 to -4095)

mov rax, 2          ; sys_open
lea rdi, [rel path]
xor esi, esi        ; O_RDONLY
syscall

cmp rax, 0
jl  .error          ; negative = error
mov rdi, rax        ; valid fd
jmp .continue

.error:
    neg rax         ; rax = positive errno
    ; common errno:
    ; 2  = ENOENT (file does not exist)
    ; 13 = EACCES (permission denied)
    ; 9  = EBADF (invalid fd)

    ; Write the error to stderr:
    mov rax, 1      ; sys_write
    mov rdi, 2      ; stderr
    lea rsi, [rel errmsg]
    mov rdx, errmsg_len
    syscall

Syscalls return -errno in RAX (negative = error). Check with cmp rax, 0 + jl. neg rax gives the positive errno. Common: 2=ENOENT, 13=EACCES, 9=EBADF. Always check in production!

sys_read (read input)
section .bss
    buffer: resb 256

section .text
    ; read(0, buffer, 256)
    mov rax, 0          ; sys_read
    mov rdi, 0          ; fd = stdin
    lea rsi, [rel buffer]
    mov rdx, 256        ; max bytes
    syscall

    ; rax = bytes read (includes \n)
    ; rax = 0 → EOF
    ; rax < 0 → error

    ; Remove the newline:
    lea rdi, [rel buffer]
    add rdi, rax        ; position of \n
    dec rdi
    mov byte [rdi], 0   ; replace with NUL

sys_read (rax=0): rdi=fd (0=stdin), rsi=buffer, rdx=maximum. Returns bytes read (includes \n). rax=0 = EOF. Buffer in BSS. Remove the newline by replacing it with NUL.

sys_getpid and sys_getuid
; getpid() → process PID
mov rax, 39         ; sys_getpid
syscall
; rax = PID (always positive)

; getppid() → parent PID
mov rax, 110        ; sys_getppid
syscall

; getuid() → user ID
mov rax, 102        ; sys_getuid
syscall
; rax = UID (0=root)

; getgid() → group ID
mov rax, 104        ; sys_getgid
syscall

; gettid() → thread ID
mov rax, 186        ; sys_gettid
syscall

; Useful for: logs, temp file names,
; permission checks, debugging

sys_getpid (39) returns the PID. sys_getuid (102) returns the UID (0=root). No arguments — just RAX with the number. Useful for logs, temporary files and permission checks.

sys_exit
; Terminate the program:
mov rax, 60         ; sys_exit
mov rdi, 0          ; exit code (0=success)
syscall

; Conventional exit codes:
; 0   = success
; 1   = generic error
; 2   = incorrect usage (bash)
; 126 = no permission
; 127 = command not found
; 128+N = killed by signal N
; 139 = segfault (128+11)

; Alternative (libc):
; call exit (does not return)
; Never returns — code after is unreachable

sys_exit (rax=60): rdi = exit code. 0=success, 1=error. The kernel terminates the process and frees resources. Code after an exit syscall never runs. In C: equivalent to exit(code).

sys_fork and sys_wait4
; fork() → creates a child process
mov rax, 57         ; sys_fork
syscall
; rax = 0 → child process
; rax > 0 → parent process (rax = child PID)
; rax < 0 → error

test rax, rax
jz  .child

.parent:
    ; wait for the child
    mov rax, 61         ; sys_wait4
    mov rdi, -1         ; any child
    xor rsi, rsi        ; status (NULL)
    xor rdx, rdx        ; options
    xor r10, r10        ; rusage (NULL)
    syscall
    jmp .end

.child:
    ; child code
    mov rax, 60
    xor rdi, rdi
    syscall
.end:

sys_fork (57) duplicates the process. Returns 0 in the child, the PID in the parent. sys_wait4 (61) waits for the child. Pattern: check rax==0 for the child/parent branch. The child must do exit or execve.

Strings


10 cards
REP MOVSB (copy)
; Copy N bytes from RSI → RDI
section .data
    src: db "Hello, World!", 0

section .bss
    dst: resb 64

section .text
    lea rsi, [rel src]   ; source
    lea rdi, [rel dst]   ; destination
    mov rcx, 14          ; number of bytes
    rep movsb            ; copies RCX bytes

; REP: repeats until RCX=0
; MOVSB: moves byte [RSI]→[RDI], inc both
; Direction: DF=0 (cld) → ascending
;            DF=1 (std) → descending

; Variants: rep movsw (word), rep movsq (qword)

REP MOVSB copies RCX bytes from [RSI] to [RDI]. REP decrements RCX to 0. CLD = ascending direction (default). Equivalent to memcpy. Variants: MOVSW (2B), MOVSQ (8B).

Manual strlen
; strlen: count bytes until NUL
; Input: RDI = pointer to the string
; Output: RAX = length
my_strlen:
    xor eax, eax        ; counter = 0
.loop:
    cmp byte [rdi + rax], 0
    je  .end            ; found NUL
    inc rax
    jmp .loop
.end:
    ret

; Alternative with REPNE SCASB:
my_strlen2:
    push rdi
    mov al, 0
    mov rcx, -1
    cld
    repne scasb
    pop rax
    sub rax, rdi        ; negative
    neg rax             ; positive
    dec rax             ; -1 (NUL)
    ret

strlen counts bytes until NUL. Manual method: loop with CMP byte, 0. Fast method: repne scasb (hardware optimized). Input in RDI, return in RAX. Does not include the NUL in the count.

printf with arguments
extern printf

section .data
    fmt1: db "Name: %s, Age: %d", 10, 0
    fmt2: db "Pi = %.2f", 10, 0
    name: db "Anna", 0

section .text
    ; printf("Name: %s, Age: %d", "Anna", 30)
    lea rdi, [rel fmt1]     ; format
    lea rsi, [rel name]     ; %s → pointer
    mov edx, 30             ; %d → integer
    xor eax, eax            ; 0 xmm args
    call printf wrt ..plt

    ; printf("Pi = %.2f", 3.14)
    lea rdi, [rel fmt2]
    movq xmm0, [pi_val]    ; %f → XMM0
    mov eax, 1              ; 1 xmm arg!
    call printf wrt ..plt

section .data
    pi_val: dq 3.14

printf: format in RDI, args in RSI, RDX, RCX... Floats in XMM0-7. EAX = number of XMM args (mandatory!). %s = pointer, %d = int. String with NUL at the end.

LODSB / STOSB
; LODSB: loads byte from [RSI] → AL, RSI++
lea rsi, [rel text]
lodsb               ; al = [rsi], rsi++
lodsb               ; al = next byte

; STOSB: writes AL → [RDI], RDI++
lea rdi, [rel buffer]
mov al, 'A'
stosb               ; [rdi] = 'A', rdi++

; REP STOSB: fill memory (memset!)
lea rdi, [rel buffer]
mov al, 0           ; fill byte
mov rcx, 256        ; 256 bytes
rep stosb           ; memset(buffer, 0, 256)

; REP STOSQ: faster (8 bytes/iter)
mov rax, 0
mov rcx, 32         ; 32*8 = 256 bytes
rep stosq

LODSB reads a byte from [RSI] into AL. STOSB writes AL into [RDI]. REP STOSB = memset. REP STOSQ fills 8 bytes per iteration (faster). Always CLD before.

Manual strcpy
; strcpy: copy until NUL (inclusive)
; Input: RDI = destination, RSI = source
my_strcpy:
    push rdi            ; save the start (return)
.loop:
    lodsb               ; al = [rsi], rsi++
    stosb               ; [rdi] = al, rdi++
    test al, al         ; is it NUL?
    jnz .loop           ; if not, continue
    pop rax             ; return = start of dst
    ret

; Alternative with REP:
; (requires strlen first)
my_strcpy2:
    push rdi
    push rsi
    call my_strlen      ; rax = len (rsi)
    inc rax             ; include NUL
    mov rcx, rax
    pop rsi
    pop rdi
    rep movsb           ; copy everything
    ret

strcpy copies bytes until NUL (inclusive). LODSB+STOSB in a loop. Returns the destination pointer (C convention). Alternative: strlen + rep movsb. Careful: the destination buffer must be large enough!

Multibyte strings (UTF-8)
section .data
    ; UTF-8: "Hello" = 5 bytes (ASCII)
    msg: db "Ol", 0xC3, 0xA1, 0
    ; 0xC3 0xA1 = 'á' in UTF-8

    ; Emoji: "😀" = 4 bytes
    emoji: db 0xF0, 0x9F, 0x98, 0x80, 0

    ; Size in bytes vs characters:
    len_bytes: equ $ - msg - 1  ; bytes
    ; characters ≠ bytes for accents!

section .text
    ; write does not know UTF-8 — bytes only
    mov rax, 1
    mov rdi, 1
    lea rsi, [rel msg]
    mov rdx, 5          ; 5 BYTES (not chars)
    syscall
    ; The terminal interprets UTF-8 correctly

Assembly works with bytes, not characters. UTF-8: accents = 2 bytes, emoji = 4 bytes. sys_write sends bytes — the terminal interprets UTF-8. strlen counts bytes, not characters. Careful with indexing!

SCASB (search byte)
; SCASB: compares AL with [RDI], RDI++
; Used for strlen and strchr

; strlen: count until NUL
lea rdi, [rel string]
mov al, 0           ; search for NUL
mov rcx, -1         ; maximum (infinite)
cld                 ; ascending direction
repne scasb         ; repeats while != AL

; RCX = -(strlen+2)
not rcx             ; invert bits
dec rcx             ; strlen = ~rcx - 1
; or: neg rcx; sub rcx, 2

; strchr: search for a character
mov al, 'o'         ; search for 'o'
repne scasb
jnz .not_found
; rdi-1 = position of the character

SCASB compares AL with [RDI]. REPNE = repeats while NOT equal. For strlen: search for NUL with repne scasb. Result: NOT RCX - 1 = length. REPE = while equal.

Manual strcmp
; strcmp: compare strings
; Return: 0=equal, <0 if s1<s2, >0 if s1>s2
my_strcmp:
.loop:
    mov al, [rdi]       ; byte of s1
    mov bl, [rsi]       ; byte of s2
    cmp al, bl
    jne .diff           ; different
    test al, al
    jz  .equal          ; both NUL → equal
    inc rdi
    inc rsi
    jmp .loop
.diff:
    movzx eax, al
    movzx ebx, bl
    sub eax, ebx        ; negative/positive
    ret
.equal:
    xor eax, eax        ; 0 = equal
    ret

; Usage:
; lea rdi, [rel str1]
; lea rsi, [rel str2]
; call my_strcmp

strcmp compares byte by byte until a difference or NUL. Returns 0 (equal), negative (s1s2). movzx to unsigned before sub. Loop with inc on both pointers.

CMPSB (compare strings)
; CMPSB: compares [RSI] with [RDI], both++
; Used for strcmp/memcmp

; Compare N bytes:
lea rsi, [rel str1]
lea rdi, [rel str2]
mov rcx, 10         ; compare 10 bytes
cld
repe cmpsb          ; repeats while equal

; Result:
jz  .equal          ; all equal (RCX=0)
; RSI-1 and RDI-1 = position of the difference
; Flags: CF/SF indicate the order

; Full strcmp (until NUL):
lea rsi, [rel str1]
lea rdi, [rel str2]
.cmp_loop:
    mov al, [rsi]
    cmp al, [rdi]
    jne .different
    test al, al
    jz  .equal      ; both NUL
    inc rsi
    inc rdi
    jmp .cmp_loop

CMPSB compares [RSI] with [RDI]. REPE = repeats while equal. For strcmp: loop until a difference or NUL. Flags after the last comparison indicate the order (JB=str1memcmp.

Print number (itoa)
; Convert integer → decimal string
; Input: RAX = number, RDI = buffer
itoa:
    push rdi
    mov rcx, 0          ; digit counter
    mov rbx, 10
.div_loop:
    xor rdx, rdx
    div rbx             ; rax /= 10, remainder in rdx
    add dl, '0'         ; ASCII of the digit
    push rdx            ; save (reverse order!)
    inc rcx
    test rax, rax
    jnz .div_loop
; Write digits (reversed):
    pop rax
    mov [rdi], al
    inc rdi
    dec rcx
    jnz .write_loop-1   ; (loop)
; NUL:
    mov byte [rdi], 0
    pop rdi
    ret

itoa converts an integer to a string. Divides by 10 repeatedly. Remainder + "0" = ASCII digit. Digits come out reversed → use the stack to reorder. Terminate with NUL. Alternative: printf via libc.

Tips and Techniques


10 cards
Basic optimizations
; Zero a register (fastest):
xor eax, eax        ; 1 byte, 1 cycle
; vs: mov rax, 0    ; 7 bytes, slower

; Multiply by a constant:
lea rax, [rax + rax*4]  ; *5 (1 cycle)
shl rax, 3              ; *8 (1 cycle)
; vs: imul rax, 5       ; 3 cycles

; Swap registers:
xchg rax, rbx       ; 1 instruction
; vs: push/pop or temp

; NOP (alignment):
nop                 ; 1 byte
; multi-byte NOP for alignment:
nop dword [rax]     ; 4 bytes (does not run)

; Branch hint (documentation):
; likely: short jmp forward
; unlikely: jmp backward

xor eax,eax is the fastest way to zero (1 cycle, 2 bytes). LEA to multiply by constants. XCHG swaps without a temporary. NOP for alignment. Avoid unnecessary branches.

NASM macros
; Macro without parameters:
%macro newline 0
    mov rax, 1
    mov rdi, 1
    lea rsi, [rel .nl]
    mov rdx, 1
    syscall
    jmp %%end
    .nl: db 10
    %%end:
%endmacro

; Macro with parameters:
%macro print_str 2
    mov rax, 1
    mov rdi, 1
    lea rsi, [%1]
    mov rdx, %2
    syscall
%endmacro

; Usage:
    newline
    print_str msg, msg_len

; %% = macro-local label (avoids conflict)

%macro name N defines a macro with N parameters. %1, %2 = arguments. %%label = local label (avoids duplicates). Expanded at assembly time (no overhead). Ideal for repetitive code (prints, asserts).

Best practices
; 1. Always CLD before string ops
cld

; 2. Preserve callee-saved
push rbx            ; if you use rbx
; ...
pop rbx

; 3. Stack aligned to 16 before CALL
and rsp, -16        ; if needed

; 4. Check syscall return values
syscall
test rax, rax
js  .error

; 5. Use LEA for arithmetic
lea rax, [rdi + rsi*4]

; 6. XOR to zero (not MOV 0)
xor eax, eax

; 7. Local labels with a dot
.loop:
.end:

; 8. Comments on each logical block

Rules: CLD before string ops. Preserve callee-saved (RBX, R12-15). Stack aligned to 16. Check syscall errors. LEA for arithmetic. XOR to zero. Local labels with .. Comment blocks.

Branchless programming
; abs(x) without a branch:
mov rax, rdi        ; x
mov rcx, rdi
sar rcx, 63         ; rcx = 0 or -1 (mask)
xor rax, rcx        ; invert if negative
sub rax, rcx        ; +1 if negative

; min(a, b) without a branch:
mov rax, rdi        ; a
mov rcx, rsi        ; b
cmp rax, rcx
cmovg rax, rcx      ; if a>b, rax=b

; max(a, b):
mov rax, rdi
cmp rax, rsi
cmovl rax, rsi      ; if a<b, rax=b

; Advantage: no CPU misprediction
; The branch predictor fails on random data

Branchless avoids conditional JMPs (misprediction penalty ~15 cycles). CMOV for min/max. Mask with SAR 63 for abs. Ideal for unpredictable data. Branches are good when predictable (loops).

Debugging with GDB
; Compile with debug info:
; nasm -f elf64 -g -F dwarf prog.asm
; gcc prog.o -o prog -no-pie -g

; GDB:
; gdb ./prog
; (gdb) break _start
; (gdb) run
; (gdb) info registers
; (gdb) x/10xw $rsp    ; 10 words on the stack
; (gdb) x/s $rdi       ; string in rdi
; (gdb) stepi           ; 1 instruction
; (gdb) nexti           ; 1 instruction (skip call)
; (gdb) display $rax    ; always show

; NASM: breakpoints with int3
int3                ; software breakpoint
; (gdb) continue → stops here

; View flags:
; (gdb) print $eflags

Compile with -g -F dwarf for debugging. GDB: stepi = 1 instruction, info registers = registers. x/10xw $rsp examines the stack. int3 = a breakpoint in the code. display $rax shows it at each step.

x86 vs ARM vs RISC-V
; x86-64 (Intel/AMD):
; • CISC: complex instructions, variable size
; • 16 GPR registers (RAX-R15)
; • Flags (EFLAGS) affected by ops
; • Little-endian

; ARM64 (Apple M, Raspberry Pi):
; • RISC: simple instructions, fixed size (4B)
; • 31 registers (X0-X30)
; • No flags (explicit comparison)
; • Bi-endian (usually little)

; RISC-V:
; • Open-source RISC, modular
; • 32 registers (x0-x31, x0=zero)
; • Extensions: M(mult), F(float), V(vector)

; The concepts are transferable:
; stack, call/ret, loops, syscalls

x86 = CISC (complex instructions, variable size). ARM = RISC (simple, fixed 4 bytes). RISC-V = modular open-source. The concepts (stack, registers, loops) are universal. x86 dominates servers/desktops. ARM dominates mobile.

SSE (vector operations)
; SSE: 128-bit (XMM0-XMM15)
; 4 floats or 2 doubles per register

; Load (requires 16 alignment):
movaps xmm0, [data]     ; 4 aligned floats
movups xmm0, [data]     ; unaligned (slow)

; Operations:
addps xmm0, xmm1       ; add 4 floats
mulps xmm0, xmm1       ; multiply 4 floats
subps xmm0, xmm1       ; subtract 4 floats

; Scalar (1 float):
addss xmm0, xmm1       ; only the low float

; Compare:
cmpps xmm0, xmm1, 0   ; equal (4 comparisons)

; Convert:
cvtsi2ss xmm0, eax     ; int → float
cvtss2si eax, xmm0     ; float → int

SSE processes 4 floats simultaneously (SIMD). XMM0-15 = 128 bits. movaps requires 16 alignment. Suffixes: ps = packed (4), ss = scalar (1). 4x faster than scalar for vectors.

Linking and ELF
; Assemble:
nasm -f elf64 main.asm -o main.o

; Link (without libc):
ld main.o -o main

; Link (with libc):
gcc main.o -o main -no-pie

; Multiple files:
nasm -f elf64 a.asm -o a.o
nasm -f elf64 b.asm -o b.o
gcc a.o b.o -o app -no-pie

; View symbols:
nm main             ; list symbols
readelf -s main.o   ; symbol table
objdump -d main     ; disassemble

; Declare external:
extern printf       ; comes from another file
global main         ; export to the linker

nasm -f elf64 generates an ELF object. ld = link without libc (syscalls only). gcc = link with libc (printf, malloc). extern imports, global exports. -no-pie avoids PIE (simpler).

AVX (256-bit)
; AVX: 256-bit (YMM0-YMM15)
; 8 floats or 4 doubles per register

; Operations (V prefix):
vaddps ymm0, ymm1, ymm2   ; add 8 floats
vmulps ymm0, ymm1, ymm2   ; mult 8 floats
vsubps ymm0, ymm1, ymm2   ; sub 8 floats

; Load:
vmovaps ymm0, [data]      ; aligned (32 bytes)
vmovups ymm0, [data]      ; unaligned

; Broadcast (1 value → all):
vbroadcastss ymm0, [val]  ; 1 float → 8

; IMPORTANT: clear the upper bits!
vzeroupper                  ; before SSE code
; Avoids the SSE↔AVX transition penalty

; Align the data:
align 32
data: dd 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0

AVX uses YMM (256 bits) = 8 floats. V prefix on the instructions. vzeroupper is MANDATORY when mixing with SSE (avoids a penalty). Align data to 32 bytes. 8x throughput vs scalar.

Performance and profiling
; RDTSC: timestamp counter (CPU cycles)
rdtsc               ; EDX:EAX = cycles
shl rdx, 32
or rax, rdx         ; rax = 64-bit counter
mov [start], rax

; ... code to measure ...

rdtsc
shl rdx, 32
or rax, rdx
sub rax, [start]    ; rax = cycles spent

; Performance tips:
; 1. Avoid branches (cmov, setcc)
; 2. Align loops to 16 bytes
; 3. Unroll small loops
; 4. Prefetch data (prefetcht0 [rax])
; 5. Avoid dependencies (ILP)

; Tools: perf stat, perf record
; perf stat ./program

RDTSC reads the CPU cycle counter. Measure before/after for profiling. Tips: avoid branches, align loops, unroll, prefetch. perf stat shows IPC, cache misses. ILP: independent instructions in parallel.