Vor langer Zeit beschloss ich aus Spaß, die Umkehrbarkeit des Prozesses zu beweisen, und zu lernen, wie man JavaScript (genauer gesagt, Asm.js) aus Maschinencode generiert. Für dieses Experiment wählte ich QEMU, und einige Zeit später schrieb ich einen Artikel auf Habr. In den Kommentaren rieten mir die Leser, das Projekt auf WebAssembly umzuschreiben, aber ich wollte mein fast fertiggestelltes Projekt nicht einfach so aufgeben... Die Arbeit ging voran, aber sehr langsam, und neulich erschien in diesem Artikel die Frage: „Wie ist es denn ausgegangen?“. Auf meine ausführliche Antwort erhielt ich den Kommentar: „Das klingt nach einem Artikel.“ Nun, wenn es nach einem Artikel klingt, wird es auch einen geben. Vielleicht kann jemand davon profitieren. Darin erfährt der Leser einige Fakten über die Struktur der Backend-Codegenerierung von QEMU und wie man einen Just-in-Time-Compiler für eine Webanwendung schreibt. Da ich bereits gelernt hatte, QEMU „irgendwie“ nach JavaScript zu portieren, wurde diesmal beschlossen, es richtig zu machen und alte Fehler nicht zu wiederholen.
Aufgaben
Fehler Nummer eins: Abzweigen vom Point Release.
Mein erster Fehler war, meine Version von der upstream-Version 2.4.1 abzuzweigen. Damals 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 erst recht als der Branch.
Mein erster Fehler war, meine Version vom Upstream 2.4.1 abzugleiten. Damals schien mir das eine gute Idee zu sein: wenn es ein Point-Release gibt, dann ist es wahrscheinlich stabiler als das einfache 2.4, vor allem im Vergleich zur Hauptversion. master. Da ich plante, eine erhebliche Anzahl meiner eigenen Bugs hinzuzufügen, waren externe für mich absolut überflüssig. So kam es wahrscheinlich auch. Doch das Problem war, dass QEMU nicht stillsteht, und irgendwann haben sie sogar eine Code-Generierungsoptimierung um 10 % angekündigt. „Aha, jetzt werde ich es einfügen“, dachte ich und stieß auf Widerstand. Hier muss ich einen Rückblick halten: Aufgrund der einheitlichen Ausführung von QEMU.js und der Tatsache, dass das ursprüngliche QEMU Mehrkernverarbeitung nicht berücksichtigt (das heißt, es ist entscheidend, dass mehrere unabhängige Code-Pfade gleichzeitig arbeiten können, anstatt einfach alle Kerne zu nutzen), mussten die Hauptfunktionen der Threads „herausgedreht“ werden, um einen externen Aufruf zu ermöglichen. Das führte zu einigen natürlichen Problemen beim Mergen. Dennoch wäre der Umstand, dass ein Teil der Änderungen aus dem Branch, master, in den ich versuchte, meinen Code zu integrieren, ebenfalls als Cherry-Pick in eine Punktversion integriert wurde (und damit auch in meinen Branch), wahrscheinlich nicht hilfreich gewesen.
Insgesamt habe ich beschlossen, dass es sinnvoll ist, den Prototypen auseinanderzunehmen und eine neue Version von Grund auf auf der Basis von etwas Aktuellerem zu entwickeln, und jetzt auch master.
Fehler Nummer zwei: TLP-Methodologie
Im Grunde genommen ist das eigentlich kein Fehler, sondern vielmehr eine Besonderheit bei der Projekterstellung unter Bedingungen völliger Unklarheit darüber, "in welche Richtung und wie wir uns bewegen?" sowie überhaupt "werden wir es schaffen?" Unter diesen Umständen Husch-husch Programmierung war eine vertretbare Option, aber natürlich wollte ich das nicht unnötig wiederholen. Diesmal wollte ich es richtig machen: atomare Commits, bewusste Codeänderungen (und nicht "zufällige Zeichen zusammenfügen, bis es kompiliert (mit Warnungen)", wie Linus Torvalds einmal über jemanden gesagt haben soll, wenn man der Wikizitaten glaubt) usw.
Fehler Nummer drei: Ohne das Wasser zu kennen, ins Wasser springen
Davor habe ich mich bis jetzt nicht vollständig befreit, aber jetzt habe ich beschlossen, nicht den Weg des geringsten Widerstands zu wählen und es "für Erwachsene" zu machen, nämlich mein TCG-Backend von Grund auf neu zu schreiben, um später nicht sagen zu müssen: "Ja, das dauert natürlich, aber ich kann nicht alles kontrollieren — TCI ist so geschrieben...". Außerdem schien es von Anfang an eine offensichtliche Lösung zu sein, da ich bin schließlich der, der den Binärcode generiert. Wie man so schön sagt, "Gent zusammengebaut"u, aber das Management kann nicht einfach so übertragen werden — es muss explizit in den Browser für die Kompilierung gepackt werden, um ein Objekt aus der Welt von JS zu erhalten, das dann irgendwo gespeichert werden muss. Soweit ich verstehe, ist es auf normalen RISC-Architekturen üblich, dass man den Cache von Anweisungen für den neu generierten Code explizit zurücksetzen muss — wenn das nicht das ist, was wir brauchen, dann ist es jedenfalls nahe dran. Außerdem habe ich aus meinem letzten Versuch gelernt, dass das Management in die Mitte des Translationsblocks nicht übertragen wird, deshalb brauchen wir den Bytecode, der von einer beliebigen Position interpretiert wird, nicht wirklich und können einfach eine Funktion auf TB generieren.
Sie kamen und traten.
Obwohl ich bereits im Juli damit begonnen habe, den Code umzuschreiben, schlich sich der magische Tritt unbemerkt ein: Normalerweise kommen die E-Mails von GitHub als Benachrichtigungen über Antworten zu Issues und Pull Requests, aber hier, plötzlich eine Erwähnung im Thread im Kontext: „Sehen Sie, er hat etwas Ähnliches gemacht, vielleicht sagt er etwas“. Es ging um die Verwendung der verwandten Bibliothek zu Emscripten, für die Erstellung von WASM JIT. Ich habe gesagt, dass Sie eine Apache 2.0-Lizenz haben, während QEMU als Gesamtpaket unter GPLv2 lizenziert ist, und diese sind nicht sehr kompatibel. Plötzlich stellte sich heraus, dass die Lizenz irgendwie angepasst werden kann (ich weiß nicht: vielleicht ändern, vielleicht doppelte Lizenzierung, vielleicht noch etwas anderes…). Das hat mich natürlich gefreut, denn ich hatte mich bis zu diesem Zeitpunkt schon mehrmals für WebAssembly interessiert, und es war mir ein wenig traurig und unverständlich. Hier gab es eine Bibliothek, die sowohl die Grundblöcke als auch das Übergraf drucken konnte, Bytecode ausgeben konnte und ihn sogar im Interpreter ausführen konnte, wenn notwendig.
Dann gab es noch in der QEMU-Mailingliste, aber das führt eher zur Frage: „Wer braucht das eigentlich?“ Und es stellte sich heraus, dass es nötig war. Mindestens kann man solche Nutzungsmöglichkeiten skizzieren, wenn es mehr oder weniger zügig funktioniert: plötzlichetwas Lehrreiches ganz ohne Installation
- Virtualisierung auf iOS, wo Gerüchte besagen, dass die einzige Anwendung, die das Recht auf On-the-Fly-Code-Generierung hat, die JS-Engine ist (stimmt das wirklich?)
- eine Demonstration eines Mini-Betriebssystems – Disketten-OS, Embedded-Systeme, verschiedene Firmware usw…
- Demonstration eines Mini-Betriebssystems – von Disketten, Embedded-Systemen, Firmware usw.
Merkmale der Browser-Laufzeitumgebung
Wie bereits erwähnt, ist QEMU auf Multithreading angewiesen, was im Browser nicht vorhanden ist. Naja, nicht direkt. Zuerst gab es überhaupt kein Multithreading, aber dann kamen die WebWorkers — so viel ich weiß, basiert das auf Nachrichtenaustausch. ohne gemeinsam veränderbare Variablen.Das schafft selbstverständlich erhebliche Probleme beim Portieren vorhandenen Codes, der auf einem Shared-Memory-Modell basiert. Unter dem Druck der Öffentlichkeit wurde schließlich dieses Konzept entwickelt, unter dem Namen SharedArrayBuffers.Diese wurden schrittweise eingeführt, die Einführung in verschiedenen Browsern gefeiert, dann feierte man Neujahr und anschließend Meltdown… Danach kamen die Erkenntnisse, dass, egal wie man die Zeit misst, durch shared memory und einen Thread, der einen Zähler inkrementiert, man dennoch So wurde das Multithreading mit gemeinsamem Speicher schließlich deaktiviert. Zunächst wurde es später wieder aktiviert, doch wie aus dem ersten Experiment deutlich wurde, kann man auch ohne es gut weiterarbeiten, und wenn dem so ist, versuchen wir es einfach ohne Multithreading.
Das zweite Merkmal besteht darin, dass niedrigstufige Manipulationen mit dem Stack nicht möglich sind: Man kann nicht einfach den aktuellen Kontext speichern und auf einen neuen Stack wechseln. Der Aufrufstack wird von der JS-VM verwaltet. Aber wo liegt das Problem, da wir die ehemaligen Threads schließlich komplett manuell verwalten möchten? Das Problem ist, dass der blockierende I/O in QEMU durch Koroutinen implementiert ist, und genau hier wären niedrigstufige Manipulationen mit dem Stack nützlich. Glücklicherweise enthält Emscripten bereits einen Mechanismus für asynchrone Operationen, sogar zwei: und . Der erste führt zu einer erheblichen Aufblähung des generierten JavaScript-Codes und wird bereits nicht mehr unterstützt. Der zweite ist der aktuelle "richtige Weg" und funktioniert durch die Erzeugung von Bytecode für den eigenen Interpreter. Es läuft natürlich langsam, bläht aber den Code nicht auf. Allerdings musste die Unterstützung für Koroutinen in diesem Mechanismus selbst beigetragen werden (es gab bereits Koroutinen, die für Asyncify geschrieben wurden, und eine Implementierung eines ungefähr gleichen APIs für Emterpreter, die nur zusammengeführt werden mussten).
Im Moment habe ich den Code noch nicht in kompilierbaren WASM-Code und interpretierten Code mit Emterpreter unterteilt, deshalb funktionieren die Blockgeräte noch nicht (sehen Sie in den nächsten Folgen, wie es heißt…). Das sollte letztendlich so eine amüsante, schichtartige Sache ergeben:
- interpretierten Block-I/O. Und was erwarteten Sie, ein emuliertes NVMe mit nativer Leistung? 🙂
- statisch kompilierter Basiscode von QEMU (Translater, andere emulierte Geräte usw.)
- dynamisch in WASM kompilierter Gastcode
Besonderheiten der QEMU-Quellen
Wie Sie wahrscheinlich schon erraten haben, sind der Code zur Emulation der Gastarchitekturen und der Code zur Generierung der Host-Maschineninstruktionen in QEMU getrennt. Tatsächlich ist es dort sogar etwas komplizierter:
- es gibt Gastarchitekturen
- gibt es Beschleuniger, insbesondere KVM für Hardware-Virtualisierung unter Linux (für kompatible Gast- und Host-Systeme) sowie TCG für JIT-Code-Generierung überall. Mit QEMU 2.9 wurde die Unterstützung für den Hardware-Virtualisierungsstandard HAXM unter Windows eingeführt ()
- Wenn TCG anstelle von hardwarebasierter Virtualisierung verwendet wird, gibt es eine separate Unterstützung für die Codegenerierung für jede Host-Architektur sowie für einen universellen Interpreter.
- … und rund um all dies — emulierte Peripherie, Benutzeroberfläche, Migration, Record-Replay usw.
Übrigens, wussten Sie: QEMU kann nicht nur einen gesamten Computer emulieren, sondern auch einen Prozessor für einen einzelnen Benutzerprozess im Host-Kernel, was beispielsweise von AFL-Fuzzer zur Instrumentierung von Binärdateien genutzt wird. Vielleicht möchte jemand diesen Betriebsmodus von QEMU nach JS portieren? 😉
Wie die meisten langjährigen Open-Source-Programme wird QEMU über einen Aufruf erstellt. configure und makeAngenommen, Sie möchten etwas hinzufügen: einen TCG-Backend, eine Implementierung von Threads, etwas anderes. Freuen Sie sich nicht zu früh über oder fürchten Sie (das gewünschte hervorheben) die Aussicht auf die Kommunikation mit Autoconf — in Wirklichkeit configure scheint QEMU anscheinend handgeschrieben zu sein und wird aus nichts generiert.
WebAssembly
Was genau ist WebAssembly (auch als WASM bekannt)? Es ist der Nachfolger von Asm.js und gibt sich nicht mehr als gültiger JavaScript-Code aus. Im Gegenteil, es ist rein binär und optimiert. Selbst das einfache Speichern einer Ganzzahl gestaltet sich als nicht ganz unkompliziert: Es wird zur Kompaktheit im Format .
Vielleicht haben Sie von dem Relooping-Algorithmus für Asm.js gehört – dabei geht es um die Wiederherstellung von 'hochgradigen' Steuerflussanweisungen (wie if-then-else, Schleifen usw.), die für JS-Engines optimiert sind, aus der niedriggradigen LLVM IR, die näher am Maschinencode liegt, der vom Prozessor ausgeführt wird. Natürlich ist die Zwischenrepräsentation von QEMU näher am zweiten Ansatz. Man könnte denken, here is der Bytecode, das Ende der Qualen… Und dann kommen die Blöcke, if-then-else und Schleifen!
Und das ist ein weiterer Grund, warum Binaryen nützlich ist: Es kann selbstverständlich hochgradige Blöcke verarbeiten, die dem entsprechen, was in WASM gespeichert werden wird. Aber es kann auch Code aus dem Graphen der Basisblöcke und deren Übergängen erzeugen. Und was es hinter dem benutzerfreundlichen C/C++ API zur Speicherung von WebAssembly verbirgt, habe ich bereits erwähnt.
TCG (Tiny Code Generator)
TCG Backend für den C-Compiler. Offenbar konnte er nicht mit GCC konkurrieren, fand aber schließlich seinen Platz in QEMU als Codegenerierungsmechanismus für die Host-Plattform. Es gibt auch einen TCG-Backend, der eine Art abstrakten Bytecode generiert, den der Interpreter dann sofort ausführt, aber ich habe mich entschlossen, diesmal darauf zu verzichten. Dennoch kam mir die Tatsache, dass QEMU bereits die Möglichkeit bietet, über die Funktion tcg_qemu_tb_exec, auf den generierten TB zuzugreifen, sehr gelegen.
Um einen neuen TCG-Backend in QEMU hinzuzufügen, muss ein Unterverzeichnis erstellt werden tcg/ (in diesem Fall, tcg/binaryen), und darin zwei Dateien: tcg-target.h und tcg-target.inc.c und alles in configure. Man kann auch andere Dateien dort ablegen, doch wie man aus den Namen dieser beiden erahnen kann, werden beide irgendwo eingebunden: eine als gewöhnliche Header-Datei (sie wird in tcg/tcg.hinkludiert, und die wird dann in andere Dateien in den Verzeichnissen tcg, accel und nicht nur), die andere nur als Code-Snippet in tcg/tcg.c, allerdings hat sie Zugang zu seinen statischen Funktionen.
Da ich entschied, dass ich zu viel Zeit mit den Details verbringen würde, wie es funktioniert, habe ich einfach die „Skeletons“ dieser beiden Dateien aus einer anderen Backend-Implementierung kopiert und dies ehrlich im Lizenzheader angegeben.
Die Datei tcg-target.h enthält hauptsächlich Einstellungen in Form von #define-en:
- wie viele Register und welche Breite auf der Zielarchitektur vorhanden sind (bei uns – so viele, wie wir wollen – es geht mehr darum, was der Browser in effektiveren Code für die „komplett zielgerichtete“ Architektur generiert...)
- Ausrichtung der Host-Befehle: Auf x86 und auch in TCI werden Befehle überhaupt nicht ausgerichtet, ich beabsichtige jedoch, in den Codepuffer nicht Befehle, sondern Zeiger auf Strukturen der Binaryen-Bibliothek zu legen, daher sage ich: 4 Bytes
- welche optionalen Befehle das Backend generieren kann – wir aktivieren alles, was wir in Binaryen finden, den Rest darf der Beschleuniger in einfachere umwandeln.
- Welche ungefähre Größe des TLB-Caches fordert das Backend an? Bei QEMU ist alles ernsthaft: Es gibt zwar Hilfsfunktionen, die Load/Store unter Berücksichtigung der Gast-MMU durchführen (worauf könnte man heutzutage verzichten?), aber ihren Übersetzungsspeicher speichern sie in Form einer Struktur, deren Verarbeitung bequem direkt in die Übersetzungsblöcke integriert werden kann. Die Frage ist jedoch, welche Verschiebung in dieser Struktur von einer kleinen und schnellen Befehlskette am effektivsten verarbeitet wird.
- Hier kann man auch die Zuordnung eines oder zwei reservierter Register anpassen, den TB-Aufruf über eine Funktion aktivieren und optional ein paar kleine Funktionen beschreiben.
inline-Funktionen wieflush_icache_range(aber das ist nicht unser Fall)
Die Datei tcg-target.inc.c, enthält in der Regel natürlich deutlich mehr Funktionen und umfasst mehrere zwingend erforderliche Funktionen:
- Initialisierung, die unter anderem Einschränkungen angibt, welche Anweisung mit welchen Operanden operieren kann. Dreist von mir aus einem anderen Backend kopiert.
- Funktion, die eine Anweisung des internen Bytecodes akzeptiert.
- Hier können auch Hilfsfunktionen platziert werden, und es können statische Funktionen aus genutzt werden.
tcg/tcg.c
Ich habe mir die folgende Strategie überlegt: In den ersten Worten jedes Übertragungsblocks notierte ich vier Indikatoren: ein Startlabel (ein Wert in der Nähe von 0xFFFFFFFF, anhand dessen der aktuelle Zustand des TB bestimmt wurde), den Kontext, das generierte Modul und eine Magic-Nummer zur Fehlersuche. Zunächst wurde das Label auf 0xFFFFFFFF - n, wobei n — einer kleinen positiven Zahl gesetzt, und bei jeder Ausführung über den Interpreter wurde es um 1 erhöht. Wenn es 0xFFFFFFFEerreichte, fand die Kompilierung statt, das Modul wurde in der Funktionstabelle gespeichert, die in einen kleinen „Launcher“ importiert wurde, aus dem die Ausführung erfolgte, und das Modul wurde aus dem Speicher von QEMU entfernt. tcg_qemu_tb_exec, und das Modul wurde aus dem QEMU-Speicher entfernt.
Um einen Klassiker umzuformulieren: „Wie viel in diesem Klang für das Herz eines Programmierers verwoben ist...“. Dennoch schien der Speicher irgendwo verloren zu gehen. Es handelte sich dabei um Speicher, der von QEMU verwaltet wurde! Ich hatte einen Code, der beim Schreiben einer neuen Anweisung (also des Zeigers) die vorherige Referenz an diesem Ort entfernte, aber das half nicht. Tatsächlich reserviert QEMU im einfachsten Fall beim Start Speicher und schreibt den generierten Code dort hinein. Wenn der Puffer endet, wird der Code verworfen und der nächste wird an seiner Stelle geschrieben.
Nachdem ich den Code studiert hatte, wurde mir klar, dass der Hack mit der Magic Number verhinderte, dass der Heap beim ersten Durchlauf durch das Freigeben von etwas Unpassendem im nicht initialisierten Puffer abstürzte. Aber wer schreibt später den Puffer um, um meine Funktion zu umgehen? Wie von den Entwicklern von Emscripten empfohlen, habe ich das resultierende Code zurück in eine native Anwendung portiert und Mozilla Record-Replay darauf angesetzt... Letztendlich wurde mir eine einfache Sache klar: Für jeden Block wird reserviert. struct TranslationBlock mit seiner Beschreibung. Erraten Sie, wo… Richtig, direkt vor dem Block, gleich im Puffer. Als ich das realisierte, beschloss ich, mit den Workarounds (wenigstens einigen) Schluss zu machen und einfach die magische Zahl wegzuwerfen, während ich die verbleibenden Wörter in struct TranslationBlock, eine einfach verkettete Liste anlegender, durch die man schnell beim Zurücksetzen des Transaktions-Caches durchgehen und Speicher freigeben kann.
Einige Workarounds sind geblieben: zum Beispiel die markierten Zeiger im Code-Puffer – ein Teil von ihnen sind einfach BinaryenExpressionRef, das heißt, sie verweisen auf Ausdrücke, die linear im zu generierenden Basisblock abgelegt werden müssen, ein Teil – die Übergangsbedingung zwischen den Blöcken, ein Teil – wohin man wechseln soll. Außerdem gibt es bereits vorbereitete Blöcke für den Relooper, die je nach Bedingungen verbunden werden müssen. Um sie zu unterscheiden, wird die Annahme genutzt, dass alle mindestens auf vier Bytes ausgerichtet sind, sodass die unteren zwei Bits zur Markierung verwendet werden können, wobei nur nicht vergessen werden darf, sie bei Bedarf zu entfernen. Übrigens werden solche Markierungen bereits in QEMU verwendet, um den Grund für das Verlassen der TCG-Schleife anzuzeigen.
Verwendung von Binaryen
Module in WebAssembly enthalten Funktionen, deren Rumpf einen Ausdruck darstellt. Ausdrücke sind unäre und binäre Operationen, Blöcke, die aus Listen anderer Ausdrücke bestehen, Kontrollfluss usw. Wie bereits erwähnt, wird der Kontrollfluss hier als hochgradig abstrahierte Verzweigungen, Schleifen, Funktionsaufrufe usw. organisiert. Argumente werden nicht über den Stack, sondern explizit wie in JS an Funktionen übergeben. Es gibt auch globale Variablen, die ich jedoch nicht verwendet habe, daher werde ich nicht darüber sprechen.
Außerdem haben Funktionen nullbasierte, nummerierte lokale Variablen, die den Typen int32 / int64 / float / double haben. Dabei sind die ersten n lokalen Variablen die an die Funktion übergebenen Argumente. Beachten Sie, dass, obwohl hier alles nicht ganz niederträchtig im Hinblick auf den Kontrollfluss ist, ganze Zahlen dennoch kein Zeichen-/Unsicherheitsmerkmal aufweisen: Wie sich eine Zahl verhält, hängt vom Operationstyp ab.
Im Allgemeinen bietet Binaryen : Sie erstellen ein Modul, in dem Sie erstellen Ausdrücke – unäre, binäre, Blöcke aus anderen Ausdrücken, Steuerfluss usw. Dann definieren Sie eine Funktion, deren Körper das Ausdruck sein muss. Wenn Sie, wie ich, ein niedrigstufiges Übergraf haben, wird Ihnen die Komponente relooper helfen. Soweit ich verstehe, können Sie hochgradige Steuerungsstrukturen innerhalb eines Blocks verwenden, solange sie nicht über den Block hinausgehen – das heißt, es ist möglich, eine interne Zweigung zwischen fast path und slow path im eingebetteten TLB-Cache-Verarbeitungscode zu machen, aber in den 'äußeren' Steuerfluss einzugreifen, ist nicht erlaubt. Wenn Sie relooper freigeben, werden deren Blöcke freigegeben, und wenn Sie das Modul freigeben, verschwinden die Ausdrücke, Funktionen usw., die ihm zugewiesen sind. Arena.
Falls Sie jedoch den Code zur Laufzeit ohne unnötige Instanzen des Interpreters erstellen und löschen möchten, könnte es sinnvoll sein, diese Logik in eine C++-Datei auszulagern und direkt die gesamte C++-API der Bibliothek zu steuern, ohne auf fertige Wrapper zurückzugreifen.
Um also Code zu generieren, benötigt man
// настроить глобальные параметры (можно поменять потом)
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 – entschuldigen Sie, das ist lediglich zur Veranschaulichung der Größenordnung, die Details finden Sie in der Dokumentation.
Und jetzt beginnt das Krex-Fex-Pex, ungefähr 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,
// ...
}
);
// und schon haben Sie eine Instanz!
}, buf, sz);Um eine Verbindung zwischen der QEMU-Welt und JS herzustellen und gleichzeitig schnell in die kompilierten Funktionen einzutauchen, wurde ein Array (eine Funktionstabelle zum Import in den Launcher) erstellt, und dort wurden die generierten Funktionen abgelegt. Um den Index schnell zu berechnen, wurde anfänglich der Index des Null-Word-Translation-Blocks verwendet, aber später wurde der nach dieser Formel berechnete Index einfach in das Feld eingetragen. struct TranslationBlock.
Übrigens, (bislang mit einer fragwürdigen Lizenz) läuft nur im Firefox gut. Die Chrome-Entwickler waren irgendwie nicht vorbereitet darauf, dass jemand mehr als tausend Instanzen von WebAssembly-Modulen erstellen möchte, weshalb sie einfach ein Gigabyte virtuellen Adressraums für jede Instanz zugewiesen haben…
Das wäre vorerst alles. Möglicherweise wird es einen weiteren Artikel geben, wenn es jemanden interessiert. Insbesondere bleibt noch mindestens nur die blockierenden Geräte zum Laufen zu bringen. Vielleicht macht es auch Sinn, die Kompilierung von WebAssembly-Modulen asynchron zu gestalten, wie es in der JS-Welt üblich ist, da es ohnehin einen Interpreter gibt, der alles ausführen kann, während das native Modul noch nicht bereit ist.
Zum Schluss ein Rätsel: Sie haben ein Binary für eine 32-Bit-Architektur erstellt, aber der Code greift durch Speicheroperationen, die aus Binaryen stammen, irgendwo auf den Stack oder anderweitig in die oberen 2 GB des 32-Bit-Adressraums zu. Das Problem besteht darin, dass dieser Zugriff aus der Sicht von Binaryen auf eine zu große resultierende Adresse verweist. Wie umgehen Sie das?
Admin-Sicht
Ich habe das letztendlich nicht getestet, aber der erste Gedanke war: „Was wäre, wenn man Linux in 32 Bit installiert?“ Dann wäre der obere Teil des Adressraums vom Kernel belegt. Die Frage ist nur, wie viel belegt wird: 1 oder 2 GB.
Entwicklersicht (Praktiker-Version)
Wir erzeugen einen Bubble im oberen Bereich des Adressraums. Ich verstehe selbst nicht, warum das funktioniert – da ist doch bereits Es muss ein Stack 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));
}… mit Valgrind ist es allerdings nicht kompatibel, aber glücklicherweise verdrängt Valgrind alle sehr effektiv daraus 🙂
Vielleicht gibt es jemanden, der eine bessere Erklärung dafür gibt, wie dieser Code funktioniert…
Quelle: habr.com
