Códigos de Operação
Códigos de operação — opcodes — são os comandos que o processador entende. Cada instrução assembly é traduzida para um ou mais bytes que representam um opcode e seus operandos. Entender como instruções são codificadas é essencial para ler assembly com fluência.
O que é um Opcode?
Um opcode (operation code) é um número binário que diz à CPU qual operação executar. Por exemplo, no x86-64:
90 ; opcode: NOP (no operation)
48 89 D8 ; opcode: MOV RAX, RBX
B8 2A 00 00 00 ; opcode: MOV EAX, 42
O primeiro byte (ou primeiros bytes) identifica a operação. Bytes adicionais especificam os operandos (registradores, imediatos, endereços de memória).
Tipos de Operandos
Cada instrução pode operar sobre diferentes tipos de operandos:
| Tipo | Exemplo | Descrição |
|---|---|---|
| Registrador | mov rax, rbx |
Opera diretamente em registradores |
| Imediato | mov rax, 42 |
Valor constante embutido na instrução |
| Memória | mov rax, [rbx] |
Acessa a memória no endereço especificado |
| Implícito | syscall |
Operandos determinados pela instrução |
Modos de Endereçamento
Instruções que acessam memória podem usar diferentes modos de endereçamento:
mov rax, [0x600000] ; Direto: endereço absoluto
mov rax, [rbx] ; Indireto: ponteiro em RBX
mov rax, [rbx + 8] ; Base + deslocamento constante
mov rax, [rbx + rcx] ; Base + índice
mov rax, [rbx + rcx*4] ; Base + índice × escala
mov rax, [rbx + rcx*4 + 8] ; Base + índice × escala + deslocamento
RISC vs CISC
A diferença fundamental entre arquiteturas RISC e CISC está nos opcodes:
CISC (Complex Instruction Set Computer) — x86:
- Muitos opcodes (centenas)
- Instruções de tamanho variável (1 a 15 bytes no x86)
- Instruções complexas que fazem múltiplas operações
- Exemplo:
rep movsbcopia uma string inteira em uma instrução - Operandos de memória podem ser usados diretamente em operações aritméticas
add rax, [rbx] ; x86: soma memória → registrador (1 instrução)
RISC (Reduced Instruction Set Computer) — ARM64, RISC-V:
- Conjunto de instruções mais regular e simples
- Instruções de tamanho fixo (4 bytes em ARM64 e RISC-V padrão)
- Cada instrução faz uma operação simples
- Apenas loads e stores acessam memória (load/store architecture)
ldr x0, [x1] ; ARM64: carrega da memória
add x0, x0, x2 ; ARM64: soma registradores (2 instruções)
Categorias de Instruções
Independentemente da arquitetura, as instruções se agrupam em categorias:
1. Movimento de Dados
Copiam valores entre registradores e memória:
| x86-64 | ARM64 | RISC-V | Significado |
|---|---|---|---|
mov rax, rbx |
mov x0, x1 |
mv a0, a1 |
Copiar registrador |
mov rax, 42 |
mov x0, #42 |
li a0, 42 |
Carregar imediato |
mov rax, [rbx] |
ldr x0, [x1] |
ld a0, 0(a1) |
Carregar da memória |
mov [rbx], rax |
str x0, [x1] |
sd a0, 0(a1) |
Armazenar na memória |
push rax |
stp x0, x1, [sp, #-16]! |
addi sp, sp, -16; sd a0, 8(sp) |
Salvar na stack |
2. Aritmética
Operações matemáticas:
| x86-64 | ARM64 | RISC-V | Significado |
|---|---|---|---|
add rax, rbx |
add x0, x0, x1 |
add a0, a0, a1 |
Soma |
sub rax, rbx |
sub x0, x0, x1 |
sub a0, a0, a1 |
Subtração |
inc rax |
add x0, x0, #1 |
addi a0, a0, 1 |
Incremento |
dec rax |
sub x0, x0, #1 |
addi a0, a0, -1 |
Decremento |
imul rax, rbx |
mul x0, x0, x1 |
mul a0, a0, a1 |
Multiplicação |
3. Lógica e Bits
Operações bit a bit:
| x86-64 | ARM64 | RISC-V | Significado |
|---|---|---|---|
and rax, rbx |
and x0, x0, x1 |
and a0, a0, a1 |
AND bit a bit |
or rax, rbx |
orr x0, x0, x1 |
or a0, a0, a1 |
OR bit a bit |
xor rax, rbx |
eor x0, x0, x1 |
xor a0, a0, a1 |
XOR bit a bit |
shl rax, 1 |
lsl x0, x0, #1 |
slli a0, a0, 1 |
Shift left |
shr rax, 1 |
lsr x0, x0, #1 |
srli a0, a0, 1 |
Shift right |
4. Controle de Fluxo
Alteram o fluxo de execução:
x86-64 (usando flags implícitas de cmp):
cmp rax, rbx
je .igual ; salta se RAX == RBX
jne .diferente ; salta se RAX != RBX
jg .maior ; salta se RAX > RBX (com sinal)
jl .menor ; salta se RAX < RBX (com sinal)
jmp .sempre ; salta incondicionalmente
ARM64 (cmp atualiza flags explicitamente):
cmp x0, x1
b.eq .igual ; branch if equal
b.ne .diferente ; branch if not equal
b.gt .maior ; branch if greater than
b.lt .menor ; branch if less than
b .sempre ; branch incondicional
RISC-V (branches comparam registradores diretamente, sem flags):
beq a0, a1, .igual ; branch if equal
bne a0, a1, .diferente ; branch if not equal
blt a0, a1, .menor ; branch if less than
bge a0, a1, .maior_igual ; branch if greater or equal
j .sempre ; jump incondicional
5. Chamadas de Função
Transferem controle para sub-rotinas e retornam:
| x86-64 | ARM64 | RISC-V | Ação |
|---|---|---|---|
call func |
bl func |
call func |
Salva retorno em stack/LR, salta |
ret |
ret |
ret |
Retorna ao endereço salvo |
Codificação Real de um Opcode
Vamos ver como a instrução mov rax, rbx (x86-64) é codificada:
mov rax, rbx → 48 89 D8
│ │ └─ ModR/M: mod=11, reg=RBX (origem), r/m=RAX (destino)
│ └─── Opcode principal: MOV r/m64, r64
└────── REX prefix: 64-bit operand size
E como addi a0, a0, 5 (RISC-V) é codificada:
addi a0, a0, 5 → palavra de instrução: 0x00550513
bytes no arquivo (little-endian): 13 05 55 00
│││││││└─────── opcode: 0010011 (OP-IMM)
│││││└───────── rd = a0 = 01010
│││└─────────── funct3 = 000 (ADDI)
│└───────────── rs1 = a0 = 01010
└────────────── immediate = 5 = 000000000101
Na prática, você raramente precisa decodificar opcodes manualmente — o assembler faz isso. Mas entender a estrutura ajuda a ler disassembly e entender o “peso” das instruções.
Prefixos e Extensões (x86)
O x86 é famoso por seus prefixos — bytes adicionais antes do opcode que modificam o comportamento:
- REX (0x48): acesso a registradores de 64 bits e R8–R15
- 0x66: operand size override (alterna entre 16 e 32 bits)
- 0xF2 / 0xF3: REP/REPE/REPNE (repetição de string)
- 0x0F: escape para opcodes de 2 bytes (SSE, etc.)
Essa complexidade é uma das razões pelas quais o decoder x86 é notoriamente complicado de implementar.
Conclusão
- Opcodes são números binários que comandam a CPU
- RISC tem poucos opcodes simples e fixos; CISC tem muitos opcodes complexos e variáveis
- Instruções se agrupam em movimento de dados, aritmética, lógica, controle de fluxo e chamadas
- Modos de endereçamento definem como operandos de memória são calculados
- O assembler cuida da codificação para você — mas entender o mecanismo é valioso