Trilha 07 · Assembly e baixo nível

Conjunto de instruções (ISA)

A ISA (Instruction Set Architecture) é o contrato entre software e hardware: define quais operações a CPU entende, como os operandos são especificados e quais flags cada instrução afeta.

① Intuição

O vocabulário da CPU

Uma linguagem de programação tem palavras-chave como if, for, return. A CPU tem sua própria "linguagem" — o conjunto de instruções (ISA). Em x86-64, existem centenas de instruções, mas você pode ler 90% do assembly do mundo com apenas ~20 delas: MOV, ADD, SUB, CMP, JMP, JE, CALL, RET, PUSH, POP.

Cada instrução é codificada em bytes. A CPU lê esses bytes da memória, decodifica (descobre o que fazer), executa e escreve o resultado. O conjunto de instruções define exatamente essa codificação — por isso software compilado para x86 não roda em ARM sem recompilação ou emulação.

CISC vs. RISC: x86 é CISC (Complex Instruction Set Computer) — instruções de comprimento variável, muitos modos de endereçamento, algumas instruções complexas como REP MOVS (copia blocos de memória). ARM/RISC-V são RISC — instruções de 32 bits fixos, operações simples, mais registradores. Internamente, CPUs x86 modernas traduzem instruções CISC em micro-operações RISC para execução no pipeline.
② Visualização interativa

Explorador do conjunto de instruções

Selecione uma categoria e depois uma instrução para ver sua sintaxe, descrição, exemplo e quais flags ela afeta.

MOVMOV dst, src

Copia valor de src para dst. O operando de origem não é alterado.

EXEMPLO
MOV EAX, EBX   ; EAX ← EBX
MOV EAX, 42    ; EAX ← 42
MOV [EBX], EAX ; memória[EBX] ← EAX
Flags alteradas:Nenhuma
③ Explicação técnica

Codificação binária das instruções

// Como uma instrução x86 é codificada em bytes
// (formato legado de comprimento variável: 1–15 bytes)

// MOV EAX, 42  →  B8 2A 00 00 00  (5 bytes)
// B8 = opcode de MOV com destino EAX
// 2A 00 00 00 = imediato 42 em little-endian

// ADD EAX, EBX  →  01 D8  (2 bytes)
// 01 = opcode ADD r/m32, r32
// D8 = ModRM: mod=11 (reg), reg=EBX (011), r/m=EAX (000)

// JMP rel32 (salto relativo)  →  E9 XX XX XX XX  (5 bytes)
// E9 = opcode JMP near
// XX = deslocamento de 32 bits relativo ao endereço após a instrução

// Por que comprimento variável?
// x86 nasceu em 1978 com 8 bits de opcode.
// Extensões (80386, MMX, SSE, AVX) adicionaram prefixos:
// REX (64-bit), VEX (AVX), EVEX (AVX-512).
// ARM usa instrução de 32 bits fixo — mais simples de decodificar!

Padrões idiomáticos — assembly que todo dev reconhece

// Padrões idiomáticos de assembly — aprenda a reconhecê-los

// Zerar registrador (menor e mais rápido que MOV EAX, 0)
xor eax, eax          // EAX = 0, ZF=1

// Verificar se EAX é zero (sem alterar EAX)
test eax, eax         // flags = EAX & EAX; não muda EAX
jz   is_zero

// Multiplicar por potência de 2 (mais rápido que IMUL)
shl eax, 3            // EAX *= 8

// Swap sem variável temporária (usando XOR)
xor eax, ebx
xor ebx, eax
xor eax, ebx          // EAX e EBX trocados

// Calcular índice em array de structs (tamanho 12 bytes)
imul eax, ecx, 12    // EAX = ECX * 12 (índice * tamanho)
add  eax, [base]      // EAX = endereço do elemento
Por que XOR EAX, EAX em vez de MOV EAX, 0? Historicamente, em x86 antigo, XOR era mais curto em bytes (2 bytes vs. 5 bytes) e mais rápido. Em CPUs modernas ambos são igualmente rápidos, mas o compilador ainda emite xor eax, eax porque (1) processadores modernos reconhecem o padrão e o tratam como idiom de zeragem, não criando dependência de dados em EAX, e (2) é 3 bytes menor no código gerado.
④ Projeto para programar

Lendo assembly gerado pelo compilador

Mini projeto: escreva uma função simples em C (if/else, loop for, chamada de função) e gere o assembly com gcc -O0 -S -o out.s prog.c. Identifique cada instrução usando a referência do explorador acima. Mapeie cada linha de C para as instruções correspondentes.

Projeto principal: implemente um mini "disassembler" em Python: dado um array de bytes de instruções simples (MOV reg, imm32: B8–BF seguido de 4 bytes), imprima o mnemônico e o valor. Tabela de opcodes para começar: 0xB8=MOV EAX, 0xB9=MOV ECX, 0xC3=RET, 0x90=NOP, 0xEB=JMP short.

Desafio extra: escreva assembly diretamente em arquivo .s e compile com gcc -nostdlib. Implemente uma função que calcule o comprimento de uma string (strlen) usando REP SCAS: mov edi, ptr / xor al, al / repne scasb / not ecx / lea eax, [ecx-1]. Chame-a de C e compare a velocidade com a versão de loop simples.

⑤ Exercícios rápidos

Teste sua intuição

O que a instrução MOV faz?
Qual é a diferença entre CMP e SUB?
Qual é a diferença entre CISC (x86) e RISC (ARM/RISC-V)?
⑥ Aplicações no mundo real

Onde você encontra isso

🔬

Godbolt Compiler Explorer

godbolt.org mostra em tempo real o assembly gerado pelo GCC, Clang, MSVC para código C/C++/Rust/Go. Essencial para entender o que o compilador faz com suas otimizações. Permite comparar x86-64, ARM64, RISC-V, WebAssembly lado a lado para o mesmo código.

🛡️

Antivírus e sandboxing

Antivírus modernos dissasemblam código suspeito para detectar padrões maliciosos sem executá-lo (análise estática). Sandboxes como Cuckoo executam o código em ambiente isolado e monitoram syscalls — que em x86-64 são feitas com a instrução syscall (Linux) ou int 0x2e / sysenter (Windows).

🎮

Cheat engines e mods de jogos

Ferramentas como Cheat Engine lêem a memória de um processo em execução e permitem alterar valores (vidas, dinheiro). Internamente, fazem injection de código assembly (code caves): encontram espaço livre no executável, escrevem bytes de instrução diretamente e redirecionam a execução com um JMP de 5 bytes.

← Anterior: Registradores Próxima: Assembly na prática →