Anweisungen
Nachdem du nun die Register und Speicherbereiche von sBPF verstanden hast, wollen wir die Anweisungen untersuchen, die sie manipulieren.
Anweisungen sind die grundlegenden Operationen, die dein Programm ausführt – Zahlen addieren, aus dem Speicher laden oder zu verschiedenen Stellen springen.
Was sind Anweisungen?
Anweisungen sind die grundlegenden Bausteine deines Programms. Denke an sie als Befehle, die dem Prozessor genau sagen, was zu tun ist:
add64 r1, r2: "Addiere die Werte in den Registernr1undr2, speichere das Ergebnis inr1"ldxdw r0, [r10 - 8]: "Lade 8 Bytes aus dem Stack-Speicher in Registerr0"jeq r1, 42, +3: "Wennr1gleich 42 ist, springe 3 Anweisungen vorwärts"
Jede Anweisung führt genau eine Operation aus und wird als exakt 8 Bytes Daten kodiert, damit die VM sie sofort dekodieren kann.
sBPF-Anweisungen arbeiten mit verschiedenen Datengrößen:
Byte = 8 Bits (1 Byte)
Halbwort = 16 Bits (2 Bytes)
Wort = 32 Bits (4 Bytes)
Doppelwort = 64 Bits (8 Bytes)
Die meisten sBPF-Operationen verwenden 64-Bit-Werte (Doppelwörter), da Register 64 Bit groß sind, aber du kannst bei Bedarf auch kleinere Größen laden und speichern, um die Effizienz zu steigern.
Anweisungskategorien und Format
Wenn du Rust-, C- oder Assembly-Code kompilierst, erzeugt die Toolchain einen Strom von Anweisungen mit fester Breite von 8 Bytes, die in den .textAbschnitt deiner ELF-Datei gepackt werden.
Jede Anweisung folgt einer einheitlichen Struktur, die die VM in einem einzigen Durchgang dekodieren kann:
1 byte 4 bits 4 bits 2 bytes 4 bytes
┌──────────┬────────┬────────┬──────────────┬──────────────────┐
│ opcode │ dst │ src │ offset │ imm │
└──────────┴────────┴────────┴──────────────┴──────────────────┘opcode: Definiert den Operationstyp. Die obersten 3 Bits wählen die Anweisungsklasse (Arithmetik, Speicher, Sprung, Aufruf, Beenden), während die unteren 5 Bits die genaue Variante angeben (Addition, Multiplikation, Laden, Sprung-wenn-gleich).dst: Die Nummer des Zielregisters (r0–r10), in dem Ergebnisse gespeichert werden – arithmetische Ergebnisse, geladene Werte oder Rückgabewerte von Hilfsfunktionen.src: Das Quellregister, das Eingaben liefert. Bei zweioperandiger Arithmetik (add r1, r2) liefert es den zweiten Wert. Bei Speicheroperationen kann es die Basisadresse liefern. Bei Immediate-Varianten (add r1, 10) werden diese 4 Bits in den Opcode eingebettet.offset: Eine kleine Ganzzahl, die das Verhalten der Anweisung modifiziert. Bei Lade-/Speicheroperationen wird sie zur Quelladresse addiert, um[src + offset]zu erreichen. Bei Sprüngen ist es ein relatives Sprungziel, gemessen in Anweisungen.imm: Das Feld für den Immediate-Wert. Arithmetische Operationen verwenden es für Konstanten (add r1, 42),CALLverwendet es für Syscall-Nummern (sol_log = 16), und Speicheroperationen können es als absolute Zeiger behandeln.
Befehlskategorien
Verschiedene Befehlstypen verwenden diese Felder auf spezifische Weise:
Datenbewegung: Werte zwischen Registern und Speicher bewegen:
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=unusedArithmetik: Mathematische Operationen durchführen:
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=100Kontrollfluss: Ausführungssequenz ändern:
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=42Opcode-Kodierung
Die Opcode-Kodierung erfasst mehrere Informationen über den Operationstyp hinaus:
Befehlsklasse: Arithmetik, Speicher, Sprung, Aufruf usw.
Operationsgröße: 32-Bit vs. 64-Bit Operationen
Quellentyp: Register vs. Direktwert
Spezifische Operation: Addition vs. Subtraktion, Laden vs. Speichern usw.
Dies erzeugt unterschiedliche Opcodes für Befehlsvarianten. Zum Beispiel verwendet add64 r1, r2 (Register-Quelle) einen anderen Opcode als add64 r1, 42 (Direktwert-Quelle). Ähnlich haben add64 und add32 unterschiedliche Opcodes für verschiedene Operationsgrößen.
Arithmetische Operationen unterscheiden weiter zwischen vorzeichenbehafteten und vorzeichenlosen Varianten. udiv64 behandelt Werte als vorzeichenlos (0 bis 18 Trillionen), während sdiv64 vorzeichenbehaftete Werte verarbeitet (-9 Trillionen bis +9 Trillionen).
Befehlsausführung
Der Opcode bestimmt, wie die VM die übrigen Felder interpretiert.
Wenn die VM auf add64 r1, r2 trifft, liest sie den Opcode und erkennt dies als 64-Bit arithmetische Operation mit zwei Registern:
Das Feld dst zeigt an, dass das Ergebnis in r1 gespeichert wird, das Feld src spezifiziert r2 als zweiten Operanden, und die Felder offset und immediate werden ignoriert.
Bei add64 r1, 42 ändert sich der Opcode, um eine Direktwert-Operation anzuzeigen. Jetzt zeigt dst immer noch auf r1, aber src wird bedeutungslos, und das Feld immediate liefert den zweiten Operanden (42).
Speicheroperationen kombinieren mehrere Felder sinnvoll:
Bei ldxdw r1, [r2+8] zeigt der Opcode einen 64-Bit Speicherladevorgang an, dst empfängt den geladenen Wert, src liefert die Basisadresse, und offset (8) wird hinzugefügt, um die endgültige Adresse r2 + 8 zu erzeugen.
Kontrollflussanweisungen folgen dem gleichen Muster:
Wenn du jeq r1, r2, +5 schreibst, kodiert der Opcode einen bedingten Sprung, der zwei Register vergleicht. Wenn r1 gleich r2 ist, addiert die VM den offset (5) zum Programmzähler und springt 5 Anweisungen vorwärts.
Funktionsaufrufe und Systemaufrufe
Der Aufrufmechanismus von sBPF hat sich über verschiedene Versionen hinweg für bessere Klarheit und Sicherheit weiterentwickelt. Bis sBPF v3 diente call imm zwei Zwecken: Der Unmittelbarwert bestimmte, ob du eine interne Funktion aufrufst oder einen Systemaufruf tätigst.
Die Laufzeitumgebung unterschied zwischen diesen basierend auf dem Wertebereich des Unmittelbarwerts, wobei Systemaufrufnummern typischerweise kleine positive Ganzzahlen wie 16 für sol_log waren.
Ab sBPF v3 wurden die Anweisungen für explizites Verhalten getrennt. call behandelt jetzt interne Funktionsaufrufe mit relativen Offsets, während syscall imm explizit Laufzeitfunktionen aufruft. Diese Trennung macht die Absichten des Bytecodes klar und ermöglicht eine bessere Überprüfung.
Indirekte Aufrufe durch callx haben sich ebenfalls weiterentwickelt. In früheren Versionen wurde das Zielregister im Unmittelbarfeld kodiert, aber ab v2 wird es im Quellregisterfeld kodiert, um Konsistenz mit dem allgemeinen Anweisungsformat zu gewährleisten.