Cheatsheet Assembly
Linguagem de baixo nível com acesso direto ao hardware (x86/NASM)
Assembly
Básico
Registros x86-64
; Registros de 64 bits (GPR): ; RAX RBX RCX RDX RSI RDI RSP RBP ; R8 R9 R10 R11 R12 R13 R14 R15 ; Sub-registros: ; EAX (32b) | AX (16b) | AH/AL (8b) ; Propósito: ; RAX - retorno de funciones ; RCX - contador (loops) ; RSP - stack pointer ; RBP - base pointer (frame) ; RSI/RDI - source/destination
Los registros son la memoria más rápida de la CPU. RAX es el acumulador (retorno). RSP apunta a la cima de la stack. R8-R15 son exclusivos de 64 bits. Acceder a EAX pone a cero los 32 bits superiores.
Directivas NASM
; Definir datos:
db 0x41 ; 1 byte ('A')
dw 1000 ; 2 bytes (word)
dd 3.14 ; 4 bytes (float)
dq 123456789 ; 8 bytes (qword)
; Reservar espacio:
resb 16 ; 16 bytes (bss)
resw 8 ; 8 words
resq 1 ; 1 qword
; Constantes:
MAX: equ 100
TAM: equ $ - inicio
; Alineación:
align 16db/dw/dd/dq definen datos de 1/2/4/8 bytes. resb/resw/resq reservan espacio en BSS. equ crea constantes. align alinea para rendimiento.
Flags (EFLAGS/RFLAGS)
; Flags principales: ; ZF - Zero Flag (resultado = 0) ; CF - Carry Flag (overflow unsigned) ; SF - Sign Flag (bit de signo) ; OF - Overflow Flag (overflow signed) ; PF - Parity Flag (paridad baja) ; Instrucciones que afectan flags: add rax, rbx ; ZF, CF, SF, OF sub rax, rbx ; ZF, CF, SF, OF cmp rax, rbx ; como sub, descarta resultado test rax, rax ; como and, descarta (ZF/SF) ; Ver flags: pushfq ; push flags a la stack pop rax ; flags en rax
Las Flags son bits que indican resultados. ZF=1 si cero. CF=1 si carry/borrow. SF=1 si negativo. OF=1 si overflow signed. CMP y TEST solo afectan flags.
Estructura NASM
; fichero.asm — estructura básica NASM
section .data ; datos inicializados
msg: db "Hola", 10
len: equ $ - msg
section .bss ; datos no inicializados
buffer: resb 64
section .text ; código
global _start
_start:
; instrucciones aquí
mov rax, 60 ; sys_exit
xor rdi, rdi ; código 0
syscall.data tiene datos con valor inicial. .bss reserva espacio (cero). .text contiene el código. global _start exporta el punto de entrada. section organiza el binario.
Modos de direccionamiento
mov rax, [rbx] ; indirecto (contenido de rbx) mov rax, [rbx + 8] ; desplazamiento mov rax, [rbx + rcx*4] ; base + índice*escala mov rax, [rbx + rcx*4 + 16] ; completo ; RIP-relative (64-bit): mov rax, [rel variable] ; Inmediato: mov rax, 42 ; valor directo ; Registro: mov rax, rbx ; registro → registro
Direccionamiento: [reg] = indirecto, [reg+N] = desplazamiento, [base+idx*escala] = arrays. RIP-relative es el estándar en 64-bit para datos globales. Escalas: 1, 2, 4, 8.
Comentarios y convenciones
; Comentario de línea (punto y coma) /* Comentario multi-línea (soportado por NASM) */ ; Convenciones de nombres: ; _start → punto de entrada (Linux) ; main → punto de entrada (C) ; snake_case → funciones y labels ; CONSTANTES → equ en mayúsculas ; Labels: loop_inicio: ; label local .fin: ; label local (punto) @@: ; label anónimo jmp @B ; salta al @@ anterior
; inicia un comentario. Los labels terminan con :. .nombre son locales (scope entre labels). @@ es anónimo (@F = forward, @B = backward). Convención: snake_case para símbolos.
Compilar y ejecutar (NASM)
; Ensamblar (assembly → objeto): nasm -f elf64 programa.asm -o programa.o ; Enlazar (objeto → ejecutable): ld programa.o -o programa ; Ejecutar: ./programa ; Con GCC (si usa funciones C): nasm -f elf64 -g -F dwarf main.asm gcc main.o -o app -no-pie ; Ver código máquina: objdump -d programa
nasm -f elf64 genera un objeto 64-bit Linux. ld enlaza sin libc. gcc si necesitas printf/malloc. -g -F dwarf añade debug info. objdump -d desensambla.
MOV y transferencia
mov rax, 42 ; inmediato → registro mov rax, rbx ; registro → registro mov rax, [mem] ; memoria → registro mov [mem], rax ; registro → memoria ; Variantes con extensión: movzx eax, byte [x] ; zero-extend (8→32) movsx eax, byte [x] ; sign-extend (8→32) movsxd rax, eax ; sign-extend (32→64) ; LEA (dirección, no contenido): lea rax, [rbx + rcx*4 + 10]
MOV copia datos (¡no mueve!). movzx extiende con ceros. movsx extiende con signo. LEA calcula una dirección sin acceder a la memoria — útil para aritmética rápida.
Hello World
section .data
msg: db "¡Hola, Mundo!", 10
len: equ $ - msg
section .text
global _start
_start:
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout (fd=1)
mov rsi, msg ; puntero a string
mov rdx, len ; longitud
syscall
mov rax, 60 ; sys_exit
xor rdi, rdi ; exit code 0
syscallsys_write (rax=1) imprime. rdi=fd (1=stdout), rsi=buffer, rdx=tamaño. syscall invoca al kernel. sys_exit (rax=60) termina. $ - msg calcula la longitud.
Tamaños y sufijos
; Tamaños explícitos: mov byte [x], 0x41 ; 1 byte mov word [x], 0x1234 ; 2 bytes mov dword [x], 0xABCD ; 4 bytes mov qword [x], 0xFF ; 8 bytes ; En un registro (implícito): mov al, 1 ; 8-bit mov ax, 1 ; 16-bit mov eax, 1 ; 32-bit (¡pone a cero upper 32!) mov rax, 1 ; 64-bit ; NASM usa byte/word/dword/qword ptr ; MASM usa PTR: mov [x], DWORD PTR 0
Sufijos: byte=1, word=2, dword=4, qword=8 bytes. Escribir en EAX pone a cero automáticamente los 32 bits superiores de RAX. En NASM, el tamaño viene del registro o es explícito.
Dados e Memória
Definir variables
section .data
; Strings:
nombre: db "Ana", 0 ; null-terminated
msg: db "Hola", 10 ; con newline
; Numéricos:
edad: db 30 ; byte
anio: 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. Los strings son arrays de bytes. 0 al final = null-terminated. Los arrays son datos contiguos.
Structs en Assembly
; Simular un struct con offsets:
; struct Persona { char nombre[32]; int edad; }
STRUC Persona
.nombre: resb 32
.edad: resd 1
.size:
ENDSTRUC
section .bss
p: resb Persona.size
section .text
; Acceso:
mov dword [p + Persona.edad], 30
lea rdi, [p + Persona.nombre]
; Offset manual:
; nombre = offset 0
; edad = offset 32STRUC/ENDSTRUC definen structs en NASM. Los campos tienen offsets automáticos (Persona.edad). Persona.size da el tamaño total. Acceso vía [base + offset]. Equivale a structs en C.
Aritmética de punteros
; Puntero en rdi, recorrer array:
mov rdi, arr ; rdi = &arr[0]
mov rcx, 5 ; 5 elementos
.loop:
mov eax, [rdi] ; leer elemento actual
add eax, 1 ; procesar
mov [rdi], eax ; escribir de vuelta
add rdi, 4 ; siguiente (dword = +4)
dec rcx
jnz .loop
; Alternativa con índice:
xor rcx, rcx
.next:
mov eax, [arr + rcx*4]
inc rcx
cmp rcx, 5
jl .nextLos punteros avanzan por el tamaño del tipo: +4 para dword, +8 para qword. Alternativa: índice con [base + idx*escala]. Ambos equivalentes. Un puntero es más natural en Assembly.
BSS (reservar espacio)
section .bss
; Reservar sin inicializar (cero):
buffer: resb 256 ; 256 bytes
contador: resd 1 ; 1 dword (4 bytes)
matriz: resq 100 ; 100 qwords (800 bytes)
array: resw 10 ; 10 words
; Ventajas del BSS:
; • No ocupa espacio en el binario
; • Inicializado a cero por el OS
; • Más rápido que .data para buffers
; Acceso:
mov byte [buffer], 'A'
mov dword [contador], 0BSS reserva espacio sin valor inicial (cero por el OS). resb/resw/resd/resq reservan 1/2/4/8 bytes por elemento. No aumenta el binario. Ideal para buffers y arrays grandes.
Pila (push/pop)
; PUSH: decrementa RSP, escribe push rax ; RSP -= 8; [RSP] = RAX push rbx push rcx ; POP: lee, incrementa RSP pop rcx ; RCX = [RSP]; RSP += 8 pop rbx pop rax ; Ver la cima sin quitar: mov rax, [rsp] ; peek ; Reservar espacio local: sub rsp, 32 ; 32 bytes para locales ; ... usar [rsp], [rsp+8], etc. add rsp, 32 ; liberar (epilogue)
PUSH coloca en la pila (RSP -= 8). POP quita (RSP += 8). LIFO: último en entrar, primero en salir. La pila crece hacia abajo (direcciones menores). sub rsp, N reserva espacio para variables locales.
Datos alineados
section .data
align 16 ; alinear a 16 bytes
vec4: dd 1.0, 2.0, 3.0, 4.0
align 32 ; para 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
; Alinear la pila (obligatorio en llamadas):
and rsp, -16 ; alinear RSP a 16 bytes
; ... llamar funciones ...
; Por qué: SIMD requiere alineación
; ¡movaps falla si no está alineado!align 16 alinea datos a 16 bytes (SSE). align 32 para AVX. and rsp, -16 alinea la pila antes de llamadas. movaps falla con segfault si está desalineado. Rendimiento: cache lines de 64 bytes.
Arrays e indexación
section .data
arr: dd 10, 20, 30, 40, 50
section .text
; Acceso por índice:
mov rax, [arr] ; arr[0] = 10
mov rax, [arr + 4] ; arr[1] = 20
mov rax, [arr + 8] ; arr[2] = 30
; Con índice variable (i en rcx):
mov rcx, 2
mov eax, [arr + rcx*4] ; arr[2] = 30
; Escritura:
mov dword [arr + rcx*4], 99 ; arr[2] = 99
; Tamaño:
len: equ ($ - arr) / 4 ; 5 elementosLos arrays son memoria contigua. [arr + idx*4] para dword (4 bytes por elemento). Escala: *1 (byte), *2 (word), *4 (dword), *8 (qword). $ - arr da el tamaño en bytes.
Acceso a memoria
; Leer de memoria: mov rax, [variable] ; 8 bytes mov eax, [variable] ; 4 bytes mov al, byte [variable] ; 1 byte ; Escribir en memoria: mov [variable], rax mov byte [buffer + 5], 'X' ; Direccionamiento RIP-relative (64-bit): mov rax, [rel contador] ; posición-independiente ; Con desplazamiento negativo: mov rax, [rbp - 8] ; variable local mov rax, [rbp + 16] ; argumento
Los corchetes [] = acceso a memoria. Sin corchetes = valor inmediato o registro. [rel var] es RIP-relative (código posición-independiente). [rbp-N] para locales, [rbp+N] para argumentos.
LEA (cargar dirección)
; LEA calcula la dirección SIN acceder a la memoria: lea rax, [rbx + rcx*4 + 8] ; Uso para aritmética (¡rápido!): 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 (accede a memoria): mov rax, [rbx + rcx*4] ; carga VALOR lea rax, [rbx + rcx*4] ; carga DIRECCIÓN ; Puntero a una variable: lea rdi, [rel msg] ; dirección de msg
LEA calcula la dirección sin leer memoria. Más rápido que MOV para aritmética (1 ciclo). Multiplica por constantes: [rax+rax*4] = ×5. No afecta flags. Muy usado para &variable.
Constantes y EQU
; Constantes (no ocupan memoria): MAX: equ 100 NEWLINE: equ 10 NULL: equ 0 STDIN: equ 0 STDOUT: equ 1 STDERR: equ 2 ; Cálculo en compilación: TAM: equ 1024 * 4 OFFSET: equ 8 + 16 ; Uso: mov rcx, MAX cmp rax, TAM ; %define (macro, redefinible): %define VERSION 2
equ define constantes en tiempo de ensamblado (sin memoria). Sustituidas textualmente. %define es redefinible (macro). Ideal para syscalls, tamaños y offsets. No confundir con variables en .data.
Aritmética e Lógica
ADD y SUB
add rax, 10 ; rax += 10 add rax, rbx ; rax += rbx sub rax, 5 ; rax -= 5 sub rax, rcx ; rax -= rcx ; Flags afectadas: ZF, CF, SF, OF add rax, rbx ; ZF=1 si resultado=0 ; CF=1 si overflow unsigned ; SF=1 si bit 63=1 (negativo) ; OF=1 si overflow signed ; ADD con memoria: add dword [contador], 1 sub qword [saldo], rax
ADD suma, SUB resta. Ambos afectan flags: ZF (cero), CF (carry), SF (signo), OF (overflow). ADD reg, mem y ADD mem, reg son válidos.
AND, OR, XOR, NOT
and rax, 0xFF ; máscara (mantener 8 bits) and rax, -16 ; alinear a 16 (limpiar 4 bits) or rax, 0x01 ; establecer bit 0 or rax, rbx ; combinar bits xor rax, rax ; ¡poner a cero (más rápido que mov!) xor eax, eax ; pone a cero el RAX entero (32→64) not rax ; invertir todos los bits ; Probar un bit: test rax, 0x04 ; ¿está el bit 2 establecido? jnz .bit_set ; ZF=0 si bit=1
AND = máscara, OR = establecer bits, XOR = toggle/poner a cero, NOT = invertir. xor rax,rax es la forma más rápida de poner a cero (1 byte, 1 ciclo). TEST = AND sin guardar el resultado.
ADC y SBB (carry)
; ADC: suma + carry flag ; Para suma de 128 bits: mov rax, [num1] add rax, [num2] ; suma low mov rdx, [num1+8] adc rdx, [num2+8] ; suma high + carry ; SBB: resta + borrow mov rax, [num1] sub rax, [num2] ; sub low mov rdx, [num1+8] sbb rdx, [num2+8] ; sub high - borrow ; Uso: aritmética de precisión arbitraria ; (números mayores que 64 bits)
ADC = ADD + Carry Flag. SBB = SUB + Borrow. Usados para aritmética multi-precisión (128+ bits). Patrón: ADD en el low, ADC en el high. Esencial para criptografía y big numbers.
MUL e IMUL
; MUL (unsigned): resultado en RDX:RAX mov rax, 100 mov rbx, 200 mul rbx ; RDX:RAX = RAX * RBX ; IMUL (signed, más versátil): imul rax, rbx ; rax *= rbx imul rax, rbx, 10 ; rax = rbx * 10 imul rax, rcx ; rax = rax * rcx ; 32-bit: resultado en EDX:EAX mov eax, 50000 mov ebx, 60000 mul ebx ; EDX:EAX = EAX * EBX ; Overflow: CF=OF=1 si el resultado no cabe
MUL = unsigned, resultado en RDX:RAX (128 bits). IMUL = signed, más flexible (2 o 3 operandos). Para multiplicar por potencias de 2, usa SHL (más rápido).
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 ; shift aritmético (preserva signo) ; sar -8, 1 = -4 (correcto para negativos) ; shr -8, 1 = valor enorme (¡incorrecto!) ; Rotación: rol rax, 1 ; rotate left (bit sale → entra) ror rax, 4 ; rotate right ; SHLD/SHRD: shift doble shld rax, rbx, 4 ; shift left con bits de rbx
SHL = ×2^n, SHR = ÷2^n (unsigned). SAR preserva el signo (para negativos). ROL/ROR rotan bits. Los shifts son mucho más rápidos que MUL/DIV para potencias de 2.
BSR, BSF, POPCNT
; Bit Scan: bsf rcx, rax ; índice del primer bit 1 (low) bsr rcx, rax ; índice del último bit 1 (high) ; ZF=1 si input=0 ; Popcount (contar bits 1): popcnt rcx, rax ; rcx = nº de bits a 1 ; Requiere: -mpopcnt o CPUID check ; Contar ceros: lzcnt rcx, rax ; leading zeros tzcnt rcx, rax ; trailing zeros (= bsf) ; Extraer un bit: bt rax, 5 ; CF = bit 5 de rax bts rax, 5 ; establece bit 5 btr rax, 5 ; limpia bit 5 btc rax, 5 ; toggle bit 5
BSF/BSR encuentran el primer/último bit 1. POPCNT cuenta bits activos. LZCNT/TZCNT cuentan ceros. BT/BTS/BTR manipulan bits individuales. Útiles para bitmaps y flags.
DIV e IDIV
; DIV (unsigned): ; Divide RDX:RAX por el operando ; Cociente → RAX, Resto → RDX mov rax, 100 xor rdx, rdx ; ¡PONER A CERO RDX primero! 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 antes de div ; ¡Cuidado: div por 0 = excepción #DE!
DIV divide RDX:RAX por el operando. Cociente en RAX, resto en RDX. Siempre poner a cero RDX (xor rdx,rdx) o sign-extend (cqo). La división por 0 causa una excepción.
CMP y TEST
; CMP: resta sin guardar (solo flags) cmp rax, 10 ; ZF=1 si rax==10 ; CF=1 si rax<10 (unsigned) ; SF≠OF si rax<10 (signed) cmp rax, rbx je .igual ; ZF=1 jl .menor ; SF≠OF (signed) ja .mayor ; CF=0 y ZF=0 (unsigned) ; TEST: AND sin guardar (solo flags) test rax, rax ; ¿es rax cero? jz .nulo ; ZF=1 si rax=0 test rax, 1 ; ¿es impar? jnz .impar ; bit 0 = 1
CMP compara (hace SUB sin guardar). TEST verifica bits (hace AND sin guardar). Ambos solo afectan flags. Seguidos de saltos condicionales. test rax,rax + jz = verificar null/cero.
INC, DEC y NEG
inc rax ; rax++ (¡no afecta CF!)
dec rcx ; rcx-- (¡no afecta CF!)
neg rax ; rax = -rax (complemento a 2)
; neg: CF=0 si input=0, CF=1 en caso contrario
; Alternativas que afectan CF:
add rax, 1 ; como inc, pero afecta CF
sub rax, 1 ; como dec, pero afecta CF
; Uso típico:
mov rcx, 10
.loop:
; procesar
dec rcx
jnz .loop ; ZF=1 cuando rcx=0INC/DEC incrementan/decrementan sin afectar CF (útil tras ADC/SBB). NEG niega (complemento a 2). En loops, DEC + JNZ es el patrón. ADD/SUB 1 si necesitas CF.
Conversiones de tipo
; Extensión con signo: 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) ; Extensión con cero: movzx eax, byte [x] ; uint8 → uint32 movzx rax, word [y] ; uint16 → uint64 ; Preparar división: xor rdx, rdx ; poner a cero RDX (unsigned) cqo ; RDX:RAX = sign-extend (signed) cdq ; EDX:EAX = sign-extend (32-bit)
MOVSX extiende con signo (correcto para negativos). MOVZX extiende con ceros (unsigned). CQO/CDQ preparan RDX para la división. Usa siempre la extensión correcta para evitar bugs.
Controlo de Fluxo
JMP (incondicional)
; Salto incondicional:
jmp .fin ; label local
jmp inicio ; label global
jmp rax ; indirecto (dirección en rax)
jmp [rax] ; indirecto vía memoria
; Labels:
inicio:
; código
.fin:
; código
; Corto vs largo:
jmp short .cerca ; -128 a +127 bytes (2 bytes)
jmp near .lejos ; ±2GB (5 bytes)
; NASM elige automáticamenteJMP salta incondicionalmente (como goto). jmp label = directo. jmp reg = indirecto (tablas de salto, vtables). short = 2 bytes (limitado). Labels locales con prefijo ..
Instrucción LOOP
; LOOP: decrementa RCX, salta si != 0
mov rcx, 5
.loop:
; cuerpo (ejecuta 5 veces)
mov rax, rcx
loop .loop ; rcx--; if rcx!=0 jmp
; Variantes:
loope .loop ; loop si RCX!=0 Y ZF=1
loopz .loop ; sinónimo de LOOPE
loopne .loop ; loop si RCX!=0 Y ZF=0
loopnz .loop ; sinónimo de LOOPNE
; Nota: ¡LOOP es lento en CPUs modernas!
; Preferir: dec rcx / jnz .loopLOOP = DEC RCX + JNZ implícito. LOOPE/LOOPNE verifican ZF también. En CPUs modernas, LOOP es más lento que DEC+JNZ (microcode). Usar solo por legibilidad en código educativo.
Ternario (CMOV)
; rax = (rbx > 10) ? 1 : 0
; Con jumps:
cmp rbx, 10
jle .cero
mov rax, 1
jmp .fin
.cero:
xor rax, rax
.fin:
; Con CMOV (sin branch, más rápido):
xor rax, rax ; rax = 0 (default)
mov rcx, 1
cmp rbx, 10
cmovg rax, rcx ; si >, rax = 1
; CMOVcc: move si condición (¡sin salto!)
; cmovl, cmovg, cmove, cmovne, cmova...CMOVcc = move condicional sin branch. Evita la misprediction del branch predictor. cmovg = move if greater. Ideal para ternarios simples. Sin branch = el pipeline no se detiene. Prefiérelo para expresiones simples.
Saltos condicionales (igualdad)
cmp rax, 10 je .igual ; jump if equal (ZF=1) jne .diferente ; jump if not equal (ZF=0) ; Sinónimos: jz .cero ; jump if zero (ZF=1) = JE jnz .no_cero ; jump if not zero (ZF=0) = JNE ; Tras TEST: test rax, rax jz .nulo ; rax == 0? jnz .válido ; rax != 0? ; Verificar un bit: test rax, 0x04 jnz .bit2_set ; ¿bit 2 establecido?
JE/JNE = igual/diferente (tras CMP). JZ/JNZ = cero/no-cero (tras TEST). Son sinónimos (mismo opcode, ZF). Usa el nombre que tenga más sentido en el contexto.
IF/ELSE en Assembly
; if (rax > 10) { rbx = 1 } else { rbx = 0 }
cmp rax, 10
jle .else ; si rax <= 10, va a else
.then:
mov rbx, 1
jmp .fin ; ¡salta el else!
.else:
mov rbx, 0
.fin:
; continúa aquí
; Patrón: la condición NEGADA salta a else
; jmp .fin en then evita ejecutar el elseIF/ELSE: niega la condición y salta a .else. En el bloque then, JMP .fin para no caer en el else. Los labels con . son locales. Equivale a if (cond) {...} else {...} en C.
SETcc (byte condicional)
; SETcc: pone un byte a 0 o 1 cmp rax, rbx sete al ; al = 1 si igual, 0 si no movzx rax, al ; extender a 64 bits setl cl ; cl = 1 si signed less setg dl ; dl = 1 si signed greater flecha bl ; bl = 1 si unsigned above setne sil ; sil = 1 si no igual ; Booleano → entero: xor eax, eax cmp rcx, 0 setnz al ; al = (rcx != 0) ? 1 : 0 ; Combinar con aritmética: ; rax += (rbx > 5) cmp rbx, 5 setg cl movzx rcx, cl add rax, rcx
SETcc pone un byte (AL, CL, etc.) a 0 o 1 según las flags. Seguido de MOVZX para usarlo como entero. Sin branches. Útil para contar condiciones y booleanos. Variantes: SETE, SETL, SETG, SETA.
Saltos condicionales (comparación)
; SIGNED (tras CMP): jl .menor ; jump if less (SF≠OF) jle .menor_igual ; less or equal (ZF=1 o SF≠OF) jg .mayor ; jump if greater (ZF=0 y SF=OF) jge .mayor_igual ; greater or equal (SF=OF) ; UNSIGNED (tras CMP): jb .debajo ; jump if below (CF=1) jbe .debajo_igual ; below or equal (CF=1 o ZF=1) ja .encima ; jump if above (CF=0 y ZF=0) jae .encima_igual ; above or equal (CF=0) ; Mnemónicos: L/G = signed, B/A = unsigned
Signed: JL/JG/JLE/JGE. Unsigned: JB/JA/JBE/JAE. L/G para negativos correctos. B/A para direcciones/tamaños. Siempre tras CMP.
Switch/Case (tabla de saltos)
section .data
jump_table: dq .case0, .case1, .case2, .case3
section .text
; rax = índice (0-3)
cmp rax, 3
ja .default ; fuera del rango
jmp [jump_table + rax*8] ; salto indirecto
.case0:
mov rbx, 10
jmp .fin
.case1:
mov rbx, 20
jmp .fin
.case2:
mov rbx, 30
jmp .fin
.default:
mov rbx, 0
.fin:Switch usa una tabla de direcciones + JMP [tabla + idx*8]. Verifica el rango primero (CMP + JA). Cada case termina con JMP .fin (evita fall-through). O(1) para cualquier case. Como una vtable.
Loops con DEC/JNZ
; Loop N veces (patrón):
mov rcx, 10 ; contador
.loop:
; cuerpo del loop
mov rax, rcx ; usar contador
dec rcx ; rcx--
jnz .loop ; repite si rcx != 0
; Loop con índice creciente:
xor rcx, rcx ; i = 0
.next:
mov rax, [arr + rcx*4]
; procesar
inc rcx
cmp rcx, 10
jl .next ; mientras i < 10
; Loop infinito:
.inf:
jmp .inf ; o: jmp $Patrón: DEC + JNZ (countdown). Alternativa: INC + CMP + JL (countup). JMP $ = loop infinito (salta a sí mismo). El countdown es más eficiente (menos instrucciones).
Cortocircuito (&&, ||)
; if (a > 0 && b > 0) { ... }
cmp rax, 0
jle .fin ; si a<=0, FALSO (cortocircuito)
cmp rbx, 0
jle .fin ; si b<=0, FALSO
; ambos verdaderos:
; cuerpo del if
.fin:
; if (a == 1 || b == 2) { ... }
cmp rax, 1
je .verdadero ; si a==1, VERDADERO (cortocircuito)
cmp rbx, 2
je .verdadero ; si b==2, VERDADERO
jmp .fin
.verdadero:
; cuerpo del if
.fin:&&: salta al fin en la primera condición falsa. ||: salta al cuerpo en la primera verdadera. Cortocircuito: la segunda condición solo se evalúa si es necesario. El mismo patrón que generan los compiladores C.
Funções e Stack
CALL y RET
; CALL: push RIP, salta a la función
call mi_funcion ; llama
; ... continúa aquí tras RET
; RET: pop RIP, vuelve al caller
mi_funcion:
mov rax, 42 ; valor de retorno
ret ; vuelve
; CALL indirecto:
call rax ; dirección en registro
call [rax] ; dirección en memoria
; RET con limpieza (stdcall, raro en x64):
ret 8 ; pop + quita 8 bytesCALL guarda la dirección de retorno en la pila y salta. RET hace pop de esa dirección y vuelve. RAX es el registro de retorno. En x86-64, CALL es siempre relativo (±2GB).
Variables locales
func:
push rbp
mov rbp, rsp
sub rsp, 48 ; 6 qwords locales
; Locales (offsets negativos de RBP):
mov [rbp-8], rdi ; local1 = arg1
mov [rbp-16], 0 ; local2 = 0
mov qword [rbp-24], 100 ; local3 = 100
; Array local:
; [rbp-32] a [rbp-48] = 2 qwords
; Acceso:
mov rax, [rbp-8] ; leer local1
add rax, [rbp-24] ; local1 + local3
leave
retLas variables locales viven en [rbp-N] (offsets negativos). Reservar con sub rsp, N (¡múltiplo de 16!). El compilador aloca todo de una vez. El acceso es relativo a RBP (estable incluso con push/pop).
Funciones variádicas
; printf(fmt, ...) — args variables
; RAX = nº de registros XMM usados (0-8)
section .data
fmt: db "x=%d, y=%f", 10, 0
section .text
extern printf
mov rdi, fmt ; formato
mov esi, 42 ; int arg
movq xmm0, [pi] ; double arg
mov eax, 1 ; ¡1 registro XMM usado!
call printf
; AL (RAX low) = conteo de args XMM
; ¡Si lo olvidas → crash o basura!
; Para enteros puros: xor eax, eaxLas funciones variádicas (printf, scanf) exigen AL = el número de args en XMM. Olvidarlo causa un crash. Enteros en RDI, RSI, RDX... Floats en XMM0-7. Siempre xor eax,eax si no hay floats.
Stack frame (prologue/epilogue)
mi_funcion:
; Prologue:
push rbp ; guardar frame anterior
mov rbp, rsp ; nuevo frame pointer
sub rsp, 32 ; espacio para locales
; Cuerpo:
mov [rbp-8], rdi ; local 1 (arg1)
mov [rbp-16], rsi ; local 2 (arg2)
; Epilogue:
mov rsp, rbp ; liberar locales
pop rbp ; restaurar frame
ret
; Alternativa (leave = mov rsp,rbp + pop rbp):
leave
retPrologue: push rbp + mov rbp, rsp + sub rsp, N. Epilogue: leave + ret. RBP es estable durante la función. Locales en [rbp-N], args en [rbp+16] y superiores.
Recursión
; factorial(n): si n<=1 retorna 1, si no n*factorial(n-1)
factorial:
cmp rdi, 1
jle .base ; caso base
push rdi ; guardar n (¡caller-saved!)
dec rdi ; n - 1
call factorial ; recursión
pop rdi ; restaurar n
imul rax, rdi ; n * factorial(n-1)
ret
.base:
mov rax, 1
ret
; ¡Cuidado: stack overflow con N grande!
; Cada llamada usa ~16+ bytes de stackRecursión: guarda los registros necesarios con PUSH antes del CALL (¡son caller-saved!). Restaura con POP después. El caso base evita la recursión infinita. Cada llamada consume stack — límite práctico ~8MB.
Llamar funciones C (extern)
; Declarar una función externa:
extern printf
extern malloc
extern strlen
section .data
msg: db "Valor: %d", 10, 0
section .text
global main
main:
push rbp
mov rbp, rsp
; printf("Valor: %d", 42)
mov rdi, msg ; arg1: formato
mov esi, 42 ; arg2: valor
xor eax, eax ; 0 xmm args
call printf wrt ..plt ; PIC (Linux)
; return 0
xor eax, eax
pop rbp
ret
; Enlazar: gcc main.o -o app -no-pieextern declara funciones C. Llama con argumentos en RDI, RSI, RDX... wrt ..plt para código PIC (position-independent). Enlaza con gcc (no ld). xor eax,eax antes de variádicas.
Argumentos (System V AMD64)
; Convención Linux/macOS (System V): ; Enteros/punteros: RDI, RSI, RDX, RCX, R8, R9 ; Floats: XMM0-XMM7 ; Retorno: RAX (int), XMM0 (float) ; Extras: en la pila (derecha → izquierda) ; Ejemplo: func(a, b, c, d, e, f, g) ; a=RDI, b=RSI, c=RDX, d=RCX, e=R8, f=R9 ; g → [rsp] (en la pila) ; Windows (Microsoft x64): ; RCX, RDX, R8, R9 (¡solo 4 en registros!) ; Shadow space: 32 bytes obligatorios
Linux: 6 args enteros en RDI, RSI, RDX, RCX, R8, R9. Retorno en RAX. Windows usa RCX, RDX, R8, R9 + shadow space. Floats en XMM0-7.
Leaf function (sin CALL)
; Leaf = no llama a otras funciones
; ¡No necesita un prologue completo!
; Versión mínima:
suma:
lea rax, [rdi + rsi] ; rax = a + b
ret
; Con red zone (128 bytes libres):
; Debajo de RSP, sin push, seguro en leaf
leaf_func:
mov [rsp-8], rdi ; ¡red zone!
mov [rsp-16], rsi
; procesar sin sub rsp
ret
; Red zone: 128 bytes debajo de RSP
; Solo seguro si NO llama a funciones
; (los signal handlers pueden corromperla)Una leaf function no hace CALL — puede omitir el prologue. Red zone: 128 bytes debajo de RSP son seguros en un leaf (System V). Sin push/pop = más rápido. Ideal para funciones simples (getters, cálculos).
Callee-saved vs Caller-saved
; Callee-saved (¡preservar si los usas!):
; RBX, RBP, R12, R13, R14, R15
; (también RSP implícitamente)
; Caller-saved (pueden ser alterados):
; RAX, RCX, RDX, RSI, RDI, R8-R11
; Ejemplo: función que usa RBX
func:
push rbx ; ¡preservar!
mov rbx, rdi ; usar rbx
call otra_func ; rbx seguro
mov rax, rbx ; aún válido
pop rbx ; restaurar
ret
; ¡Si no preservas → bug intermitente!Callee-saved: RBX, RBP, R12-R15 — la función llamada DEBE preservarlos. Caller-saved: RAX, RCX, RDX, RSI, RDI, R8-R11 — pueden ser destruidos por un CALL. Siempre push/pop de los callee-saved.
Alineación de stack
; El ABI exige RSP alineado a 16 bytes ; ANTES de un CALL (tras CALL, RSP%16 = 8) ; Verificar: ; rsp % 16 == 0 antes de call → correcto ; Si está desalineado: sub rsp, 8 ; alinear (si es necesario) call func add rsp, 8 ; restaurar ; Al entrar en la función (tras CALL): ; RSP % 16 == 8 (el CALL hizo push de 8) ; push rbp → RSP % 16 == 0 ✓ ; Por qué: movaps/movdqa requieren alineación ; SSE/AVX fallan con una stack desalineada
La stack DEBE estar alineada a 16 bytes antes de un CALL. Tras CALL, RSP%16=8. El prologue (push rbp) la realinea. Si haces sub rsp, N, N debe ser múltiplo de 16. movaps causa un segfault si está desalineado.
System Calls
Syscall (visión general)
; Syscalls Linux x86-64: ; RAX = número de la syscall ; RDI, RSI, RDX, R10, R8, R9 = argumentos ; syscall → invoca al kernel ; Retorno en RAX (negativo = error) ; Syscalls comunes: ; 0 = read ; 1 = write ; 2 = open ; 3 = close ; 39 = getpid ; 60 = exit ; 63 = uname ; Tabla completa: ; /usr/include/asm/unistd_64.h
syscall invoca al kernel Linux. Número en RAX, args en RDI, RSI, RDX, R10, R8, R9. Retorno en RAX (negativo = errno). Destruye RCX y R11.
sys_open y sys_close
section .data
filename: db "/tmp/prueba.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 ; permisos (octal)
syscall
; rax = file descriptor (o -errno)
mov rdi, rax ; guardar el fd
; ... escribir en el fichero ...
; close(fd)
mov rax, 3 ; sys_close
; rdi ya tiene el 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, modo. Retorna fd (≥0) o -errno. sys_close (rax=3): fd en rdi. Flags: O_RDONLY=0, O_WRONLY=1, O_CREAT=0100. ¡Siempre cerrar fds!
sys_execve
section .data
path: db "/bin/echo", 0
arg0: db "echo", 0
arg1: db "¡Hola!", 0
section .text
; execve(path, argv, envp)
mov rax, 59 ; sys_execve
lea rdi, [rel path] ; filename
; argv = {"echo", "¡Hola!", NULL}
lea rsi, [rel argv] ; puntero al array
; envp = NULL (sin entorno)
xor rdx, rdx
syscall
; Si retorna → ¡ERROR! (execve no vuelve)
section .data
argv: dq arg0, arg1, 0 ; array de punterossys_execve (59) reemplaza el proceso por otro. rdi=path, rsi=argv (array de punteros, NULL-terminated), rdx=envp. Si retorna, es un error. Nunca vuelve en caso de éxito. Combinar con fork.
sys_write (imprimir)
section .data
msg: db "¡Hola, Mundo!", 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 ; tamaño
syscall
; rax = bytes escritos (o -errno)
cmp rax, 0
jl .error ; verificar 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=tamaño. Retorna bytes escritos. Verificar rax < 0 para errores. No añade un newline automáticamente.
sys_mmap (asignar memoria)
; mmap(NULL, size, PROT, FLAGS, fd, offset) mov rax, 9 ; sys_mmap xor rdi, rdi ; addr = NULL (el kernel elige) mov rsi, 4096 ; size = 4KB mov rdx, 3 ; PROT_READ|PROT_WRITE mov r10, 0x22 ; MAP_PRIVATE|MAP_ANONYMOUS mov r8, -1 ; fd = -1 (anónimo) xor r9, r9 ; offset = 0 syscall ; rax = dirección de memoria (o -errno) ; Usar: mov [rax], byte 'A' ; escribir ; Liberar: 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) asigna memoria. MAP_ANONYMOUS = sin fichero. Retorna una dirección o -errno. sys_munmap (rax=11) la libera. Equivale a malloc (que usa mmap internamente). Páginas de 4096 bytes.
Manejo de errores
; Una syscall retorna -errno en RAX
; (valores negativos: -1 a -4095)
mov rax, 2 ; sys_open
lea rdi, [rel path]
xor esi, esi ; O_RDONLY
syscall
cmp rax, 0
jl .error ; negativo = error
mov rdi, rax ; fd válido
jmp .continuar
.error:
neg rax ; rax = errno positivo
; errno comunes:
; 2 = ENOENT (el fichero no existe)
; 13 = EACCES (permiso denegado)
; 9 = EBADF (fd inválido)
; Escribir el error en stderr:
mov rax, 1 ; sys_write
mov rdi, 2 ; stderr
lea rsi, [rel errmsg]
mov rdx, errmsg_len
syscallLas syscalls retornan -errno en RAX (negativo = error). Verificar con cmp rax, 0 + jl. neg rax da el errno positivo. Comunes: 2=ENOENT, 13=EACCES, 9=EBADF. ¡Siempre verificar en producción!
sys_read (leer 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 leídos (incluye \n)
; rax = 0 → EOF
; rax < 0 → error
; Quitar el newline:
lea rdi, [rel buffer]
add rdi, rax ; posición del \n
dec rdi
mov byte [rdi], 0 ; reemplazar por NULsys_read (rax=0): rdi=fd (0=stdin), rsi=buffer, rdx=máximo. Retorna bytes leídos (incluye \n). rax=0 = EOF. Buffer en BSS. Quita el newline reemplazándolo por NUL.
sys_getpid y sys_getuid
; getpid() → PID del proceso mov rax, 39 ; sys_getpid syscall ; rax = PID (siempre positivo) ; getppid() → PID del padre 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 ; Útil para: logs, nombres de ficheros temp, ; verificación de permisos, debugging
sys_getpid (39) retorna el PID. sys_getuid (102) retorna el UID (0=root). Sin argumentos — solo RAX con el número. Útiles para logs, ficheros temporales y verificación de permisos.
sys_exit
; Terminar el programa: mov rax, 60 ; sys_exit mov rdi, 0 ; exit code (0=éxito) syscall ; Exit codes convencionales: ; 0 = éxito ; 1 = error genérico ; 2 = uso incorrecto (bash) ; 126 = sin permiso ; 127 = comando no encontrado ; 128+N = matado por la señal N ; 139 = segfault (128+11) ; Alternativa (libc): ; call exit (no retorna) ; Nunca retorna — el código posterior es inalcanzable
sys_exit (rax=60): rdi = código de salida. 0=éxito, 1=error. El kernel termina el proceso y libera recursos. El código tras un syscall de exit nunca se ejecuta. En C: equivale a exit(code).
sys_fork y sys_wait4
; fork() → crea un proceso hijo
mov rax, 57 ; sys_fork
syscall
; rax = 0 → proceso hijo
; rax > 0 → proceso padre (rax = PID del hijo)
; rax < 0 → error
test rax, rax
jz .hijo
.padre:
; esperar al hijo
mov rax, 61 ; sys_wait4
mov rdi, -1 ; cualquier hijo
xor rsi, rsi ; status (NULL)
xor rdx, rdx ; options
xor r10, r10 ; rusage (NULL)
syscall
jmp .fin
.hijo:
; código del hijo
mov rax, 60
xor rdi, rdi
syscall
.fin:sys_fork (57) duplica el proceso. Retorna 0 en el hijo, el PID en el padre. sys_wait4 (61) espera al hijo. Patrón: verificar rax==0 para la rama hijo/padre. El hijo debe hacer exit o execve.
Strings
REP MOVSB (copiar)
; Copiar N bytes de RSI → RDI
section .data
src: db "Hello, World!", 0
section .bss
dst: resb 64
section .text
lea rsi, [rel src] ; origen
lea rdi, [rel dst] ; destino
mov rcx, 14 ; nº de bytes
rep movsb ; copia RCX bytes
; REP: repite hasta RCX=0
; MOVSB: mueve byte [RSI]→[RDI], inc ambos
; Dirección: DF=0 (cld) → creciente
; DF=1 (std) → decreciente
; Variantes: rep movsw (word), rep movsq (qword)REP MOVSB copia RCX bytes de [RSI] a [RDI]. REP decrementa RCX hasta 0. CLD = dirección creciente (por defecto). Equivale a memcpy. Variantes: MOVSW (2B), MOVSQ (8B).
strlen manual
; strlen: contar bytes hasta NUL
; Input: RDI = puntero a la string
; Output: RAX = longitud
my_strlen:
xor eax, eax ; contador = 0
.loop:
cmp byte [rdi + rax], 0
je .fin ; encontró NUL
inc rax
jmp .loop
.fin:
ret
; Alternativa con REPNE SCASB:
my_strlen2:
push rdi
mov al, 0
mov rcx, -1
cld
repne scasb
pop rax
sub rax, rdi ; negativo
neg rax ; positivo
dec rax ; -1 (NUL)
retstrlen cuenta bytes hasta NUL. Método manual: loop con CMP byte, 0. Método rápido: repne scasb (optimizado por hardware). Input en RDI, retorno en RAX. No incluye el NUL en el conteo.
printf con argumentos
extern printf
section .data
fmt1: db "Nombre: %s, Edad: %d", 10, 0
fmt2: db "Pi = %.2f", 10, 0
nombre: db "Ana", 0
section .text
; printf("Nombre: %s, Edad: %d", "Ana", 30)
lea rdi, [rel fmt1] ; formato
lea rsi, [rel nombre] ; %s → puntero
mov edx, 30 ; %d → entero
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: formato en RDI, args en RSI, RDX, RCX... Floats en XMM0-7. EAX = nº de args XMM (¡obligatorio!). %s = puntero, %d = int. String con NUL al final.
LODSB / STOSB
; LODSB: carga byte de [RSI] → AL, RSI++ lea rsi, [rel texto] lodsb ; al = [rsi], rsi++ lodsb ; al = siguiente byte ; STOSB: escribe AL → [RDI], RDI++ lea rdi, [rel buffer] mov al, 'A' stosb ; [rdi] = 'A', rdi++ ; REP STOSB: rellenar memoria (¡memset!) lea rdi, [rel buffer] mov al, 0 ; byte de relleno mov rcx, 256 ; 256 bytes rep stosb ; memset(buffer, 0, 256) ; REP STOSQ: más rápido (8 bytes/iter) mov rax, 0 mov rcx, 32 ; 32*8 = 256 bytes rep stosq
LODSB lee un byte de [RSI] a AL. STOSB escribe AL en [RDI]. REP STOSB = memset. REP STOSQ rellena 8 bytes por iteración (más rápido). Siempre CLD antes.
strcpy manual
; strcpy: copiar hasta NUL (inclusive)
; Input: RDI = destino, RSI = origen
my_strcpy:
push rdi ; guardar el inicio (retorno)
.loop:
lodsb ; al = [rsi], rsi++
stosb ; [rdi] = al, rdi++
test al, al ; ¿es NUL?
jnz .loop ; si no, continúa
pop rax ; retorno = inicio de dst
ret
; Alternativa con REP:
; (requiere strlen primero)
my_strcpy2:
push rdi
push rsi
call my_strlen ; rax = len (rsi)
inc rax ; incluir NUL
mov rcx, rax
pop rsi
pop rdi
rep movsb ; copiar todo
retstrcpy copia bytes hasta NUL (inclusive). LODSB+STOSB en un loop. Retorna el puntero del destino (convención C). Alternativa: strlen + rep movsb. ¡Cuidado: el buffer destino debe ser suficiente!
Strings multibyte (UTF-8)
section .data
; UTF-8: "Hola" = 4 bytes (á = 2 bytes)
msg: db "Ol", 0xC3, 0xA1, 0
; 0xC3 0xA1 = 'á' en UTF-8
; Emoji: "😀" = 4 bytes
emoji: db 0xF0, 0x9F, 0x98, 0x80, 0
; Tamaño en bytes vs caracteres:
len_bytes: equ $ - msg - 1 ; bytes
; ¡caracteres ≠ bytes para acentos!
section .text
; write no conoce UTF-8 — solo bytes
mov rax, 1
mov rdi, 1
lea rsi, [rel msg]
mov rdx, 5 ; 5 BYTES (no chars)
syscall
; El terminal interpreta UTF-8 correctamenteAssembly trabaja con bytes, no caracteres. UTF-8: acentos = 2 bytes, emoji = 4 bytes. sys_write envía bytes — el terminal interpreta UTF-8. strlen cuenta bytes, no caracteres. ¡Cuidado con la indexación!
SCASB (buscar byte)
; SCASB: compara AL con [RDI], RDI++ ; Usado para strlen y strchr ; strlen: contar hasta NUL lea rdi, [rel string] mov al, 0 ; buscar NUL mov rcx, -1 ; máximo (infinito) cld ; dirección creciente repne scasb ; repite mientras != AL ; RCX = -(strlen+2) not rcx ; invertir bits dec rcx ; strlen = ~rcx - 1 ; o: neg rcx; sub rcx, 2 ; strchr: buscar un carácter mov al, 'o' ; buscar 'o' repne scasb jnz .no_encontrado ; rdi-1 = posición del carácter
SCASB compara AL con [RDI]. REPNE = repite mientras NO igual. Para strlen: buscar NUL con repne scasb. Resultado: NOT RCX - 1 = longitud. REPE = mientras igual.
strcmp manual
; strcmp: comparar strings
; Retorno: 0=iguales, <0 si s1<s2, >0 si s1>s2
my_strcmp:
.loop:
mov al, [rdi] ; byte de s1
mov bl, [rsi] ; byte de s2
cmp al, bl
jne .diff ; diferentes
test al, al
jz .equal ; ambos NUL → iguales
inc rdi
inc rsi
jmp .loop
.diff:
movzx eax, al
movzx ebx, bl
sub eax, ebx ; negativo/positivo
ret
.equal:
xor eax, eax ; 0 = iguales
ret
; Uso:
; lea rdi, [rel str1]
; lea rsi, [rel str2]
; call my_strcmpstrcmp compara byte a byte hasta una diferencia o NUL. Retorna 0 (iguales), negativo (s1movzx a unsigned antes de sub. Loop con inc en ambos punteros.
CMPSB (comparar strings)
; CMPSB: compara [RSI] con [RDI], ambos++
; Usado para strcmp/memcmp
; Comparar N bytes:
lea rsi, [rel str1]
lea rdi, [rel str2]
mov rcx, 10 ; comparar 10 bytes
cld
repe cmpsb ; repite mientras iguales
; Resultado:
jz .iguales ; todos iguales (RCX=0)
; RSI-1 y RDI-1 = posición de la diferencia
; Flags: CF/SF indican el orden
; strcmp completo (hasta NUL):
lea rsi, [rel str1]
lea rdi, [rel str2]
.cmp_loop:
mov al, [rsi]
cmp al, [rdi]
jne .diferentes
test al, al
jz .iguales ; ambos NUL
inc rsi
inc rdi
jmp .cmp_loopCMPSB compara [RSI] con [RDI]. REPE = repite mientras iguales. Para strcmp: loop hasta una diferencia o NUL. Las flags tras la última comparación indican el orden (JB=str1
Imprimir número (itoa)
; Convertir entero → string decimal
; Input: RAX = número, RDI = buffer
itoa:
push rdi
mov rcx, 0 ; contador de dígitos
mov rbx, 10
.div_loop:
xor rdx, rdx
div rbx ; rax /= 10, resto en rdx
add dl, '0' ; ASCII del dígito
push rdx ; guardar (¡orden inverso!)
inc rcx
test rax, rax
jnz .div_loop
; Escribir dígitos (invertidos):
pop rax
mov [rdi], al
inc rdi
dec rcx
jnz .write_loop-1 ; (loop)
; NUL:
mov byte [rdi], 0
pop rdi
retitoa convierte un entero a string. Divide por 10 repetidamente. Resto + "0" = dígito ASCII. Los dígitos salen invertidos → usar la stack para reordenar. Terminar con NUL. Alternativa: printf vía libc.
Dicas e Técnicas
Optimizaciones básicas
; Poner a cero un registro (más rápido): xor eax, eax ; 1 byte, 1 ciclo ; vs: mov rax, 0 ; 7 bytes, más lento ; Multiplicar por una constante: lea rax, [rax + rax*4] ; *5 (1 ciclo) shl rax, 3 ; *8 (1 ciclo) ; vs: imul rax, 5 ; 3 ciclos ; Intercambiar registros: xchg rax, rbx ; 1 instrucción ; vs: push/pop o temp ; NOP (alineación): nop ; 1 byte ; NOP multi-byte para alineación: nop dword [rax] ; 4 bytes (no ejecuta) ; Branch hint (documentación): ; likely: jmp corto hacia adelante ; unlikely: jmp hacia atrás
xor eax,eax es la forma más rápida de poner a cero (1 ciclo, 2 bytes). LEA para multiplicar por constantes. XCHG intercambia sin temporal. NOP para alineación. Evita branches innecesarios.
Macros NASM
; Macro sin parámetros:
%macro newline 0
mov rax, 1
mov rdi, 1
lea rsi, [rel .nl]
mov rdx, 1
syscall
jmp %%fin
.nl: db 10
%%fin:
%endmacro
; Macro con parámetros:
%macro print_str 2
mov rax, 1
mov rdi, 1
lea rsi, [%1]
mov rdx, %2
syscall
%endmacro
; Uso:
newline
print_str msg, msg_len
; %% = label local a la macro (evita conflicto)%macro nombre N define una macro con N parámetros. %1, %2 = argumentos. %%label = label local (evita duplicados). Expandida en ensamblado (sin overhead). Ideal para código repetitivo (prints, asserts).
Buenas prácticas
; 1. Siempre CLD antes de string ops cld ; 2. Preservar callee-saved push rbx ; si usas rbx ; ... pop rbx ; 3. Stack alineada a 16 antes de CALL and rsp, -16 ; si es necesario ; 4. Verificar valores de retorno de syscalls syscall test rax, rax js .error ; 5. Usar LEA para aritmética lea rax, [rdi + rsi*4] ; 6. XOR para poner a cero (no MOV 0) xor eax, eax ; 7. Labels locales con un punto .loop: .fin: ; 8. Comentarios en cada bloque lógico
Reglas: CLD antes de string ops. Preservar callee-saved (RBX, R12-15). Stack alineada a 16. Verificar errores de syscall. LEA para aritmética. XOR para poner a cero. Labels locales con .. Comentar bloques.
Branchless programming
; abs(x) sin branch: mov rax, rdi ; x mov rcx, rdi sar rcx, 63 ; rcx = 0 o -1 (máscara) xor rax, rcx ; invierte si negativo sub rax, rcx ; +1 si negativo ; min(a, b) sin branch: mov rax, rdi ; a mov rcx, rsi ; b cmp rax, rcx cmovg rax, rcx ; si a>b, rax=b ; max(a, b): mov rax, rdi cmp rax, rsi cmovl rax, rsi ; si a<b, rax=b ; Ventaja: sin misprediction de la CPU ; El branch predictor falla con datos aleatorios
Branchless evita JMPs condicionales (penalización de misprediction ~15 ciclos). CMOV para min/max. Máscara con SAR 63 para abs. Ideal para datos impredecibles. Los branches son buenos cuando son predecibles (loops).
Debugging con GDB
; Compilar con 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 en la stack ; (gdb) x/s $rdi ; string en rdi ; (gdb) stepi ; 1 instrucción ; (gdb) nexti ; 1 instrucción (skip call) ; (gdb) display $rax ; mostrar siempre ; NASM: breakpoints con int3 int3 ; breakpoint por software ; (gdb) continue → para aquí ; Ver flags: ; (gdb) print $eflags
Compilar con -g -F dwarf para debug. GDB: stepi = 1 instrucción, info registers = registros. x/10xw $rsp examina la stack. int3 = un breakpoint en el código. display $rax lo muestra en cada paso.
x86 vs ARM vs RISC-V
; x86-64 (Intel/AMD): ; • CISC: instrucciones complejas, tamaño variable ; • 16 registros GPR (RAX-R15) ; • Flags (EFLAGS) afectadas por ops ; • Little-endian ; ARM64 (Apple M, Raspberry Pi): ; • RISC: instrucciones simples, tamaño fijo (4B) ; • 31 registros (X0-X30) ; • Sin flags (comparación explícita) ; • Bi-endian (generalmente little) ; RISC-V: ; • RISC open-source, modular ; • 32 registros (x0-x31, x0=zero) ; • Extensiones: M(mult), F(float), V(vector) ; Los conceptos son transferibles: ; stack, call/ret, loops, syscalls
x86 = CISC (instrucciones complejas, tamaño variable). ARM = RISC (simple, 4 bytes fijos). RISC-V = open-source modular. Los conceptos (stack, registros, loops) son universales. x86 domina servers/desktops. ARM domina mobile.
SSE (operaciones vectoriales)
; SSE: 128-bit (XMM0-XMM15) ; 4 floats o 2 doubles por registro ; Cargar (requiere alineación 16): movaps xmm0, [datos] ; 4 floats alineados movups xmm0, [datos] ; no alineado (lento) ; Operaciones: addps xmm0, xmm1 ; suma 4 floats mulps xmm0, xmm1 ; multiplica 4 floats subps xmm0, xmm1 ; resta 4 floats ; Scalar (1 float): addss xmm0, xmm1 ; solo el float bajo ; Comparar: cmpps xmm0, xmm1, 0 ; equal (4 comparaciones) ; Convertir: cvtsi2ss xmm0, eax ; int → float cvtss2si eax, xmm0 ; float → int
SSE procesa 4 floats simultáneamente (SIMD). XMM0-15 = 128 bits. movaps requiere alineación 16. Sufijos: ps = packed (4), ss = scalar (1). 4x más rápido que scalar para vectores.
Linking y ELF
; Ensamblar: nasm -f elf64 main.asm -o main.o ; Enlazar (sin libc): ld main.o -o main ; Enlazar (con libc): gcc main.o -o main -no-pie ; Múltiples ficheros: 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 ; Ver símbolos: nm main ; listar símbolos readelf -s main.o ; tabla de símbolos objdump -d main ; desensamblar ; Declarar externo: extern printf ; viene de otro fichero global main ; exportar al linker
nasm -f elf64 genera un objeto ELF. ld = link sin libc (solo syscalls). gcc = link con libc (printf, malloc). extern importa, global exporta. -no-pie evita PIE (más simple).
AVX (256-bit)
; AVX: 256-bit (YMM0-YMM15) ; 8 floats o 4 doubles por registro ; Operaciones (prefijo V): vaddps ymm0, ymm1, ymm2 ; suma 8 floats vmulps ymm0, ymm1, ymm2 ; mult 8 floats vsubps ymm0, ymm1, ymm2 ; sub 8 floats ; Cargar: vmovaps ymm0, [datos] ; alineado (32 bytes) vmovups ymm0, [datos] ; no alineado ; Broadcast (1 valor → todos): vbroadcastss ymm0, [val] ; 1 float → 8 ; IMPORTANTE: ¡limpiar los upper bits! vzeroupper ; antes de código SSE ; Evita la penalización de transición SSE↔AVX ; Alinear los datos: align 32 datos: dd 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0
AVX usa YMM (256 bits) = 8 floats. Prefijo V en las instrucciones. vzeroupper es OBLIGATORIO al mezclar con SSE (evita una penalización). Alinear datos a 32 bytes. 8x throughput vs scalar.
Performance y profiling
; RDTSC: timestamp counter (ciclos de CPU) rdtsc ; EDX:EAX = ciclos shl rdx, 32 or rax, rdx ; rax = 64-bit counter mov [start], rax ; ... código a medir ... rdtsc shl rdx, 32 or rax, rdx sub rax, [start] ; rax = ciclos gastados ; Consejos de performance: ; 1. Evitar branches (cmov, setcc) ; 2. Alinear loops a 16 bytes ; 3. Unroll de loops pequeños ; 4. Prefetch de datos (prefetcht0 [rax]) ; 5. Evitar dependencias (ILP) ; Herramientas: perf stat, perf record ; perf stat ./programa
RDTSC lee el contador de ciclos de la CPU. Medir antes/después para profiling. Consejos: evitar branches, alinear loops, unroll, prefetch. perf stat muestra IPC, cache misses. ILP: instrucciones independientes en paralelo.