
So schreibst du Solana-Programme in SBPF-Assembly
Inhaltsverzeichnis
- Architektur der virtuellen sBPF-Maschine
- Befehlssatzarchitektur von sBPF
- Speichermodell von sBPF
- Solana-Syscalls in sBPF
- Richte deine Umgebung ein
- Richte dein Projekt ein
- Beispiel für ein Memo in sBPF-Assembly
- Baue dein Programm und stelle es bereit
- Interagiere mit deinem Programm
- Eingabevalidierung
- Prüfung der Speichergrenzen
- Registerverwaltung
- Sichere Arithmetik
- Validierung von Syscall-Parametern
Im Solana-Ökosystem läuft derzeit parallel ein Wettlauf um die maximale Programmoptimierung.
Auf hoher Ebene revolutionieren Bibliotheken wie Pinocchio die Entwicklung mit Rust und verbessern die Recheneffizienz um Größenordnungen. Gleichzeitig geht eine Gruppe engagierter Entwickler auf der tiefstmöglichen Ebene noch einen Schritt weiter – vereint durch ihre gemeinsame Missachtung des Compilers. Statt Solana-Programme in kompilierten Sprachen wie Rust oder C zu schreiben, erstellen sie den Bytecode sorgfältig von Hand, um aus jedem einzelnen Befehl die maximale Leistung herauszuholen.
Solche Verbesserungen auf niedriger Ebene sind nur möglich, wenn wir die VM direkt in ihrer nativen Sprache anweisen: sBPF-Assembly, Solanas eigene Variante des Extended Berkeley Packet Filter (eBPF). Dieser Bytecode wird in jedem einzelnen On-Chain-Programm verwendet und ausgeführt.
Mit sBPF-Assembly erhalten Entwickler direkten Zugriff auf die unterste Schnittstelle der Solana Virtual Machine. Der Rust-Compiler und LLVM versuchen zwar, den Code zu optimieren. Doch weil die Sprachsyntax nicht aussagekräftig genug ist oder der nötige Kontext für bessere Kompilierungsentscheidungen fehlt, erzeugen sie oft suboptimalen Bytecode. Ein erfahrener Entwickler kann dank der vollständigen Kontrolle auf Befehlsebene, die Assembly bietet, bessere Ergebnisse erzielen.
Diese zusätzliche Kontrolle geht zwar zulasten der Ergonomie, spart aber erheblich Recheneinheiten und Binärgröße – und damit Miete. Besonders wichtig sind diese Einsparungen bei stark umkämpften, wettbewerbsintensiven und leistungskritischen Vorgängen.
Gleichzeitig lässt sich argumentieren, dass nicht alle Programme in Assembly geschrieben werden sollten.
Obwohl sich die Situation drastisch verbessert hat, waren die Tools in der Vergangenheit begrenzt. Noch wichtiger ist, dass Leistungsgewinne oft einen erheblichen Nachteil mit sich bringen: Du musst die Korrektheit manuell überprüfen und mit höheren Auditkosten rechnen. Das liegt an fehlenden automatisierten Tools und an der Syntax, die schwieriger zu lesen, zu schreiben und zu verstehen ist.
Umgekehrt lässt sich argumentieren, dass kompilierte Sprachen eine Blackbox sind und die Entscheidungen des Compilers verbergen. Die zusätzliche Transparenz und Kontrolle von Assembly kann daher Dinge sichtbar machen, die bei der Arbeit mit kompilierten Sprachen nur schwer zu erkennen sind. Tatsächlich entstanden die meisten jüngeren Leistungsdurchbrüche in unseren Rust SDKs aus der Erkenntnis, dass wir von Hand effizienteren Bytecode als der Compiler erstellen können.
In diesem Artikel lernst du:
- Was sBPF-Assembly ist und wie es dir direkte Kontrolle über die virtuelle Maschine gibt
- Wie sich der Berkeley Packet Filter zu eBPF entwickelt hat und warum Solana ihn übernommen hat
- Die Architektur der virtuellen Maschine, den Befehlssatz und das Speichermodell von sBPF
- Wie du deine Entwicklungsumgebung einrichtest und sBPF-Programme erstellst
- Schrittweise Assembly-Programmierung anhand eines praktischen Memo-Beispiels
- Wichtige Sicherheitsaspekte beim Schreiben von Low-Level-Code
Was ist Assembly?
Assembly ist eine für Menschen lesbare Variante von Maschinencode: die Programmiersprache auf der niedrigsten Ebene, die direkt dem Befehlssatz einer CPU oder VM entspricht.
Statt mit Variablen und Funktionen arbeitet Assembly mit Registern (schnellen, temporären Speicherplätzen in der CPU), Speicheradressen (physischen Speicherorten im RAM oder auf einem Datenträger) und grundlegenden Operationen wie Load (aus dem Speicher lesen), Store (im Speicher ablegen), Arithmetik und Sprüngen (Kontrollfluss).
Jeder Assembly-Befehl entspricht genau einem gleichwertigen Befehl im Maschinencode.
Durch diese Eins-zu-eins-Zuordnung kontrollieren Programmierer präzise, was der Prozessor ausführt. Dazu gehört, welche Register Daten enthalten, wie auf den Speicher zugegriffen wird und in welcher exakten Reihenfolge die Operationen ablaufen.
Anders als Hochsprachen, bei denen eine einzelne Funktion Dutzende Befehle erzeugen kann, bietet Assembly vollständige Transparenz und Kontrolle über das Verhalten der Maschine – ohne undurchsichtige Abstraktionen.
Was sind Berkeley Packet Filter (BPF) und eBPF?
Der Berkeley Packet Filter (BPF) entstand 1992 als virtuelle Maschine, um Netzwerkpakete in Unix-Kernels effizient zu filtern. Der ursprüngliche BPF nutzte einen einfachen Befehlssatz und eine registerbasierte Architektur, mit der sich isolierter Code sicher innerhalb des Kernels ausführen ließ.
Der Extended Berkeley Packet Filter (eBPF) modernisierte dieses Konzept und entwickelte sich vom Paketfilter zu einer universellen virtuellen Maschine. eBPF führte eine 64-Bit-Architektur, mehr Register und umfangreichere Befehlssätze ein. Dadurch können komplexe Programme für Netzwerke, Sicherheit und Systemüberwachung sicher im Kernel-Space ausgeführt werden.
Solana übernahm eBPF, weil es eine bewährte, sichere Ausführungsumgebung mit integrierter Sandbox bot. Die Sandbox verhindert, dass Programme auf Systemressourcen zugreifen, Nodes zum Absturz bringen oder andere Programme beeinträchtigen. Gleichzeitig stellt die deterministische Ausführung sicher, dass alle Validatoren identische Ergebnisse erzeugen.
Die registerbasierte Architektur und die ausgereifte Toolchain eigneten sich außerdem ideal für eine leistungsstarke On-Chain-Ausführung. Das vorhandene LLVM-Backend ermöglichte es Entwicklern zudem, Hochsprachen wie Rust zu kompilieren.
Architektur der virtuellen sBPF-Maschine
Wenn ein Solana-Programm ausgeführt wird, lädt die Runtime den sBPF-Bytecode in den Speicher und prüft ihn statisch auf Sicherheit. Dabei sucht sie nach Endlosschleifen, ungültigen Speicherzugriffen und einer falschen Verwendung von Befehlen. Anschließend führt sie ihn in der virtuellen Maschine aus.
Die VM stellt eine kontrollierte 64-Bit-Ausführungsumgebung bereit. Darin laufen Programme vollständig vom Hostsystem und anderen Programmen isoliert. Die Runtime vermittelt jeden Zugriff auf Ressourcen.
Befehlssatzarchitektur von sBPF
sBPF arbeitet mit elf 64-Bit-Registern (r0-r10). Dabei dient r10 als schreibgeschützter Frame-Pointer und r0 als Rückgaberegister.
Befehle folgen einem einheitlichen Format. Opcodes geben die Operationen an – etwa Arithmetik, Logik, Speicherzugriffe und Sprünge. Operanden bezeichnen Quell- und Zielregister, Offsets und/oder unmittelbare Werte.
Zu den wichtigsten Befehlskategorien gehören ALU-Operationen (Addition, Subtraktion und bitweise Operationen), Speicheroperationen (Load/Store) und der Kontrollfluss (bedingte und unbedingte Sprünge).
Speichermodell von sBPF
sBPF-Programme arbeiten in einer strukturierten Speicheraufteilung: einem 4 KB großen Stack für lokale Variablen und Funktionsaufrufe, einem Heap für dynamische Zuweisungen, schreibgeschützten Programmdaten mit Bytecode und Konstanten sowie Kontodatenbereichen, die Solana-Konten abbilden und auf die das Programm während der Ausführung zugreifen kann.
Alle Speicherzugriffe werden auf ihre Grenzen geprüft. Programme können nicht auf Speicher außerhalb der ihnen zugewiesenen Bereiche zugreifen.
Solana-Syscalls in sBPF
sBPF-Programme können nicht direkt auf Systemressourcen zugreifen oder I/O-Operationen ausführen. Stattdessen fordern sie Dienste über Syscalls an. Das sind spezielle Befehle, die die Kontrolle an die Solana-Runtime übergeben.
In sBPF-Assembly werden Syscalls mit dem call-Befehl und einem Aufrufsymbol ausgelöst, das der Compiler während der Assemblierung in ein Aufrufziel umwandelt. Derzeit werden Syscalls über textbasierte dynamische Relokationen aufgerufen. Dabei ordnet ein komplexes String-Lookup-Table-System Symbole während der JIT-Kompilierung einem 32-Bit-Mumur3-Hash zu. Es gibt jedoch einen aktiven Vorschlag, dies durch statische Syscalls zu ersetzen und die Aufrufkonventionen drastisch zu vereinfachen. Beim Aufruf eines Syscalls werden Argumente über die Register 1 bis 5 übergeben. Register 5 dient dabei manchmal als Stack-Spill. Rückgabewerte werden in r0 geschrieben.
Zu den gängigen Syscalls gehören Speicheroperationen (sol_memcpy, sol_memcmp), kryptografische Funktionen (Hashing und Signaturprüfung), Logging und programmübergreifende Aufrufe.
Während reguläre sBPF-Befehle jeweils 1 CU kosten, werden die Recheneinheiten für Syscalls ganz anders berechnet. Wenn ein Syscall zum Protokoll hinzugefügt wird, wird seine Leistung gemessen. Daraus ergibt sich ein grundlegender Aufrufpreis – bei einem CPI-Aufruf sind es beispielsweise 1000. In einigen Fällen fallen außerdem variable Kosten an, die von der verarbeiteten Datenmenge abhängen.
Tutorial zu SBPF-Assembly
Richte deine Umgebung ein
Für das Schreiben von sBPF-Assembly war traditionell die vollständige Solana-Toolchain erforderlich: ein aufgeblähter, komplexer und plattformabhängiger Prozess.
Deshalb entwickelte Dean Little das sBPF SDK. Es bietet eine vollständige End-to-End-Lösung, um sBPF-Programme einzurichten, zu bauen, zu kompilieren, zu testen und bereitzustellen.
Du kannst das SDK mit Cargo auf jedem Betriebssystem installieren:
cargo install --git https://github.com/blueshift-gg/sbpf.gitBevor du in den Code einsteigst, solltest du außerdem die VS Code-Erweiterung für sBPF-Assembly installieren. Sie bietet Syntaxhervorhebung, automatische Vervollständigung und Fehlererkennung.
Richte dein Projekt ein
Erstelle ein neues Projektgerüst mit:
sbpf init <name_of_the_project>Dadurch entsteht ein Projekt mit Rust-Tests von Mollusk.
Wenn du stattdessen ein Gerüst mit TypeScript-Tests erstellen möchtest, kannst du damit ein neues Gerüst mit TypeScript-Tests initialisieren:
sbpf init <name_of_the_project> --ts-tests Beispiel für ein Memo in sBPF-Assembly
Ein einfacheres Programm als ein Memo ist kaum vorstellbar. Genau deshalb eignet es sich perfekt als Einführung in sBPF-Assembly.
Das Programm erledigt genau eine Aufgabe: Es nimmt beliebige Anweisungsdaten entgegen, die du sendest, und protokolliert sie auf der Blockchain. Keine Konten, keine komplexe Logik – nur direkte Kontrolle über die Solana Virtual Machine auf Befehlsebene.
Sehen wir uns zunächst das vollständige Programm an:
.equ NUM_ACCOUNTS, 0x00
.equ DATA_LEN, 0x08
.equ DATA, 0x10
.globl entrypoint
entrypoint:
ldxdw r0, [r1+NUM_ACCOUNTS]
ldxdw r2, [r1+DATA_LEN]
add64 r1, DATA
call sol_log_
exitDefiniere deine Konstanten
Das Programm definiert zunächst drei Konstanten, die die Struktur des serialisierten Eingabebereichs der Solana-Runtime abbilden:
.equ NUM_ACCOUNTS, 0x00 // Account count offset
.equ DATA_LEN, 0x08 // Data length offset
.equ DATA, 0x10 // Data start offsetDiese Offsets entsprechen den Speicherpositionen, an denen die Solana-Runtime Anweisungsdaten ablegt.
Wenn die VM unser Programm aufruft, übergibt sie uns in Register r1 einen strukturierten Puffer. Mit diesen Konstanten können wir uns in dieser Struktur bewegen.
Tools wie sbpf.xyz können diese Offsets anhand des Layouts deiner Konto- und Anweisungsdaten automatisch berechnen.
Erstelle den Einstiegspunkt
Anschließend erstellen wir den Einstiegspunkt und die Validierung für unser Programm.
.globl entrypoint
entrypoint:
ldxdw r0, [r1+NUM_ACCOUNTS]Die Einstiegspunkt-Direktive .globl weist den Linker an, das Einstiegspunktsymbol global sichtbar zu machen. Die Solana-Runtime sucht nach diesem Symbol, um zu erkennen, wo sie mit der Ausführung deines Programms beginnen soll. Interessant: Wir nennen es üblicherweise entrypoint, aber es kann tatsächlich beliebig heißen!
Der erste Befehl nutzt eine clevere, Assembly-spezifische Validierungstechnik: r0 ist unser Rückgaberegister, und jeder Rückgabewert ungleich 0 dient als Fehlercode. Wenn wir die Anzahl der Konten direkt in r0 laden, wird das Programm daher mit einem Fehlercode ungleich null beendet, sobald mehr als 0 Konten übergeben werden.
Dadurch müssen wir keine eingegebenen Konten manuell überspringen, um den Offset unserer Anweisungsdaten in der VM zu validieren.
Rufe den sol_log-Syscall auf
Jetzt können wir schließlich den Syscall sol_log_ ausführen:
ldxdw r2, [r1+DATA_LEN] ; Load memo length
add64 r1, DATA ; Point r1 to memo data
call sol_log_ ; Log the memo
exit ; Exit with r0 valueDer Syscall sol_log_ erwartet die Länge der Nachricht in r2 und in r1 einen Zeiger auf die zu protokollierende Nachricht. Im serialisierten Eingabebereich stellt die Runtime den Anweisungsdaten praktischerweise automatisch einen 64-Bit-Längenzähler voran. Daher können wir den Syscall sol_log_ einfach folgendermaßen aufrufen:
- Lade den Wert am Offset DATA_LEN in r2
- Lass r1 auf den Offset am Anfang unserer Anweisungsdaten zeigen
Sobald unsere beiden Register auf die richtigen Werte zeigen, rufen wir einfach den Syscall auf und beenden das Programm mit dem Wert in r0.
Baue dein Programm und stelle es bereit
Du kannst dein Programm mit dem integrierten Assembler von SBPF bauen, den Claire Fan geschrieben hat. Diese 5 MB große Rust-Binärdatei ersetzt die mehr als 2 GB große LLVM-Toolchain, die du mit den Solana-Plattformtools verwenden müsstest.
Führe zum Bauen einfach Folgendes aus:
sbpf buildNachdem du dein Programm gebaut hast, kannst du es wie folgt bereitstellen:
sbpf deployInteragiere mit deinem Programm
Um dein Programm mit den vorbereiteten Tests zu testen, kannst du sbpf test ausführen. Alternativ kannst du mit sbpf e2e die gesamte Pipeline starten und dein Programm mit einem einzigen Befehl bauen, bereitstellen und testen.
Sicherheitsaspekte bei sBPF-Assembly
Wenn du Assembly-Code schreibst, trägst du die volle Verantwortung für die Sicherheit: Es gibt keinen Compiler, der deine Fehler erkennt. Jeder Befehl wirkt sich direkt auf die Sicherheit des Programms aus. Deshalb sind diese grundlegenden Sicherheitsprinzipien unverzichtbar.
Eingabevalidierung
Assembly-Programme müssen alle Eingaben manuell validieren. Prüfe vor der Verwendung immer die Anzahl der Konten, die Datenlängen und die Puffergrößen.
In unserem Memo-Programm hat beispielsweise das Laden der Kontoanzahl in r0 eine automatische Validierung erzeugt: Wenn fälschlicherweise Konten übergeben würden, würde das Programm fehlschlagen. Prüfe bei der Datenverarbeitung die Längen anhand der erwarteten Bereiche, bevor du auf den Speicher zugreifst.
Prüfung der Speichergrenzen
sBPF bietet keine automatische Prüfung der Speichergrenzen. Bevor du auf Arrays oder Puffer zugreifst, musst du manuell prüfen, ob deine Lese- und Schreibvorgänge innerhalb der zugewiesenen Grenzen bleiben. Eine einfache Prüfung vor dem Speicherzugriff kann Abstürze und beschädigte Daten verhindern:
jgt r2, MAX_LENGTH, error # Check if length exceeds limit
ldxb r3, [r1+r2] # Safe to load if check passesRegisterverwaltung
Register enthalten kritische Programmzustände. Wenn du Funktionen oder Syscalls aufrufst, sichere wichtige Werte auf dem Stack oder in anderen Registern.
Der Frame-Pointer (r10) und die Rückgabewerte in r0 erfordern besondere Aufmerksamkeit: Werden sie beschädigt, kann dein Programm abstürzen oder Sicherheitslücken aufweisen.
Sichere Arithmetik
Die manuelle Erkennung von Überläufen ist bei arithmetischen Operationen entscheidend. Prüfe vor dem Addieren großer Werte, ob das Ergebnis die 64-Bit-Grenzen überschreiten könnte.
Bei Divisionen musst du explizit auf null prüfen, um Runtime-Fehler zu vermeiden.
Validierung von Syscall-Parametern
Syscalls erwarten gültige Parameter und schlagen bei ungültigen Eingaben fehl. Stelle vor ihrem Aufruf sicher, dass die Register korrekte Zeiger, Längen und Werte enthalten. Ungültige Parameter verursachen nicht nur Fehler, sondern verbrauchen auch unnötig Recheneinheiten.
Fazit
sBPF-Assembly ist nicht für alle geeignet – und genau darum geht es.
Die meisten Entwickler sollten bei Rust bleiben und dem Compiler die Optimierung überlassen. Wenn du jedoch jede einzelne Recheneinheit optimierst oder leistungskritische Infrastruktur baust, bietet Assembly etwas, das keine Hochsprache leisten kann: vollständige Kontrolle.
Wir haben hier die Grundlagen behandelt: von der Rolle von sBPF in der Architektur von Solana bis zum Bau deines ersten Memo-Programms.
Das Memo-Beispiel mag trivial wirken, zeigt aber die zentralen Prinzipien, die du in komplexeren Programmen verwenden wirst:
- Direkte Bearbeitung von Registern
- Manuelle Speicherverwaltung
- Explizite Verarbeitung von Syscalls
Diese Vorteile haben ihren Preis: Du tauschst Sicherheitsnetze gegen Geschwindigkeit und Abstraktionen gegen Kontrolle. Entscheide dich für Assembly, wenn du deinen Rust-Code bereits optimiert hast und noch mehr Leistung brauchst, wenn du Infrastruktur entwickelst, bei der jede Mikrosekunde zählt, oder wenn du etwas tun musst, das der Compiler schlicht nicht gut optimieren kann.
In allen anderen Fällen ersparst du dir den Aufwand und bleibst bei Rust.
Die Tools werden besser, die Community wächst und die Leistungsvorteile sprechen für sich. Denk aber daran: „Große Macht bringt große Verantwortung mit sich, nichts kaputtzumachen.“
Wenn du mehr über die Verwendung von sBPF-Assembly lesen möchtest, sieh dir den Einführungskurs zu Assembly auf Blueshift an und teste dein Können mit einigen der dort verfügbaren Aufgaben!
Ähnliche Artikel
Helius abonnieren
Bleib bei der Solana-Entwicklung auf dem Laufenden und erhalte Updates, wenn wir neue Beiträge veröffentlichen


