Instruções
Agora que você entende os registradores e regiões de memória do sBPF, vamos examinar as instruções que os manipulam.
Instruções são as operações fundamentais que seu programa executa — somando números, carregando da memória ou pulando para diferentes localizações.
O que são Instruções?
Instruções são os blocos básicos de construção do seu programa. Pense nelas como comandos que dizem ao processador exatamente o que fazer:
add64 r1, r2: "Somar os valores nos registradoresr1er2, armazenar o resultado emr1"ldxdw r0, [r10 - 8]: "Carregar 8 bytes da memória da stack no registradorr0"jeq r1, 42, +3: "Ser1for igual a 42, pular 3 instruções para frente"
Cada instrução executa exatamente uma operação e é codificada em exatamente 8 bytes de dados para decodificação instantânea pela VM.
Instruções sBPF trabalham com diferentes tamanhos de dados:
byte = 8 bits (1 byte)
halfword = 16 bits (2 bytes)
word = 32 bits (4 bytes)
doubleword = 64 bits (8 bytes)
A maioria das operações sBPF usa valores de 64 bits (doublewords) já que os registradores são de 64 bits, mas você pode carregar e armazenar tamanhos menores quando necessário para eficiência.
Categorias de Instruções e Formato
Quando você compila código Rust, C ou assembly, a toolchain emite um fluxo de instruções de largura fixa de 8 bytes empacotadas na seção .text do seu ELF.
Cada instrução segue uma estrutura consistente que a VM pode decodificar em uma única passagem:
1 byte 4 bits 4 bits 2 bytes 4 bytes
┌──────────┬────────┬────────┬──────────────┬──────────────────┐
│ opcode │ dst │ src │ offset │ imm │
└──────────┴────────┴────────┴──────────────┴──────────────────┘opcode: Define o tipo de operação. Os 3 bits superiores selecionam a classe da instrução (aritmética, memória, jump, call, exit), enquanto os 5 bits inferiores especificam a variante exata (add, multiply, load, jump-if-equal).dst: O número do registrador de destino (r0–r10) onde os resultados são armazenados — resultados aritméticos, valores carregados ou retornos de funções auxiliares.src: O registrador de origem que fornece entrada. Para aritmética de dois operandos (add r1, r2), fornece o segundo valor. Para operações de memória, pode fornecer o endereço base. Para variantes imediatas (add r1, 10), estes 4 bits se incorporam ao opcode.offset: Um pequeno inteiro que modifica o comportamento da instrução. Para loads/stores, é adicionado ao endereço de origem para alcançar[src + offset]. Para jumps, é um destino de branch relativo medido em instruções.imm: O campo de valor imediato. Operações aritméticas o usam para constantes (add r1, 42),CALLo usa para números de syscall (sol_log = 16), e operações de memória podem tratá-lo como um ponteiro absoluto.
Categorias de Instruções
Diferentes tipos de instruções usam estes campos de formas específicas:
Movimentação de Dados: Move valores entre registradores e memória:
mov64 r1, 42 // Colocar valor imediato 42 em r1
// opcode=move_imm, dst=1, src=unused, imm=42
ldxdw r0, [r10 - 8] // Carregar 8 bytes da stack em r0
// opcode=load64, dst=0, src=10, offset=-8, imm=unused
stxdw [r1 + 16], r0 // Armazenar r0 na memória em [r1 + 16]
// opcode=store64, dst=1, src=0, offset=16, imm=unusedAritmética: Realiza operações matemáticas:
add64 r1, r2 // r1 = r1 + r2
// opcode=add_reg, dst=1, src=2, offset=unused, imm=unused
add64 r1, 100 // r1 = r1 + 100
// opcode=add_imm, dst=1, src=unused, offset=unused, imm=100Fluxo de Controle: Altera a sequência de execução:
ja +5 // Pular 5 instruções incondicionalmente
// opcode=jump, dst=unused, src=unused, offset=5, imm=unused
jeq r1, r2, +3 // Se r1 == r2, pular 3 instruções
// opcode=jump_eq_reg, dst=1, src=2, offset=3, imm=unused
jeq r1, 42, +3 // Se r1 == 42, pular 3 instruções
// opcode=jump_eq_imm, dst=1, src=unused, offset=3, imm=42Codificação do Opcode
A codificação do opcode captura múltiplas informações além do tipo de operação:
Classe da instrução: Aritmética, memória, jump, call, etc.
Tamanho da operação: Operações de 32 bits vs 64 bits
Tipo de origem: Registrador vs valor imediato
Operação específica: Add vs subtract, load vs store, etc.
Isso cria opcodes distintos para variantes de instruções. Por exemplo, add64 r1, r2 (origem registrador) usa um opcode diferente de add64 r1, 42 (origem imediata). Da mesma forma, add64 e add32 têm opcodes diferentes para diferentes tamanhos de operação.
Operações aritméticas distinguem ainda entre variantes com e sem sinal. udiv64 trata valores como sem sinal (0 a 18 quintilhões), enquanto sdiv64 lida com valores com sinal (-9 quintilhões a +9 quintilhões).
Execução de Instruções
O opcode determina como a VM interpreta os campos restantes.
Quando a VM encontra add64 r1, r2, ela lê o opcode e reconhece isso como uma operação aritmética de 64 bits usando dois registradores:
O campo dst indica que o resultado vai para r1, o campo src especifica r2 como o segundo operando, e os campos offset e immediate são ignorados.
Para add64 r1, 42, o opcode muda para indicar uma operação imediata. Agora dst ainda aponta para r1, mas src torna-se sem significado, e o campo immediate fornece o segundo operando (42).
Operações de memória combinam múltiplos campos de forma significativa:
Para ldxdw r1, [r2+8], o opcode indica um load de memória de 64 bits, dst recebe o valor carregado, src fornece o endereço base, e offset (8) é adicionado para criar o endereço final r2 + 8.
Instruções de fluxo de controle seguem o mesmo padrão:
Quando você escreve jeq r1, r2, +5, o opcode codifica um jump condicional comparando dois registradores. Se r1 for igual a r2, a VM adiciona o offset (5) ao contador de programa, pulando 5 instruções.
Chamadas de Função e Syscalls
O mecanismo de chamada do sBPF evoluiu ao longo das versões para maior clareza e segurança. Até o sBPF v3, call imm servia a propósitos duplos: o valor imediato determinava se você estava chamando uma função interna ou invocando uma syscall.
O runtime distinguia entre elas com base na faixa de valores imediatos, com números de syscall tipicamente sendo pequenos inteiros positivos como 16 para sol_log.
A partir do sBPF v3, as instruções foram separadas para comportamento explícito. call off agora lida com chamadas de função internas usando offsets relativos, enquanto syscall imm invoca explicitamente funções do runtime. Essa separação torna as intenções do bytecode claras e permite melhor verificação.
Chamadas indiretas através de callx também evoluíram. Versões anteriores codificavam o registrador de destino no campo imediato, mas a partir da v2, é codificado no campo do registrador de origem para consistência com o formato geral de instrução.