QEMU.js: jetzt ernst und mit WASM

Es war einmal, ich habe mich aus Spaß entschieden Beweisen Sie die Reversibilität des Prozesses und erfahren Sie, wie Sie JavaScript (genauer gesagt Asm.js) aus Maschinencode generieren. QEMU wurde für das Experiment ausgewählt und einige Zeit später wurde ein Artikel über Habr geschrieben. In den Kommentaren wurde mir geraten, das Projekt in WebAssembly neu zu erstellen und mich sogar selbst aufzuhören fast fertig Irgendwie wollte ich das Projekt nicht... Die Arbeit ging voran, aber sehr langsam, und jetzt, vor kurzem, erschien dieser Artikel Kommentar zum Thema „Wie endete alles?“ Als Antwort auf meine ausführliche Antwort hörte ich: „Das klingt nach einem Artikel.“ Wenn Sie können, wird es einen Artikel geben. Vielleicht findet es jemand nützlich. Daraus erfährt der Leser einige Fakten über das Design von QEMU-Codegenerierungs-Backends und darüber, wie man einen Just-in-Time-Compiler für eine Webanwendung schreibt.

Aufgaben

Da ich bereits gelernt hatte, wie man QEMU „irgendwie“ auf JavaScript portiert, wurde dieses Mal beschlossen, es mit Bedacht anzugehen und alte Fehler nicht zu wiederholen.

Fehler Nummer eins: Verzweigung vom Point-Release

Mein erster Fehler bestand darin, meine Version von der Upstream-Version 2.4.1 abzuzweigen. Dann schien es mir eine gute Idee zu sein: Wenn es ein Point-Release gibt, dann ist es wahrscheinlich stabiler als das einfache 2.4, und noch stabiler der Zweig master. Und da ich vorhatte, eine ganze Menge meiner eigenen Käfer hinzuzufügen, brauchte ich überhaupt keine anderen. Wahrscheinlich ist es so gekommen. Aber hier ist die Sache: QEMU bleibt nicht stehen und hat irgendwann sogar eine Optimierung des generierten Codes um 10 Prozent angekündigt. „Ja, jetzt friere ich ein“, dachte ich und brach zusammen. Hier müssen wir einen Exkurs machen: Aufgrund der Single-Thread-Natur von QEMU.js und der Tatsache, dass das ursprüngliche QEMU nicht das Fehlen von Multi-Threading impliziert (d. h. die Fähigkeit, mehrere unabhängige Codepfade gleichzeitig zu betreiben, und Nicht nur „Alle Kernel nutzen“) ist dafür entscheidend, die Hauptfunktionen von Threads musste ich „ausschalten“, um sie von außen aufrufen zu können. Dies führte während der Fusion zu einigen natürlichen Problemen. Allerdings ist die Tatsache, dass einige der Änderungen aus der Branche master, mit denen ich versucht habe, meinen Code zusammenzuführen, wurden im Point-Release (und damit in meinem Zweig) ebenfalls von Rosinen ausgewählt und hätten wahrscheinlich auch keinen zusätzlichen Komfort geboten.

Im Allgemeinen habe ich entschieden, dass es immer noch sinnvoll ist, den Prototyp wegzuwerfen, ihn in Einzelteile zu zerlegen und eine neue Version von Grund auf zu bauen, die auf etwas Frischerem und Aktuellem basiert master.

Fehler Nummer zwei: TLP-Methodik

Im Wesentlichen handelt es sich hierbei nicht um einen Fehler, sondern lediglich um eine Besonderheit bei der Erstellung eines Projekts unter Bedingungen völliger Missverständnisse sowohl hinsichtlich der Frage „Wohin und wie sollen wir uns bewegen?“ als auch allgemein: „Werden wir dort ankommen?“ Unter diesen Bedingungen ungeschickte Programmierung war eine berechtigte Option, aber ich wollte sie natürlich nicht unnötig wiederholen. Dieses Mal wollte ich es mit Bedacht angehen: atomare Commits, bewusste Codeänderungen (und nicht „das Aneinanderreihen zufälliger Zeichen bis zur Kompilierung (mit Warnungen)“, wie Linus Torvalds laut Wikiquote einmal über jemanden sagte) usw.

Fehler Nummer drei: Ins Wasser gehen, ohne die Furt zu kennen

Ich bin immer noch nicht ganz davon losgeworden, aber jetzt habe ich beschlossen, überhaupt nicht den Weg des geringsten Widerstands zu gehen, sondern es „als Erwachsener“ zu tun, nämlich mein TCG-Backend von Grund auf neu zu schreiben, um es nicht zu tun um später sagen zu müssen: „Ja, das ist natürlich langsam, aber ich kann nicht alles kontrollieren – so schreibt man TCI ...“ Darüber hinaus schien dies zunächst eine naheliegende Lösung zu sein, da Ich generiere Binärcode. Wie sie sagen: „Gent hat sich versammeltу, aber nicht das eine“: Der Code ist natürlich binär, aber die Kontrolle kann nicht einfach darauf übertragen werden – er muss explizit zur Kompilierung in den Browser geschoben werden, was zu einem bestimmten Objekt aus der JS-Welt führt, das noch benötigt wird irgendwo gespeichert werden. Auf normalen RISC-Architekturen besteht jedoch, soweit ich weiß, eine typische Situation darin, dass der Befehlscache für regenerierten Code explizit zurückgesetzt werden muss. Wenn dies nicht das ist, was wir benötigen, ist es auf jeden Fall nah dran. Außerdem habe ich bei meinem letzten Versuch gelernt, dass die Kontrolle scheinbar nicht in die Mitte des Übersetzungsblocks übertragen wird, sodass wir den Bytecode nicht unbedingt von einem Offset aus interpretieren müssen und ihn einfach aus der Funktion auf TB generieren können .

Sie kamen und traten

Obwohl ich bereits im Juli damit begonnen habe, den Code neu zu schreiben, kam es unbemerkt zu einem magischen Kick: Normalerweise kommen Briefe von GitHub als Benachrichtigungen über Antworten auf Issues und Pull-Requests an, aber hier plötzlich Erwähnung im Thread Binaryen als Qemu-Backend im Kontext: „Er hat so etwas getan, vielleicht sagt er etwas.“ Wir haben über die Verwendung der zugehörigen Bibliothek von Emscripten gesprochen Binär um WASM JIT zu erstellen. Nun, ich sagte, dass Sie dort eine Apache 2.0-Lizenz haben und QEMU insgesamt unter GPLv2 vertrieben wird und sie nicht sehr kompatibel sind. Plötzlich stellte sich heraus, dass eine Lizenz möglich ist es irgendwie beheben (Ich weiß nicht: vielleicht ändern, vielleicht Doppellizenzierung, vielleicht etwas anderes ...). Das hat mich natürlich gefreut, denn zu diesem Zeitpunkt hatte ich es mir schon genau angeschaut Binärformat WebAssembly, und ich war irgendwie traurig und unverständlich. Es gab auch eine Bibliothek, die die Basisblöcke mit dem Übergangsgraphen verschlang, den Bytecode erzeugte und ihn bei Bedarf sogar im Interpreter selbst ausführte.

Dann war da noch mehr письмо auf der QEMU-Mailingliste, aber hier geht es eher um die Frage: „Wer braucht es überhaupt?“ Und es ist plötzlich, es stellte sich heraus, dass es notwendig war. Zumindest folgende Einsatzmöglichkeiten lassen sich zusammenkratzen, wenn es mehr oder weniger schnell klappt:

  • etwas Lehrreiches starten, ganz ohne Installation
  • Virtualisierung auf iOS, wo Gerüchten zufolge die einzige Anwendung, die das Recht zur Codegenerierung im laufenden Betrieb hat, eine JS-Engine ist (ist das wahr?)
  • Demonstration von Mini-Betriebssystemen – Single-Diskette, integriert, alle Arten von Firmware usw.

Browser-Laufzeitfunktionen

Wie ich bereits sagte, ist QEMU an Multithreading gebunden, der Browser verfügt jedoch nicht darüber. Nun, das heißt, nein ... Zuerst existierte es überhaupt nicht, dann tauchten WebWorker auf – soweit ich weiß, handelt es sich dabei um Multithreading, das auf der Nachrichtenübermittlung basiert ohne gemeinsam genutzte Variablen. Dies führt natürlich zu erheblichen Problemen bei der Portierung von vorhandenem Code auf Basis des Shared-Memory-Modells. Dann wurde es auf öffentlichen Druck hin auch unter dem Namen umgesetzt SharedArrayBuffers. Es wurde nach und nach eingeführt, sie feierten seinen Start in verschiedenen Browsern, dann feierten sie das neue Jahr und dann Meltdown ... Danach kamen sie zu dem Schluss, dass die Zeitmessung grob oder grob sein sollte, aber mit Hilfe von Shared Memory und a Thread, der den Zähler erhöht, es ist alles das Gleiche es wird ziemlich genau klappen. Deshalb haben wir Multithreading mit Shared Memory deaktiviert. Es scheint, dass sie es später wieder eingeschaltet haben, aber wie beim ersten Experiment klar wurde, gibt es ein Leben ohne es, und wenn ja, werden wir versuchen, es zu schaffen, ohne auf Multithreading angewiesen zu sein.

Das zweite Merkmal ist die Unmöglichkeit von Manipulationen auf niedriger Ebene mit dem Stapel: Sie können nicht einfach den aktuellen Kontext übernehmen, speichern und mit einem neuen Stapel zu einem neuen wechseln. Der Aufrufstapel wird von der virtuellen JS-Maschine verwaltet. Es scheint, was ist das Problem, da wir uns immer noch entschieden haben, die früheren Ströme vollständig manuell zu verwalten? Tatsache ist, dass Block-I/O in QEMU durch Coroutinen implementiert wird, und hier wären Stack-Manipulationen auf niedriger Ebene nützlich. Glücklicherweise enthält Emscipten bereits einen Mechanismus für asynchrone Operationen, sogar zwei: Asynchronisieren и Emdolmetscher. Die erste Lösung führt zu einer erheblichen Aufblähung des generierten JavaScript-Codes und wird nicht mehr unterstützt. Der zweite ist der aktuelle „richtige Weg“ und funktioniert über die Bytecode-Generierung für den nativen Interpreter. Es funktioniert natürlich langsam, aber es bläht den Code nicht auf. Zwar musste die Unterstützung für Coroutinen für diesen Mechanismus unabhängig beigesteuert werden (es gab bereits Coroutinen, die für Asyncify geschrieben wurden, und es gab eine Implementierung ungefähr derselben API für Emterpreter, man musste sie nur verbinden).

Im Moment ist es mir noch nicht gelungen, den Code in einen in WASM kompilierten und mit Emterpreter interpretierten Code aufzuteilen, daher funktionieren Blockgeräte noch nicht (siehe in der nächsten Serie, wie es heißt...). Das heißt, am Ende sollten Sie so etwas wie dieses lustige, vielschichtige Ding erhalten:

  • interpretierter Block-I/O. Haben Sie wirklich emuliertes NVMe mit nativer Leistung erwartet? 🙂
  • statisch kompilierter Haupt-QEMU-Code (Übersetzer, andere emulierte Geräte usw.)
  • dynamisch kompilierter Gastcode in WASM

Merkmale von QEMU-Quellen

Wie Sie wahrscheinlich bereits vermutet haben, sind der Code zum Emulieren von Gastarchitekturen und der Code zum Generieren von Host-Maschinenanweisungen in QEMU getrennt. Tatsächlich ist es sogar noch etwas kniffliger:

  • Es gibt Gastarchitekturen
  • es Beschleuniger, nämlich KVM für Hardware-Virtualisierung unter Linux (für miteinander kompatible Gast- und Hostsysteme), TCG für JIT-Codegenerierung überall. Ab QEMU 2.9 wurde der HAXM-Hardwarevirtualisierungsstandard unter Windows unterstützt (Einzelheiten)
  • Wenn TCG und keine Hardware-Virtualisierung verwendet wird, bietet es separate Unterstützung für die Codegenerierung für jede Host-Architektur sowie für den universellen Interpreter
  • ... und rundherum - emulierte Peripheriegeräte, Benutzeroberfläche, Migration, Aufnahme/Wiedergabe usw.

Wussten Sie übrigens: QEMU kann nicht nur den gesamten Computer emulieren, sondern auch den Prozessor für einen separaten Benutzerprozess im Host-Kernel, der beispielsweise vom AFL-Fuzzer zur binären Instrumentierung verwendet wird. Vielleicht möchte jemand diese Funktionsweise von QEMU auf JS portieren? 😉

Wie die meisten seit langem verfügbaren freien Software wird QEMU durch den Aufruf erstellt configure и make. Nehmen wir an, Sie möchten etwas hinzufügen: ein TCG-Backend, eine Thread-Implementierung oder etwas anderes. Beeilen Sie sich nicht, sich über die Aussicht auf eine Kommunikation mit Autoconf zu freuen bzw. zu entsetzen (zutreffendes unterstreichen). configure QEMUs ist offenbar selbst geschrieben und wird aus nichts generiert.

WebAssembly

Was ist also dieses Ding namens WebAssembly (auch bekannt als WASM)? Dies ist ein Ersatz für Asm.js und gibt nicht mehr vor, gültiger JavaScript-Code zu sein. Im Gegenteil, es ist rein binär und optimiert, und selbst das einfache Schreiben einer Ganzzahl darin ist nicht ganz einfach: Aus Gründen der Kompaktheit wird es im Format gespeichert LEB128.

Sie haben vielleicht schon vom Relooping-Algorithmus für Asm.js gehört – dabei handelt es sich um die Wiederherstellung von „High-Level“-Flusskontrollanweisungen (d. h. if-then-else, Schleifen usw.), für die JS-Engines entwickelt wurden die Low-Level-LLVM-IR, näher am vom Prozessor ausgeführten Maschinencode. Natürlich liegt die Zwischendarstellung von QEMU näher an der zweiten. Es scheint, dass es hier ist, Bytecode, das Ende der Qual... Und dann gibt es noch Blöcke, Wenn-Dann-Sonst- und Schleifen!...

Und das ist ein weiterer Grund, warum Binaryen nützlich ist: Es kann natürlich High-Level-Blöcke akzeptieren, die denen ähneln, die in WASM gespeichert würden. Es kann aber auch Code aus einem Diagramm grundlegender Blöcke und Übergänge zwischen ihnen erstellen. Nun, ich habe bereits gesagt, dass es das WebAssembly-Speicherformat hinter der praktischen C/C++-API verbirgt.

TCG (Tiny Code Generator)

TCG war ursprünglich Backend für den C-Compiler. Damals konnte es der Konkurrenz mit GCC offenbar nicht standhalten, fand aber am Ende seinen Platz in QEMU als Codegenerierungsmechanismus für die Host-Plattform. Es gibt auch ein TCG-Backend, das abstrakten Bytecode generiert, der sofort vom Interpreter ausgeführt wird, aber ich habe mich entschieden, dieses Mal darauf zu verzichten. Tatsache ist jedoch, dass es in QEMU bereits möglich ist, den Übergang zum generierten TB über die Funktion zu ermöglichen tcg_qemu_tb_exec, es hat sich für mich als sehr nützlich erwiesen.

Um ein neues TCG-Backend zu QEMU hinzuzufügen, müssen Sie ein Unterverzeichnis erstellen tcg/<имя архитектуры> (in diesem Fall, tcg/binaryen), und es enthält zwei Dateien: tcg-target.h и tcg-target.inc.c и verschreiben Es geht nur um configure. Sie können dort andere Dateien ablegen, aber wie Sie anhand der Namen dieser beiden erraten können, werden sie beide irgendwo eingebunden: eine als reguläre Header-Datei (sie ist in enthalten). tcg/tcg.h, und dieser befindet sich bereits in anderen Dateien in den Verzeichnissen tcg, accel und nicht nur), das andere - nur als Code-Snippet tcg/tcg.c, aber es hat Zugriff auf seine statischen Funktionen.

Da ich beschloss, dass ich zu viel Zeit mit detaillierten Untersuchungen der Funktionsweise verbringen würde, kopierte ich einfach die „Skelette“ dieser beiden Dateien aus einer anderen Backend-Implementierung und gab dies ehrlich im Lizenz-Header an.

Datei tcg-target.h enthält hauptsächlich Einstellungen im Formular #define-S:

  • Wie viele Register und welche Breite gibt es auf der Zielarchitektur (wir haben so viele wie wir wollen, so viele wie wir wollen – die Frage ist eher, was vom Browser auf der „vollständig Ziel“-Architektur in effizienteren Code generiert wird ...)
  • Ausrichtung von Host-Anweisungen: Auf x86 und sogar in TCI sind Anweisungen überhaupt nicht ausgerichtet, aber ich werde überhaupt keine Anweisungen in den Codepuffer einfügen, sondern Zeiger auf binäre Bibliotheksstrukturen, also sage ich: 4 Bytes
  • welche optionalen Anweisungen das Backend generieren kann – wir schließen alles ein, was wir in Binaryen finden, den Rest überlassen wir dem Beschleuniger selbst in einfachere Anweisungen
  • Wie groß ist der vom Backend angeforderte TLB-Cache ungefähr? Tatsache ist, dass in QEMU alles ernst ist: Obwohl es Hilfsfunktionen gibt, die das Laden/Speichern unter Berücksichtigung der Gast-MMU durchführen (was wären wir jetzt ohne sie?), speichern sie ihren Übersetzungscache in Form einer Struktur, der Die Verarbeitung lässt sich bequem direkt in Broadcast-Blöcke einbetten. Die Frage ist, welcher Offset in dieser Struktur von einer kleinen und schnellen Befehlsfolge am effizientesten verarbeitet wird?
  • Hier können Sie den Zweck eines oder zweier reservierter Register optimieren, den Aufruf von TB über eine Funktion aktivieren und optional ein paar kleine beschreiben inline-Funktionen wie flush_icache_range (aber das ist nicht unser Fall)

Datei tcg-target.inc.cist natürlich normalerweise viel größer und enthält mehrere obligatorische Funktionen:

  • Initialisierung, einschließlich Einschränkungen, welche Anweisungen mit welchen Operanden arbeiten können. Von mir offensichtlich von einem anderen Backend kopiert
  • Funktion, die eine interne Bytecode-Anweisung benötigt
  • Sie können hier auch Hilfsfunktionen einfügen und statische Funktionen von verwenden tcg/tcg.c

Für mich selbst habe ich folgende Strategie gewählt: In den ersten Wörtern des nächsten Übersetzungsblocks habe ich vier Zeiger notiert: eine Startmarke (ein bestimmter Wert in der Nähe). 0xFFFFFFFF, der den aktuellen Status der TB bestimmt), Kontext, generiertes Modul und magische Zahl für das Debuggen. Zunächst wurde die Marke angebracht 0xFFFFFFFF - nWo n - eine kleine positive Zahl, die bei jeder Ausführung durch den Interpreter um 1 erhöht wird 0xFFFFFFFE, die Kompilierung fand statt, das Modul wurde in der Funktionstabelle gespeichert, in einen kleinen „Launcher“ importiert, von dem aus die Ausführung erfolgte tcg_qemu_tb_exec, und das Modul wurde aus dem QEMU-Speicher entfernt.

Um die Klassiker zu paraphrasieren: „Crutch, wie viel ist in diesem Klang für das Herz des Progers verflochten …“. Allerdings ist der Speicher irgendwo durchgesickert. Darüber hinaus wurde der Speicher von QEMU verwaltet! Ich hatte einen Code, der beim Schreiben der nächsten Anweisung (also eines Zeigers) denjenigen löschte, dessen Link sich zuvor an dieser Stelle befand, aber das hat nicht geholfen. Tatsächlich reserviert QEMU im einfachsten Fall beim Start Speicher und schreibt den generierten Code dort. Wenn der Puffer leer ist, wird der Code verworfen und der nächste beginnt an seiner Stelle zu schreiben.

Nachdem ich den Code studiert hatte, wurde mir klar, dass der Trick mit der magischen Zahl es mir ermöglichte, bei der Heap-Zerstörung nicht zu scheitern, indem beim ersten Durchgang etwas Falsches in einem nicht initialisierten Puffer freigegeben wurde. Aber wer schreibt den Puffer neu, um meine Funktion später zu umgehen? Wie die Emscripten-Entwickler raten, habe ich, als ich auf ein Problem stieß, den resultierenden Code zurück in die native Anwendung portiert und Mozilla Record-Replay darauf eingestellt ... Im Allgemeinen wurde mir am Ende eine einfache Sache klar: Für jeden Block wurde A struct TranslationBlock mit seiner Beschreibung. Ratet mal, wo... Genau, direkt vor dem Block direkt im Puffer. Als mir dies klar wurde, beschloss ich, (zumindest einige) mit den Krücken aufzuhören, warf einfach die magische Zahl weg und übertrug die restlichen Wörter auf struct TranslationBlockDadurch wird eine einfach verknüpfte Liste erstellt, die beim Zurücksetzen des Übersetzungscache schnell durchlaufen werden kann und Speicher freigibt.

Einige Krücken bleiben bestehen: zum Beispiel markierte Zeiger im Codepuffer – einige davon sind einfach BinaryenExpressionRefDas heißt, sie betrachten die Ausdrücke, die linear in den generierten Basisblock eingefügt werden müssen. Ein Teil ist die Bedingung für den Übergang zwischen BBs, ein anderer Teil ist, wohin er gehen soll. Nun, es gibt bereits vorbereitete Blöcke für Relooper, die entsprechend den Bedingungen verbunden werden müssen. Um sie zu unterscheiden, wird davon ausgegangen, dass sie alle um mindestens vier Bytes ausgerichtet sind. Sie können also sicher die zwei niedrigstwertigen Bits für die Beschriftung verwenden. Sie müssen nur daran denken, sie bei Bedarf zu entfernen. Übrigens werden solche Labels bereits in QEMU verwendet, um den Grund für das Verlassen der TCG-Schleife anzuzeigen.

Verwendung von Binaryen

Module in WebAssembly enthalten Funktionen, von denen jede einen Körper enthält, der ein Ausdruck ist. Ausdrücke sind unäre und binäre Operationen, Blöcke, die aus Listen anderer Ausdrücke bestehen, Kontrollfluss usw. Wie ich bereits sagte, ist der Kontrollfluss hier genau in Form von Verzweigungen, Schleifen, Funktionsaufrufen usw. auf hoher Ebene organisiert. Argumente für Funktionen werden nicht auf dem Stapel übergeben, sondern explizit, genau wie in JS. Es gibt auch globale Variablen, aber ich habe sie nicht verwendet, daher werde ich Ihnen nichts darüber erzählen.

Funktionen verfügen auch über lokale, von Null an nummerierte Variablen vom Typ: int32 / int64 / float / double. In diesem Fall sind die ersten n lokalen Variablen die an die Funktion übergebenen Argumente. Bitte beachten Sie, dass hier zwar nicht alles in Bezug auf den Kontrollfluss ganz niedrig ist, Ganzzahlen jedoch immer noch nicht das Attribut „signed/unsigned“ tragen: Wie sich die Zahl verhält, hängt vom Operationscode ab.

Im Allgemeinen bietet Binaryen einfache C-API: Sie erstellen ein Modul, in ihm Erstellen Sie Ausdrücke – unär, binär, Blöcke aus anderen Ausdrücken, Kontrollfluss usw. Anschließend erstellen Sie eine Funktion mit einem Ausdruck als Rumpf. Wenn Sie wie ich über ein Übergangsdiagramm auf niedriger Ebene verfügen, hilft Ihnen die Relooper-Komponente. Soweit ich weiß, ist es möglich, den Ausführungsfluss in einem Block auf hoher Ebene zu steuern, solange er nicht über die Grenzen des Blocks hinausgeht – das heißt, es ist möglich, den internen schnellen Pfad / langsam zu machen Pfadverzweigung innerhalb des integrierten TLB-Cache-Verarbeitungscodes, ohne jedoch den „externen“ Kontrollfluss zu beeinträchtigen. Wenn Sie einen Relooper freigeben, werden seine Blöcke freigegeben; wenn Sie ein Modul freigeben, verschwinden die ihm zugewiesenen Ausdrücke, Funktionen usw Arena.

Wenn Sie jedoch Code im Handumdrehen interpretieren möchten, ohne unnötige Erstellung und Löschung einer Interpreter-Instanz, kann es sinnvoll sein, diese Logik in eine C++-Datei zu schreiben und von dort aus direkt die gesamte C++-API der Bibliothek zu verwalten und dabei vorgefertigte Codes zu umgehen. Wrapper gemacht.

Um den Code zu generieren, den Sie benötigen

// настроить глобальные параметры (можно поменять потом)
BinaryenSetAPITracing(0);

BinaryenSetOptimizeLevel(3);
BinaryenSetShrinkLevel(2);

// создать модуль
BinaryenModuleRef MODULE = BinaryenModuleCreate();

// описать типы функций (как создаваемых, так и вызываемых)
helper_type  BinaryenAddFunctionType(MODULE, "helper-func", BinaryenTypeInt32(), int32_helper_args, ARRAY_SIZE(int32_helper_args));
// (int23_helper_args приоб^Wсоздаются отдельно)

// сконструировать супер-мега выражение
// ... ну тут уж вы как-нибудь сами :)

// потом создать функцию
BinaryenAddFunction(MODULE, "tb_fun", tb_func_type, func_locals, FUNC_LOCALS_COUNT, expr);
BinaryenAddFunctionExport(MODULE, "tb_fun", "tb_fun");
...
BinaryenSetMemory(MODULE, (1 << 15) - 1, -1, NULL, NULL, NULL, NULL, NULL, 0, 0);
BinaryenAddMemoryImport(MODULE, NULL, "env", "memory", 0);
BinaryenAddTableImport(MODULE, NULL, "env", "tb_funcs");

// запросить валидацию и оптимизацию при желании
assert (BinaryenModuleValidate(MODULE));
BinaryenModuleOptimize(MODULE);

... falls ich etwas vergessen habe, tut mir leid, dies dient nur zur Darstellung des Maßstabs und die Details finden Sie in der Dokumentation.

Und jetzt beginnt das Crack-Fex-Pex, etwa so:

static char buf[1 << 20];
BinaryenModuleOptimize(MODULE);
BinaryenSetMemory(MODULE, 0, -1, NULL, NULL, NULL, NULL, NULL, 0, 0);
int sz = BinaryenModuleWrite(MODULE, buf, sizeof(buf));
BinaryenModuleDispose(MODULE);
EM_ASM({
  var module = new WebAssembly.Module(new Uint8Array(wasmMemory.buffer, $0, $1));
  var fptr = $2;
  var instance = new WebAssembly.Instance(module, {
      'env': {
          'memory': wasmMemory,
          // ...
      }
  );
  // и вот уже у вас есть instance!
}, buf, sz);

Um die Welten von QEMU und JS irgendwie zu verbinden und gleichzeitig schnell auf die kompilierten Funktionen zuzugreifen, wurde ein Array erstellt (eine Funktionstabelle zum Import in den Launcher) und die generierten Funktionen dort platziert. Um den Index schnell zu berechnen, wurde zunächst der Index des Nullwort-Übersetzungsblocks verwendet, aber dann begann der mit dieser Formel berechnete Index einfach in das Feld hinein zu passen struct TranslationBlock.

Übrigens Demo (derzeit mit unklarer Lizenz) Funktioniert nur in Firefox einwandfrei. Chrome-Entwickler waren irgendwie nicht bereit auf die Tatsache, dass jemand mehr als tausend Instanzen von WebAssembly-Modulen erstellen möchte, also hat er einfach jedem ein Gigabyte virtuellen Adressraum zugewiesen ...

Das ist alles für den Moment. Vielleicht gibt es noch einen Artikel, falls es jemanden interessiert. Es bleibt nämlich zumindest nur Sorgen Sie dafür, dass Blockgeräte funktionieren. Es kann auch sinnvoll sein, die Kompilierung von WebAssembly-Modulen asynchron zu gestalten, wie es in der JS-Welt üblich ist, da es noch einen Interpreter gibt, der dies alles tun kann, bis das native Modul fertig ist.

Zum Schluss noch ein Rätsel: Sie haben eine Binärdatei auf einer 32-Bit-Architektur kompiliert, aber der Code klettert durch Speicheroperationen von Binaryen irgendwo auf den Stapel oder irgendwo anders in die oberen 2 GB des 32-Bit-Adressraums. Das Problem besteht darin, dass aus Binaryens Sicht auf eine zu große resultierende Adresse zugegriffen wird. Wie kann man das umgehen?

Auf die Art und Weise des Administrators

Am Ende habe ich das nicht getestet, aber mein erster Gedanke war: „Was wäre, wenn ich 32-Bit-Linux installieren würde?“ Dann wird der obere Teil des Adressraums vom Kernel belegt. Die Frage ist nur, wie viel belegt wird: 1 oder 2 GB.

Auf die Art und Weise eines Programmierers (Option für Praktiker)

Lassen Sie uns oben im Adressraum eine Blase blasen. Ich selbst verstehe nicht, warum es dort funktioniert bereits Es muss ein Stapel vorhanden sein. Aber „wir sind Praktiker: Bei uns funktioniert alles, aber niemand weiß warum …“

// 2gbubble.c
// Usage: LD_PRELOAD=2gbubble.so <program>

#include <sys/mman.h>
#include <assert.h>

void __attribute__((constructor)) constr(void)
{
  assert(MAP_FAILED != mmap(1u >> 31, (1u >> 31) - (1u >> 20), PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0));
}

... es ist wahr, dass es nicht mit Valgrind kompatibel ist, aber glücklicherweise drängt Valgrind selbst sehr effektiv alle da raus :)

Vielleicht kann jemand eine bessere Erklärung geben, wie dieser Code von mir funktioniert ...

Source: habr.com

Kommentar hinzufügen