QEMU.js: nu serieus en met WASM

Ooit besloot ik voor de lol bewijzen de omkeerbaarheid van het proces en leer hoe u JavaScript (meer precies, Asm.js) kunt genereren op basis van machinecode. Voor het experiment werd QEMU gekozen en enige tijd later werd er een artikel geschreven over Habr. In de reacties kreeg ik het advies om het project opnieuw te maken in WebAssembly en zelfs te stoppen bijna klaar Op de een of andere manier wilde ik het project niet... Het werk ging door, maar heel langzaam, en nu verscheen onlangs in dat artikel комментарий over het onderwerp “Hoe is het allemaal afgelopen?” Als reactie op mijn gedetailleerde antwoord hoorde ik: “Dit klinkt als een artikel.” Nou, als je kunt, zal er een artikel zijn. Misschien vindt iemand het nuttig. Hieruit zal de lezer enkele feiten leren over het ontwerp van QEMU-backends voor het genereren van code, evenals hoe een Just-in-Time-compiler voor een webapplicatie moet worden geschreven.

taken

Omdat ik al had geleerd hoe ik QEMU “op de een of andere manier” naar JavaScript kon porten, werd er deze keer besloten om het verstandig te doen en geen oude fouten te herhalen.

Fout nummer één: vertakking vanaf puntvrijgave

Mijn eerste fout was om mijn versie af te splitsen van de upstream-versie 2.4.1. Toen leek het mij een goed idee: als puntrelease bestaat, dan is het waarschijnlijk stabieler dan simpel 2.4, en nog meer de branch master. En aangezien ik van plan was een behoorlijk aantal van mijn eigen bugs toe te voegen, had ik die van iemand anders helemaal niet nodig. Zo is het waarschijnlijk ook gegaan. Maar het punt is: QEMU staat niet stil, en op een gegeven moment hebben ze zelfs een optimalisatie van de gegenereerde code met 10 procent aangekondigd. “Ja, nu ga ik bevriezen”, dacht ik en stortte in. Hier moeten we een uitweiding maken: vanwege de single-threaded aard van QEMU.js en het feit dat de oorspronkelijke QEMU niet de afwezigheid van multi-threading impliceert (dat wil zeggen, de mogelijkheid om tegelijkertijd verschillende niet-gerelateerde codepaden te gebruiken, en niet alleen “alle kernels gebruiken”) is daarvoor van cruciaal belang, de belangrijkste functies van threads moest ik “uitzetten” om van buitenaf te kunnen bellen. Dit zorgde voor een aantal natuurlijke problemen tijdens de fusie. Echter, het feit dat een deel van de veranderingen van de branche master, waarmee ik mijn code probeerde samen te voegen, waren ook in de puntrelease (en dus in mijn branch) uitgekozen en zouden waarschijnlijk ook geen extra gemak hebben opgeleverd.

Over het algemeen besloot ik dat het nog steeds zinvol is om het prototype weg te gooien, het te demonteren voor onderdelen en een geheel nieuwe versie te bouwen op basis van iets nieuws en nu van master.

Fout nummer twee: TLP-methodologie

In wezen is dit geen vergissing, maar in het algemeen is het slechts een kenmerk van het creëren van een project in omstandigheden van volledig misverstand over zowel “waar en hoe te verhuizen?” als in het algemeen “zullen we daar komen?” In deze omstandigheden onhandig programmeren was een terechte optie, maar ik wilde het uiteraard niet onnodig herhalen. Deze keer wilde ik het verstandig doen: atomaire commits, bewuste codewijzigingen (en niet “willekeurige karakters aan elkaar rijgen totdat deze compileert (met waarschuwingen)”, zoals Linus Torvalds ooit over iemand zei, volgens Wikiquote), enz.

Fout nummer drie: het water ingaan zonder de doorwaadbare plaats te kennen

Ik ben hier nog steeds niet helemaal van af, maar nu heb ik besloten om helemaal niet het pad van de minste weerstand te volgen, en het “als volwassene” te doen, namelijk mijn TCG-backend helemaal opnieuw schrijven, om niet om later te moeten zeggen: “Ja, dit gaat natuurlijk langzaam, maar ik heb niet alles onder controle – zo is TCI geschreven...” Bovendien leek dit aanvankelijk een voor de hand liggende oplossing Ik genereer binaire code. Zoals ze zeggen: “Gent verzameldу, but not that one”: de code is natuurlijk binair, maar de controle kan er niet zomaar naar worden overgedragen - hij moet expliciet in de browser worden gepusht voor compilatie, wat resulteert in een bepaald object uit de JS-wereld, dat nog moet worden ergens opgeslagen worden. Op normale RISC-architecturen is, voor zover ik het begrijp, een typische situatie echter de noodzaak om de instructiecache expliciet opnieuw in te stellen voor geregenereerde code - als dit niet is wat we nodig hebben, dan is het in ieder geval dichtbij. Bovendien heb ik bij mijn laatste poging geleerd dat de besturing niet lijkt te worden overgedragen naar het midden van het vertaalblok, dus we hebben niet echt bytecode nodig die vanuit welke offset dan ook wordt geïnterpreteerd, en we kunnen deze eenvoudig genereren vanuit de functie op TB .

Ze kwamen en schopten

Hoewel ik in juli begon met het herschrijven van de code, kroop er onopgemerkt een magische kick naar boven: meestal komen brieven van GitHub binnen als meldingen over reacties op Issues en Pull-verzoeken, maar hier: plotseling vermelden in draad Binaryen als qemu-backend in de context: "Hij deed zoiets, misschien zal hij iets zeggen." We hadden het over het gebruik van de gerelateerde bibliotheek van Emscripten binair om WASM JIT te creëren. Nou, ik zei dat je daar een Apache 2.0-licentie hebt, en QEMU als geheel wordt gedistribueerd onder GPLv2, en ze zijn niet erg compatibel. Opeens bleek dat er een vergunning kan zijn repareer het op de een of andere manier (Ik weet het niet: misschien veranderen, misschien dubbele licenties, misschien iets anders...). Dit maakte mij natuurlijk blij, want tegen die tijd had ik er al goed naar gekeken binair formaat WebAssembly, en ik was op de een of andere manier verdrietig en onbegrijpelijk. Er was ook een bibliotheek die de basisblokken met de overgangsgrafiek zou verslinden, de bytecode zou produceren en deze indien nodig zelfs in de interpreter zelf zou uitvoeren.

Toen was er meer een brief op de QEMU-mailinglijst, maar dit gaat meer over de vraag: “Wie heeft het eigenlijk nodig?” En het is plotseling, het bleek nodig te zijn. Als het min of meer snel werkt, kun je in ieder geval de volgende gebruiksmogelijkheden bij elkaar schrapen:

  • iets educatiefs lanceren zonder enige installatie
  • virtualisatie op iOS, waarbij volgens geruchten de enige applicatie die het recht heeft om on-the-fly code te genereren een JS-engine is (is dit waar?)
  • demonstratie van mini-OS - enkele floppy, ingebouwd, allerlei soorten firmware, enz...

Browser Runtime-functies

Zoals ik al zei, is QEMU gebonden aan multithreading, maar de browser heeft dit niet. Nou ja, nee... Aanvankelijk bestond het helemaal niet, toen verschenen WebWorkers - voor zover ik het begrijp is dit multithreading gebaseerd op het doorgeven van berichten zonder gedeelde variabelen. Uiteraard levert dit aanzienlijke problemen op bij het overzetten van bestaande code op basis van het gedeelde geheugenmodel. Vervolgens werd het onder publieke druk ook onder de naam geïmplementeerd SharedArrayBuffers. Het werd geleidelijk geïntroduceerd, ze vierden de lancering ervan in verschillende browsers, daarna vierden ze het nieuwe jaar, en toen Meltdown... Waarna ze tot de conclusie kwamen dat de tijdmeting grof of grof was, maar met behulp van gedeeld geheugen en een thread verhogen van de teller, het is allemaal hetzelfde het zal behoorlijk nauwkeurig werken. Daarom hebben we multithreading met gedeeld geheugen uitgeschakeld. Het lijkt erop dat ze het later weer hebben ingeschakeld, maar zoals uit het eerste experiment duidelijk werd, is er een leven zonder, en als dat zo is, zullen we proberen het te doen zonder afhankelijk te zijn van multithreading.

Het tweede kenmerk is de onmogelijkheid van manipulaties op laag niveau met de stapel: je kunt niet simpelweg de huidige context nemen, opslaan en overschakelen naar een nieuwe met een nieuwe stapel. De call-stack wordt beheerd door de virtuele JS-machine. Het lijkt erop, wat is het probleem, aangezien we nog steeds besloten hebben om de voormalige stromen volledig handmatig te beheren? Feit is dat blok-I/O in QEMU wordt geïmplementeerd via coroutines, en dit is waar stackmanipulaties op laag niveau van pas zouden kunnen komen. Gelukkig bevat Emscipten al een mechanisme voor asynchrone operaties, zelfs twee: Asynchroon и Keizer. De eerste werkt door een aanzienlijke toename van de gegenereerde JavaScript-code en wordt niet langer ondersteund. De tweede is de huidige "juiste manier" en werkt via het genereren van bytecodes voor de native tolk. Het werkt natuurlijk langzaam, maar de code wordt er niet groter van. Het is waar dat ondersteuning voor coroutines voor dit mechanisme onafhankelijk moest worden bijgedragen (er waren al coroutines geschreven voor Asyncify en er was een implementatie van ongeveer dezelfde API voor Emterpreter, je hoefde ze alleen maar te verbinden).

Op dit moment ben ik er nog niet in geslaagd de code op te splitsen in één code, gecompileerd in WASM en geïnterpreteerd met behulp van Emterpreter, dus blokapparaten werken nog niet (zie in de volgende serie, zoals ze zeggen...). Dat wil zeggen, uiteindelijk zou je zoiets als dit grappige gelaagde ding moeten krijgen:

  • geïnterpreteerde blok-I/O. Had je echt geëmuleerde NVMe met native prestaties verwacht? 🙂
  • statisch gecompileerde QEMU-hoofdcode (vertaler, andere geëmuleerde apparaten, enz.)
  • dynamisch gecompileerde gastcode in WASM

Kenmerken van QEMU-bronnen

Zoals je waarschijnlijk al geraden hebt, zijn de code voor het emuleren van gastarchitecturen en de code voor het genereren van hostmachine-instructies gescheiden in QEMU. Sterker nog, het is zelfs een beetje lastiger:

  • er zijn gastarchitecturen
  • er is versnellers, namelijk KVM voor hardwarevirtualisatie op Linux (voor gast- en hostsystemen die met elkaar compatibel zijn), TCG voor het genereren van JIT-code waar dan ook. Vanaf QEMU 2.9 verscheen ondersteuning voor de HAXM-hardwarevirtualisatiestandaard op Windows (gegevens)
  • als TCG wordt gebruikt en geen hardwarevirtualisatie, dan heeft het afzonderlijke ondersteuning voor het genereren van code voor elke hostarchitectuur, evenals voor de universele tolk
  • ... en rondom dit alles - geëmuleerde randapparatuur, gebruikersinterface, migratie, record-replay, enz.

Wist je trouwens: QEMU kan niet alleen de hele computer emuleren, maar ook de processor voor een afzonderlijk gebruikersproces in de hostkernel, dat bijvoorbeeld door de AFL-fuzzer wordt gebruikt voor binaire instrumentatie. Misschien wil iemand deze werkingsmodus van QEMU overbrengen naar JS? 😉

Zoals de meeste al lang bestaande gratis software, wordt QEMU gebouwd via de oproep configure и make. Stel dat u besluit iets toe te voegen: een TCG-backend, threadimplementatie, iets anders. Haast je niet om blij/geschokt te zijn (onderstreep wat van toepassing is) bij het vooruitzicht om met Autoconf te communiceren - sterker nog, configure QEMU's is blijkbaar zelfgeschreven en wordt nergens uit gegenereerd.

WebAssembly

Dus wat is dit ding genaamd WebAssembly (ook bekend als WASM)? Dit is een vervanging voor Asm.js en doet zich niet langer voor als geldige JavaScript-code. Integendeel, het is puur binair en geoptimaliseerd, en zelfs het eenvoudigweg schrijven van een geheel getal erin is niet erg eenvoudig: vanwege de compactheid wordt het opgeslagen in het formaat LEB128.

Je hebt misschien gehoord van het relooping-algoritme voor Asm.js - dit is het herstel van "high-level" flow control-instructies (dat wil zeggen, if-then-else, loops, enz.), waarvoor JS-engines zijn ontworpen, van de low-level LLVM IR, dichter bij de machinecode die door de processor wordt uitgevoerd. Uiteraard ligt de tussenweergave van QEMU dichter bij de tweede. Het lijkt erop dat hier het is, bytecode, het einde van de kwelling... En dan zijn er blokken, als-dan-anders en lussen!..

En dit is nog een reden waarom Binaryen nuttig is: het kan natuurlijk blokken van hoog niveau accepteren die dicht bij wat in WASM zou worden opgeslagen, liggen. Maar het kan ook code produceren uit een grafiek van basisblokken en overgangen daartussen. Welnu, ik heb al gezegd dat het het WebAssembly-opslagformaat verbergt achter de handige C/C++ API.

TCG (kleine codegenerator)

TCG was origineel backend voor de compiler C. Vervolgens kon het blijkbaar de concurrentie met GCC niet weerstaan, maar uiteindelijk vond het zijn plaats in QEMU als een mechanisme voor het genereren van code voor het hostplatform. Er is ook een TCG-backend die een abstracte bytecode genereert, die onmiddellijk door de tolk wordt uitgevoerd, maar ik besloot deze deze keer niet te gebruiken. Het feit is echter dat het in QEMU al mogelijk is om via de functie de overgang naar de gegenereerde tuberculose mogelijk te maken tcg_qemu_tb_exec, het bleek voor mij erg nuttig te zijn.

Om een ​​nieuwe TCG-backend aan QEMU toe te voegen, moet u een submap aanmaken tcg/<имя архитектуры> (in dit geval, tcg/binaryen), en het bevat twee bestanden: tcg-target.h и tcg-target.inc.c и voorschrijven het draait allemaal om configure. Je kunt daar andere bestanden plaatsen, maar zoals je uit de namen van deze twee kunt afleiden, zullen ze beide ergens worden opgenomen: één als een gewoon headerbestand (het is opgenomen in tcg/tcg.h, en die staat al in andere bestanden in de mappen tcg, accel en niet alleen), de andere - alleen als codefragment in tcg/tcg.c, maar het heeft toegang tot zijn statische functies.

Omdat ik besloot dat ik te veel tijd zou besteden aan gedetailleerd onderzoek naar hoe het werkt, kopieerde ik eenvoudigweg de “skeletten” van deze twee bestanden van een andere backend-implementatie, waarbij ik dit eerlijk aangaf in de licentiekop.

file tcg-target.h bevat voornamelijk instellingen in het formulier #define-S:

  • hoeveel registers en welke breedte zijn er op de doelarchitectuur (we hebben er zoveel als we willen, zoveel als we willen - de vraag gaat meer over wat er door de browser zal worden gegenereerd in efficiëntere code op de "volledig doel" -architectuur ...)
  • uitlijning van hostinstructies: op x86, en zelfs in TCI, zijn instructies helemaal niet uitgelijnd, maar ik ga helemaal geen instructies in de codebuffer plaatsen, maar verwijzingen naar Binaryen-bibliotheekstructuren, dus ik zal zeggen: 4 bytes
  • welke optionele instructies de backend kan genereren - we nemen alles op wat we in Binaryen vinden, laten de accelerator de rest zelf in eenvoudigere opsplitsen
  • Wat is bij benadering de grootte van de TLB-cache die door de backend wordt aangevraagd. Feit is dat in QEMU alles serieus is: hoewel er helperfuncties zijn die laden/opslaan uitvoeren, rekening houdend met de gast-MMU (waar zouden we nu zijn zonder deze?), slaan ze hun vertaalcache op in de vorm van een structuur, de waarvan de verwerking handig is om rechtstreeks in uitzendblokken in te bedden. De vraag is welke offset in deze structuur het meest efficiënt wordt verwerkt door een kleine en snelle reeks opdrachten?
  • hier kun je het doel van een of twee gereserveerde registers aanpassen, het aanroepen van TB via een functie mogelijk maken en optioneel een paar kleine beschrijven inline-functies zoals flush_icache_range (maar dit is niet ons geval)

file tcg-target.inc.c, is uiteraard meestal veel groter van formaat en bevat verschillende verplichte functies:

  • initialisatie, inclusief beperkingen op welke instructies op welke operanden kunnen werken. Door mij schaamteloos gekopieerd van een andere backend
  • functie die één interne bytecode-instructie nodig heeft
  • Je kunt hier ook hulpfuncties plaatsen, en je kunt ook statische functies gebruiken van tcg/tcg.c

Voor mezelf heb ik de volgende strategie gekozen: in de eerste woorden van het volgende vertaalblok heb ik vier pointers opgeschreven: een startteken (een bepaalde waarde in de buurt van 0xFFFFFFFF, die de huidige status van de TB bepaalde), context, gegenereerde module en magisch getal voor foutopsporing. In eerste instantie werd het merkteken geplaatst 0xFFFFFFFF - nWaar n - een klein positief getal, en elke keer dat het via de tolk werd uitgevoerd, werd het met 1 verhoogd. Toen het werd bereikt 0xFFFFFFFE, de compilatie vond plaats, de module werd opgeslagen in de functietabel, geïmporteerd in een kleine "launcher", waarin de uitvoering van tcg_qemu_tb_execen de module is verwijderd uit het QEMU-geheugen.

Om de klassiekers te parafraseren: “Crutch, hoeveel is er verweven in dit geluid voor het hart van de proger...”. Het geheugen lekte echter ergens. Bovendien was het geheugen beheerd door QEMU! Ik had een code die bij het schrijven van de volgende instructie (nou ja, dat wil zeggen een aanwijzer) degene verwijderde waarvan de link eerder op deze plaats stond, maar dit hielp niet. In het eenvoudigste geval wijst QEMU geheugen toe bij het opstarten en schrijft de gegenereerde code daar. Wanneer de buffer leeg is, wordt de code weggegooid en wordt de volgende op zijn plaats geschreven.

Nadat ik de code had bestudeerd, realiseerde ik me dat de truc met het magische getal ervoor zorgde dat ik bij de vernietiging van de heap niet faalde door bij de eerste doorgang iets verkeerds in een niet-geïnitialiseerde buffer vrij te maken. Maar wie herschrijft de buffer om mijn functie later te omzeilen? Zoals de Emscripten-ontwikkelaars adviseren, heb ik, toen ik een probleem tegenkwam, de resulterende code teruggezet naar de oorspronkelijke applicatie en Mozilla Record-Replay daarop ingesteld... Over het algemeen realiseerde ik me uiteindelijk een simpel ding: voor elk blok, A struct TranslationBlock met zijn beschrijving. Raad eens waar... Dat klopt, vlak voor het blok midden in de buffer. Toen ik dit besefte, besloot ik te stoppen met het gebruik van krukken (tenminste enkele), en gooide eenvoudigweg het magische getal weg en bracht de resterende woorden over naar struct TranslationBlock, waardoor een enkelvoudig gekoppelde lijst ontstaat die snel kan worden doorlopen wanneer de vertaalcache opnieuw wordt ingesteld, en geheugen wordt vrijgemaakt.

Sommige krukken blijven bestaan: bijvoorbeeld gemarkeerde verwijzingen in de codebuffer - sommige zijn eenvoudigweg BinaryenExpressionRef, dat wil zeggen, ze kijken naar de uitdrukkingen die lineair in het gegenereerde basisblok moeten worden geplaatst, een deel is de voorwaarde voor de overgang tussen BB's, een deel is waar naartoe te gaan. Welnu, er zijn al blokken voor Relooper voorbereid die volgens de voorwaarden moeten worden aangesloten. Om ze te onderscheiden, wordt ervan uitgegaan dat ze allemaal ten minste vier bytes uitgelijnd zijn, zodat u veilig de minst significante twee bits voor het label kunt gebruiken, u hoeft er alleen maar aan te denken deze indien nodig te verwijderen. Overigens worden dergelijke labels al in QEMU gebruikt om de reden aan te geven voor het verlaten van de TCG-lus.

Binaryen gebruiken

Modules in WebAssembly bevatten functies, die elk een body bevatten, wat een expressie is. Expressies zijn unaire en binaire bewerkingen, blokken die bestaan ​​uit lijsten met andere expressies, besturingsstromen, enz. Zoals ik al zei, is de controlestroom hier precies georganiseerd als vertakkingen op hoog niveau, lussen, functieaanroepen, enz. Argumenten voor functies worden niet op de stapel doorgegeven, maar expliciet, net als in JS. Er zijn ook globale variabelen, maar ik heb ze niet gebruikt, dus ik zal je er niets over vertellen.

Functies hebben ook lokale variabelen, genummerd vanaf nul, van het type: int32 / int64 / float / double. In dit geval zijn de eerste n lokale variabelen de argumenten die aan de functie worden doorgegeven. Houd er rekening mee dat hoewel alles hier niet helemaal op een laag niveau is in termen van controlestroom, gehele getallen nog steeds niet het attribuut "signed/unsigned" dragen: hoe het getal zich gedraagt, hangt af van de bewerkingscode.

Over het algemeen biedt Binaryen eenvoudige C-API: u maakt een module, in hem maak expressies - unair, binair, blokken van andere expressies, controlestroom, enz. Vervolgens maakt u een functie met een uitdrukking als hoofdtekst. Als je, net als ik, een overgangsgrafiek op laag niveau hebt, zal de relooper-component je helpen. Voor zover ik het begrijp, is het mogelijk om controle op hoog niveau van de uitvoeringsstroom in een blok te gebruiken, zolang deze de grenzen van het blok niet overschrijdt - dat wil zeggen, het is mogelijk om intern snel pad / langzaam te maken padvertakking binnen de ingebouwde TLB-cacheverwerkingscode, maar om de “externe” controlestroom niet te verstoren. Wanneer u een relooper vrijgeeft, worden de blokken ervan vrijgegeven; wanneer u een module vrijgeeft, verdwijnen de uitdrukkingen, functies, enz. die eraan zijn toegewezen arena.

Als u echter code direct wilt interpreteren zonder onnodig een tolkinstantie aan te maken en te verwijderen, kan het zinvol zijn om deze logica in een C++-bestand te plaatsen en van daaruit direct de gehele C++ API van de bibliotheek te beheren, waarbij u de kant-en-klare versie omzeilt. wikkels gemaakt.

Dus om de code te genereren die je nodig hebt

// настроить глобальные параметры (можно поменять потом)
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);

... als ik iets vergeten ben, sorry, dit is alleen maar om de schaal weer te geven, en de details staan ​​in de documentatie.

En nu begint de crack-fex-pex, zoiets als dit:

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);

Om op de een of andere manier de werelden van QEMU en JS met elkaar te verbinden en tegelijkertijd snel toegang te krijgen tot de gecompileerde functies, werd een array gemaakt (een tabel met functies om in het opstartprogramma te importeren) en werden de gegenereerde functies daar geplaatst. Om de index snel te berekenen, werd aanvankelijk de index van het nulwoordvertaalblok gebruikt zoals deze was, maar toen begon de index die met deze formule werd berekend eenvoudigweg in het veld te passen struct TranslationBlock.

Overigens demonstratie (momenteel met een duistere licentie) werkt alleen prima in Firefox. Chrome-ontwikkelaars waren dat wel op de een of andere manier niet klaar aan het feit dat iemand meer dan duizend exemplaren van WebAssembly-modules zou willen maken, dus wezen ze eenvoudigweg een gigabyte aan virtuele adresruimte toe voor elke...

Dat is het voor nu. Misschien komt er nog een artikel als iemand geïnteresseerd is. Er blijft namelijk tenminste nog over enkel en alleen laat blokapparaten werken. Het kan ook zinvol zijn om de compilatie van WebAssembly-modules asynchroon te maken, zoals gebruikelijk is in de JS-wereld, aangezien er nog steeds een tolk is die dit allemaal kan doen totdat de native module gereed is.

Tenslotte een raadsel: je hebt een binair bestand gecompileerd op een 32-bits architectuur, maar de code klimt, door middel van geheugenbewerkingen, vanuit Binaryen, ergens op de stapel, of ergens anders in de bovenste 2 GB van de 32-bits adresruimte. Het probleem is dat dit vanuit het oogpunt van Binaryen toegang krijgt tot een te groot resulterend adres. Hoe kun je dit omzeilen?

Op de manier van de beheerder

Ik heb dit uiteindelijk niet getest, maar mijn eerste gedachte was: "Wat als ik 32-bit Linux installeerde?" Het bovenste deel van de adresruimte wordt dan ingenomen door de kernel. De vraag is alleen hoeveel er in beslag zal worden genomen: 1 of 2 Gb.

Op de manier van een programmeur (optie voor beoefenaars)

Laten we een bel blazen bovenaan de adresruimte. Ik begrijp zelf niet waarom het daar werkt reeds er moet een stapel zijn. Maar “wij zijn beoefenaars: alles werkt voor ons, maar niemand weet waarom...”

// 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));
}

... het is waar dat het niet compatibel is met Valgrind, maar gelukkig duwt Valgrind zelf iedereen heel effectief weg :)

Misschien zal iemand een betere uitleg geven over hoe deze code van mij werkt...

Bron: www.habr.com

Voeg een reactie