Instructions
Maintenant que vous comprenez les registres et les régions mémoire de sBPF, examinons les instructions qui les manipulent.
Les instructions sont les opérations fondamentales que votre programme effectue—additionner des nombres, charger depuis la mémoire, ou sauter à différents emplacements.
Que sont les instructions ?
Les instructions sont les éléments de base de votre programme. Considérez-les comme des commandes qui indiquent précisément au processeur ce qu'il doit faire :
add64 r1, r2: "Additionne les valeurs dans les registresr1etr2, stocke le résultat dansr1"ldxdw r0, [r10 - 8]: "Charge 8 octets depuis la mémoire de pile dans le registrer0"jeq r1, 42, +3: "Sir1est égal à 42, saute 3 instructions en avant"
Chaque instruction effectue exactement une opération et est encodée en précisément 8 octets de données pour un décodage instantané par la VM.
Les instructions sBPF fonctionnent avec différentes tailles de données :
octet = 8 bits (1 octet)
demi-mot = 16 bits (2 octets)
mot = 32 bits (4 octets)
double mot = 64 bits (8 octets)
La plupart des opérations sBPF utilisent des valeurs 64 bits (double mots) puisque les registres font 64 bits, mais vous pouvez charger et stocker des tailles plus petites quand nécessaire pour plus d'efficacité.
Catégories d'instructions et format
Lorsque vous compilez du code Rust, C ou assembleur, la chaîne d'outils émet un flux d'instructions de largeur fixe de 8 octets, empaquetées dans la section .text de votre ELF.
Chaque instruction suit une structure cohérente que la VM peut décoder en un seul passage :
1 byte 4 bits 4 bits 2 bytes 4 bytes
┌──────────┬────────┬────────┬──────────────┬──────────────────┐
│ opcode │ dst │ src │ offset │ imm │
└──────────┴────────┴────────┴──────────────┴──────────────────┘opcode: Définit le type d'opération. Les 3 bits supérieurs sélectionnent la classe d'instruction (arithmétique, mémoire, saut, appel, sortie), tandis que les 5 bits inférieurs spécifient la variante exacte (addition, multiplication, chargement, saut-si-égal).dst: Le numéro du registre de destination (r0–r10) où les résultats sont stockés—résultats arithmétiques, valeurs chargées ou retours de fonctions auxiliaires.src: Le registre source fournissant l'entrée. Pour l'arithmétique à deux opérandes (add r1, r2), il fournit la seconde valeur. Pour les opérations mémoire, il peut fournir l'adresse de base. Pour les variantes immédiates (add r1, 10), ces 4 bits sont intégrés dans l'opcode.offset: Un petit entier qui modifie le comportement de l'instruction. Pour les chargements/stockages, il est ajouté à l'adresse source pour atteindre[src + offset]. Pour les sauts, c'est une cible de branchement relative mesurée en instructions.imm: Le champ de valeur immédiate. Les opérations arithmétiques l'utilisent pour les constantes (add r1, 42),CALLl'utilise pour les numéros d'appels système (sol_log = 16), et les opérations mémoire peuvent le traiter comme un pointeur absolu.
Catégories d'instructions
Les différents types d'instructions utilisent ces champs de manières spécifiques :
Déplacement de données : Déplacer des valeurs entre les registres et la mémoire :
mov64 r1, 42 // Put immediate value 42 into r1
// opcode=move_imm, dst=1, src=unused, imm=42
ldxdw r0, [r10 - 8] // Load 8 bytes from stack into r0
// opcode=load64, dst=0, src=10, offset=-8, imm=unused
stxdw [r1 + 16], r0 // Store r0 to memory at [r1 + 16]
// opcode=store64, dst=1, src=0, offset=16, imm=unusedArithmétique : Effectuer des opérations mathématiques :
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=100Flux de contrôle : Modifier la séquence d'exécution :
ja +5 // Jump forward 5 instructions unconditionally
// opcode=jump, dst=unused, src=unused, offset=5, imm=unused
jeq r1, r2, +3 // If r1 == r2, jump forward 3 instructions
// opcode=jump_eq_reg, dst=1, src=2, offset=3, imm=unused
jeq r1, 42, +3 // If r1 == 42, jump forward 3 instructions
// opcode=jump_eq_imm, dst=1, src=unused, offset=3, imm=42Encodage des opcodes
L'encodage de l'opcode capture plusieurs informations au-delà du simple type d'opération :
Classe d'instruction : arithmétique, mémoire, saut, appel, etc.
Taille de l'opération : opérations 32 bits vs 64 bits
Type de source : registre vs valeur immédiate
Opération spécifique : addition vs soustraction, chargement vs stockage, etc.
Cela crée des opcodes distincts pour les variantes d'instructions. Par exemple, add64 r1, r2 (source registre) utilise un opcode différent de add64 r1, 42 (source immédiate). De même, add64 et add32 ont des opcodes différents pour différentes tailles d'opération.
Les opérations arithmétiques distinguent également les variantes signées et non signées. udiv64 traite les valeurs comme non signées (de 0 à 18 quintillions), tandis que sdiv64 gère les valeurs signées (de -9 quintillions à +9 quintillions).
Exécution des instructions
L'opcode détermine comment la VM interprète les champs restants.
Lorsque la VM rencontre add64 r1, r2, elle lit l'opcode et reconnaît qu'il s'agit d'une opération arithmétique 64 bits utilisant deux registres :
Le champ dst indique que le résultat va dans r1, le champ src spécifie r2 comme second opérande, et les champs offset et immediate sont ignorés.
Pour add64 r1, 42, l'opcode change pour indiquer une opération immédiate. Maintenant dst pointe toujours vers r1, mais src devient sans importance, et le champ immediate fournit le second opérande (42).
Les opérations de mémoire combinent plusieurs champs de manière significative :
Pour ldxdw r1, [r2+8], l'opcode indique un chargement mémoire 64 bits, dst reçoit la valeur chargée, src fournit l'adresse de base, et offset (8) est ajouté pour créer l'adresse finale r2 + 8.
Les instructions de flux de contrôle suivent le même modèle :
Lorsque vous écrivez jeq r1, r2, +5, l'opcode encode un saut conditionnel comparant deux registres. Si r1 est égal à r2, la VM ajoute l'offset (5) au compteur de programme, sautant ainsi 5 instructions vers l'avant.
Appels de fonctions et appels système
Le mécanisme d'appel de sBPF a évolué à travers différentes versions pour une meilleure clarté et sécurité. Jusqu'à sBPF v3, call imm servait à deux fins : la valeur immédiate déterminait si vous appeliez une fonction interne ou si vous invoquiez un appel système.
Le runtime faisait la distinction entre ces deux cas en fonction de la plage de valeurs immédiates, les numéros d'appels système étant généralement de petits entiers positifs comme 16 pour sol_log.
À partir de sBPF v3, les instructions ont été séparées pour un comportement explicite. call gère maintenant les appels de fonctions internes en utilisant des décalages relatifs, tandis que syscall imm invoque explicitement les fonctions d'exécution. Cette séparation clarifie les intentions du bytecode et permet une meilleure vérification.
Les appels indirects via callx ont également évolué. Les versions antérieures encodaient le registre cible dans le champ immédiat, mais à partir de la v2, il est encodé dans le champ du registre source pour plus de cohérence avec le format général d'instruction.