Cheatsheet Assembly
Linguagem de baixo nível com acesso direto ao hardware (x86/NASM)
Assembly
Basic
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 16db/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
syscallsys_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
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, 5db = 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 32STRUC/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 .nextPointers 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], 0BSS 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 elementsArrays 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
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=0INC/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
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 automaticallyJMP 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 .loopLOOP = 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 elseIF/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 (&&, ||)
; 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
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 bytesCALL 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
retLocal 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, eaxVariadic 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
retPrologue: 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 stackRecursion: 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-pieextern 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
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=02000sys_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 pointerssys_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
syscallsys_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
syscallSyscalls 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 NULsys_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
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)
retstrlen 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.14printf: 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
retstrcpy 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 correctlyAssembly 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_strcmpstrcmp compares byte by byte until a difference or NUL. Returns 0 (equal), negative (s1movzx 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_loopCMPSB 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=str1
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
retitoa 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
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.