Qemu.js met JIT-ondersteuning: je kunt gehakt toch achteruit draaien

Een paar jaar geleden schreef Fabrice Bellard jslinux — een pc-emulator geschreven in JavaScript. Daarna was er in elk geval nog Virtual x86. Maar voor zover ik weet, waren ze allemaal interpreters, terwijl Qemu, geschreven veel eerder door dezelfde Fabrice Bellard, en waarschijnlijk elke zelfrespecterende moderne emulator, JIT-compilatie van gastcode naar hostcode gebruikt. Het leek me tijd om de omgekeerde taak te implementeren van die welke browsers uitvoeren: JIT-compilatie van machinecode naar JavaScript, waarvoor het logisch leek om Qemu te porten. Waarom specifiek Qemu, vraag je je af? Er zijn eenvoudigere en gebruiksvriendelijkere emulators — zoals VirtualBox, bijvoorbeeld — instellen en het werkt. Maar Qemu heeft enkele interessante kenmerken

  • open source
  • de mogelijkheid om zonder kernel driver te werken
  • de mogelijkheid om in interpretermodus te werken
  • ondersteuning voor een groot aantal host- en gastarchitecturen

Over het derde punt kan ik nu al uitleggen dat eigenlijk niet de gastmachine-instructies in TCI-modus worden geïnterpreteerd, maar de bytecode die daaruit voortkomt, maar dat verandert de essentie niet — om Qemu op een nieuwe architectuur te bouwen en te draaien, heb je als je geluk hebt genoeg aan een C-compiler — het schrijven van een codegenerator kan worden uitgesteld.

En na twee jaar op mijn gemak prutsen in de Qemu-broncode is er een werkend prototype ontstaan, waarin je bijvoorbeeld Kolibri OS kunt opstarten.

Wat is Emscripten

Tegenwoordig zijn er veel compilers waarvan het eindresultaat JavaScript is. Sommige, zoals TypeScript, zijn oorspronkelijk bedacht als de beste manier om voor het web te schrijven. Ondertussen is Emscripten een manier om bestaande C- of C++-code te nemen en deze te compileren naar een formaat dat de browser begrijpt. Op deze pagina is een aantal poorten van bekende programma's verzameld: hier, bijvoorbeeld, je kunt naar PyPy kijken — overigens, zoals beweerd wordt, hebben zij al JIT. In feite kan je niet elk programma gewoon compileren en in de browser draaien — er zijn een aantal kenmerken, waar je mee moet leven, zoals de tekst op deze pagina zegt "Emscripten kan worden gebruikt om bijna elke portable C/C++ code naar JavaScript". Dit betekent dat er verschillende bewerkingen zijn die volgens de standaard onbepaald gedrag vertonen, maar meestal werken op x86 - bijvoorbeeld, niet-uitgelijnde toegang tot variabelen, die op sommige architecturen zelfs verboden is. In het algemeen is Qemu een cross-platform programma en, zo hoopt men, bevat het niet veel onbepaald gedrag - gewoon compileren, dan wat rommelen met JIT - en klaar! Maar zo simpel is het niet…

Eerste poging

Over het algemeen ben ik niet de eerste die het idee heeft om Qemu naar JavaScript te porteren. Op het ReactOS-forum werd de vraag gesteld of dit mogelijk is met behulp van Emscripten. Eerder gingen er geruchten dat dit persoonlijk was gedaan door Fabrice Bellard, maar dat ging over jslinux, dat, voor zover ik weet, een poging is om handmatig voldoende prestaties op JS te bereiken en helemaal opnieuw is geschreven. Later werd Virtual x86 geschreven - de niet-obfuscate broncode ervan werd vrijgegeven, en zoals beweerd werd, maakte de grotere 'realiteit' van de emulatie het mogelijk om SeaBIOS te gebruiken als firmware. Daarnaast was er minstens één poging om Qemu te porteren met behulp van Emscripten - dat werd geprobeerd socketpair, maar de ontwikkeling, zo ver ik begreep, werd bevroren.

Dus, het lijkt erop dat hier de bronbestanden zijn, hier is Emscripten - ga je gang en compileer. Maar er zijn ook bibliotheken waarvan Qemu afhankelijk is, en bibliotheken waarvan die bibliotheken afhankelijk zijn, enzovoort, en een daarvan is - libffi, waarvan glib afhankelijk is. Er waren geruchten op het internet dat het in een grote collectie van bibliotheekpoorten onder Emscripten zat, maar dat leek een beetje ongeloofwaardig: ten eerste zou het niet werken met de nieuwe compiler, ten tweede is het een te low-level bibliotheek om zomaar in JS te kunnen compileren. En het gaat niet alleen om assembler-instructies — waarschijnlijk, als je creatief bent, zou je voor sommige calling conventions ook zonder kunnen werken door de juiste argumenten op de stack te plaatsen en de functie aan te roepen. Maar Emscripten is een slimme tool: om ervoor te zorgen dat de gegenereerde code er vertrouwd uitziet voor de optimizer van de JS-engine in de browser, worden er een aantal trucs toegepast. In het bijzonder de zogenaamde relooping — de codegenerator probeert op basis van de verkregen LLVM IR met abstracte spronginstructies plausibele if-statements, lussen, enz. opnieuw te creëren. En hoe worden argumenten aan de functies doorgegeven? Natuurlijk, als argumenten van JS-functies, dus zoveel mogelijk niet via de stack.

Aanvankelijk dacht ik gewoon een vervanging van libffi naar JS te schrijven en de standaardtests te draaien, maar uiteindelijk raakte ik verstrikt in hoe ik mijn headerbestanden moest maken zodat ze werkten met de bestaande code — wat wil je, zoals men zegt, "Of de taken zijn zo moeilijk, of wij zijn zo dom". Ik moest libffi naar nog een architectuur porten, als je het zo kunt noemen — gelukkig, in Emscripten zijn er zowel macro's voor inline assembly (ja, in JavaScript — nou ja, welke architectuur, zo is ook de assembler), als de mogelijkheid om de on-the-fly gegenereerde code uit te voeren. In het algemeen, na enige tijd met de platformafhankelijke fragmenten van libffi te hebben geknutseld, kreeg ik een soort compileerbare code en voerde deze uit op de eerste de beste test. Tot mijn verbazing slaagde de test. Verbaasd over mijn genialiteit — het is geen grap, het werkte bij de eerste poging — kijkend naar de resulterende code om te evalueren waar ik verder moest graven, was ik een tweede keer verrast — het enige wat mijn functie deed ffi_call — was rapporteren dat de aanroep succesvol was. De aanroep zelf vond niet plaats. Zo verstuurde ik mijn eerste pull request, waarin ik een fout corrigeerde die voor elke olympiade-deelnemer begrijpelijk was in de test — reële getallen moeten niet worden vergeleken als a == b en zelfs niet als a - b < EPS — vergeet niet het module, anders blijkt 0 heel goed 1/3 te zijn... Kortom, ik heb een soort libffi-port gemaakt die de eenvoudigste tests doorstaat en waarmee glib wordt gecompileerd — ik besloot, als het nodig is, schrijf ik later verder. Vooruitlopend moet ik zeggen dat de compiler de finale code van de libffi-functie zelfs niet heeft opgenomen.

Maar, zoals ik al zei, zijn er enkele beperkingen, en onder het vrije gebruik van verschillende onduidelijke gedragingen is er een minder prettige eigenschap — JavaScript ondersteunt vanuit het ontwerp geen multithreading met gedeeld geheugen. In principe kan dit doorgaans zelfs een goed idee worden genoemd, maar niet voor het porteren van code waarvan de architectuur afhankelijk is van C-threads. Over het algemeen worden er in Firefox experimenten gedaan met de ondersteuning van gedeelde werkers, en de implementatie van pthread voor hen is aanwezig in Emscripten, maar ik wilde daar niet van afhankelijk zijn. Het was nodig om voorzichtig de multithreading uit de Qemu-code te verwijderen — dat wil zeggen, uit te zoeken waar threads worden gestart, de body van de lus die in deze thread werd uitgevoerd naar een aparte functie te verplaatsen, en deze functies om de beurt vanuit de hoofdloop aan te roepen.

De tweede poging

Op een gegeven moment werd duidelijk dat het nog steeds niets opleverde en dat het willekeurig verspreiden van krukken in de code niet tot iets goeds zou leiden. Conclusie: het moet op een of andere manier systematisch worden georganiseerd om krukken toe te voegen. Daarom heb ik de toen nog nieuwe versie 2.4.1 genomen (niet 2.5.0, want wie weet kunnen er nog niet ontdekte bugs in de nieuwe versie zitten, en ik heb al genoeg bugs van mezelf), en als eerste is deze veilig herschreven thread-posix.c. Dus zoals veilig: als iemand probeerde een operatie uit te voeren die tot blokkering leidde, werd onmiddellijk de functie abort() opgeroepen — natuurlijk loste dit niet meteen alle problemen op, maar het was tenminste iets prettiger dan stilletjes onconsistentie in gegevens te ontvangen.

Over het algemeen helpen de Emscripten-opties heel goed bij het porteren van code naar JS -s ASSERTIONS=1 -s SAFE_HEAP=1 — ze vangen sommige soorten undefined behavior, zoals toegang tot ongelijke adressen (wat helemaal niet overeenkomt met de code voor typed arrays zoals HEAP32[addr >> 2] = 1) of het aanroepen van een functie met een onjuist aantal argumenten.

Overigens, uitlijnfouten zijn een apart onderwerp. Zoals ik al zei, heeft Qemu een "degeneratieve" interpreter backend voor de TCI (tiny code interpreter) codegeneratie, en om Qemu op een nieuwe architectuur te bouwen en te draaien, is het, als je geluk hebt, voldoende om een C-compiler te hebben. Sleutelwoorden "als je geluk hebt". Ik had geen geluk, en het bleek dat TCI bij het parseren van zijn bytecode ongealigneerde toegang gebruikt. Dit betekent dat voor allerlei architecturen zoals ARM, waar gealigneerde toegang vereist is, Qemu compileert omdat er een normale TCG-backend is die native code genereert; of TCI daar ook zal werken, is nog de vraag. Echter, zoals bleek, werd iets dergelijks expliciet vermeld in de documentatie van TCI. Uiteindelijk zijn er functies toegevoegd voor ongealigneerd lezen, die in een ander deel van Qemu zijn ontdekt.

Heap-corruptie

. Uiteindelijk is de ongealigneerde toegang in TCI verholpen, is er een hoofdcyclus gemaakt die de processor, RCU en wat kleine zaken afwisselend aanroept. En nu start ik Qemu met de optie -d exec,in_asm,out_asm, wat betekent dat het moet aangeven welke codeblokken worden uitgevoerd, en tegelijkertijd tijdens de vertaling moet aangeven welke gastcode was en welke hostcode is geworden (in dit geval bytecode). Het start, voert een paar vertaalblokken uit, schrijft het debugbericht dat ik heb achtergelaten, dat de RCU nu zal starten en… crasht vanwege abort() binnen de functie free(). Door het onderzoek van de functie free() kon ik achterhalen dat in de header van het heapblok, dat zich in de acht bytes voor het toegewezen geheugen bevindt, in plaats van de blokgrootte of iets dergelijks, rommel zat.

De vernietiging van de stack - hoe schattig... In een dergelijk geval is er een nuttig hulpmiddel - verzamel indien mogelijk een native binaire versie uit dezelfde bronbestanden en draai deze onder Valgrind. Na enige tijd was de binaire versie klaar. Ik start deze op met dezelfde opties - het crasht nog steeds tijdens de initialisatie, voordat het daadwerkelijk uitgevoerd wordt. Dat is vervelend, natuurlijk - het lijkt erop dat de bronbestanden toch niet helemaal hetzelfde waren, wat niet verrassend is, want configure had verschillende opties gevonden, maar ik heb Valgrind - eerst repareer ik deze bug, en daarna, als ik geluk heb, zal de oorspronkelijke zich ook manifesteren. Ik voer hetzelfde weer uit onder Valgrind... Yeah-yeah, uhh, it started, het ging normaal door de initialisatie en ging verder zonder een enkele waarschuwing over onjuiste geheugen toegang, laat staan dat het crasht. Dit was iets waar het leven me, zoals het gezegde gaat, niet op had voorbereid - een crasher stopt met crashen wanneer deze onder Valgrind wordt uitgevoerd. Wat was dit - een mysterie. Mijn hypothese is dat als in de buurt van de huidige instructie na de crash bij de initialisatie gdb de werking toonde memset- met een geldige pointer met behulp van mmx, of xmm registers, dan was het misschien een soort uitlijningsfout, hoewel ik het niet echt geloof.

Oké, Valgrind lijkt hier geen hulp te bieden. En hier begint het vervelendste — alles lijkt op te starten, maar valt om volkomen onbekende redenen uit door een gebeurtenis die miljoenen instructies geleden heeft kunnen plaatsvinden. Een lange tijd was het zelfs onduidelijk hoe eraan te komen. Uiteindelijk moest ik gewoon gaan zitten en debuggen. Het afdrukken van de inhoud van de kop liet zien dat dit meer op een binair gegeven leek dan op een getal. En, oh wonder, deze binaire string werd gevonden in het BIOS-bestand — wat betekent dat je met voldoende zekerheid kunt zeggen dat dit een buffer overflow was, en het was zelfs duidelijk wat er in deze buffer werd geschreven. Nou, en verder, in Emscripten is geluk voorhanden: er is geen randomisatie van het adresruimte, er zijn ook geen gaten, dus we kunnen ergens in het midden van de code gegevens schrijven aan de pointer van de vorige sessie, de gegevens bekijken, de pointer bekijken, en als deze niet veranderd is, is er informatie om over na te denken. Het linken kost inderdaad een paar minuten na elke wijziging, maar ja. Uiteindelijk werd er een specifieke string gevonden die de BIOS van de tijdelijke buffer naar het gastgeheugen kopieerde — en inderdaad, er was niet genoeg ruimte in de buffer. Het zoeken naar de oorsprong van dat vreemde bufferadres leidde naar de functie qemu_anon_ram_alloc in het bestand oslib-posix.c — de logica daar was als volgt: soms kan het nuttig zijn om het adres uit te lijnen op een huge page van 2 MB, hiervoor vragen we mmap eerder iets meer aan, en dan geven we het overtollige terug met munmap. En als zo'n uitlijning niet nodig is, geven we in plaats van 2 MB het resultaat aan getpagesize() — mmap dezelfde zal hoe dan ook een uitgelijnd adres geven... Dus, in Emscripten mmap gewoon aanroepen malloc, en die, natuurlijk, niet op pagina's uitlijnt. Kortom, een bug die me een paar maanden heeft gekweld, is opgelost door wijzigingen in twee regels.

Kenmerken van functieaanroepen

En nu berekent de processor iets, Qemu crasht niet, maar het scherm gaat niet aan, en de processor loopt snel vast, gezien de output. -d exec,in_asm,out_asm. Er verscheen een hypothese: de timerinterrupts komen niet door (of helemaal geen interrupts). En inderdaad, als je de interrupts van de native build, die om de een of andere reden werkte, loskoppelt, verschijnt een vergelijkbaar beeld. Maar de oplossing bleek helemaal niet daarin te liggen: het vergelijken van de tracinggegevens, die met de bovengenoemde optie zijn verkregen, toonde aan dat de uitvoertrajecten heel vroeg uit elkaar gingen. Hier moeten we zeggen dat de vergelijking van de opname via de launcher emrun met de uitvoer van de native build - niet helemaal een mechanisch proces is. Ik weet niet precies hoe de in de browser draaiende applicatie zich verbindt met emrun, maar sommige regels in de uitvoer blijken omgewisseld te zijn, dus het verschil in de diff is nog geen reden om aan te nemen dat de trajecten uit elkaar zijn gegaan. In het algemeen werd het duidelijk dat volgens de instructie ljmpl de overgang naar verschillende adressen plaatsvindt, en ook de bytecode heel verschillend wordt gegenereerd: de ene bevat een aanroepinstructie voor een C-helperfunctie, de andere niet. Na het googlen van instructies en het bestuderen van de code die deze instructies transpileert, werd duidelijk dat, ten eerste, direct voor deze instructie een registratie in register cr0 werd uitgevoerd - ook met behulp van een helper - die de processor in beschermde modus zette, en ten tweede, dat de js-versie nooit in beschermde modus is gegaan. En het punt is dat nog een eigenschap van Emscripten de weigering is om code te tolereren, zoals de implementatie van de instructie call in TCI, die elke functiepointer naar het type brengt long long f(int arg0, .. int arg9) — functies moeten worden aangeroepen met het juiste aantal argumenten. Bij overtreding van deze regel zal de applicatie, afhankelijk van de debuginstellingen, ofwel crashen (wat goed is), ofwel helemaal niet de juiste functie aanroepen (wat treurig zal zijn om te debuggen). Er is ook een derde optie - het inschakelen van de generatie van wrappers die argumenten toevoegen/verwijderen, maar deze wrappers nemen in totaal veel ruimte in beslag, terwijl ik eigenlijk slechts iets meer dan honderd wrappers nodig heb. Dit alleen al is zeer treurig, maar er bleek een ernstiger probleem te zijn: in de gegenereerde code van wrapperfuncties werden de argumenten wel geconverteerd, maar de functie met de gegenereerde argumenten werd soms niet aangeroepen - net als in mijn implementatie van libffi. Dus sommige helpers werden gewoon niet uitgevoerd.

Gelukkig heeft Qemu machine-leesbare lijsten van helpers in de vorm van een headerbestand zoals

DEF_HELPER_0(lock, void)
DEF_HELPER_0(unlock, void)
DEF_HELPER_3(write_eflags, void, env, tl, i32)

Ze worden op een vrij grappige manier gebruikt: eerst worden de macro's op de meest bizarre manier overschreven DEF_HELPER_n, en dan wordt helper.h. Tot en met het punt dat de macro wordt onthuld in de initialisator van de structuur en een komma, en dan wordt een array gedefinieerd, en in plaats van elementen — #include <helper.h> Als resultaat kwam eindelijk de gelegenheid om de bibliotheek pyparsing, en er werd een script geschreven dat wrappers genereert precies voor die en alleen voor die functies waarvoor het nodig is.

En kijk, na dit lijkt het alsof de processor eindelijk werkte. Bijna, want het scherm werd nog steeds niet geïnitialiseerd, hoewel het in de native build mogelijk was om memtest86+ te starten. Hier moet worden verduidelijkt dat de code voor blokinvoer/uitvoer van Qemu is geschreven op coroutines. In Emscripten is er een vrij gecompliceerde implementatie, maar die moet nog ondersteund worden in de code van Qemu, en de processor kan al nu worden gedebugd: Qemu ondersteunt de opties -kernel, -initrd, -append, waarmee je Linux of bijvoorbeeld memtest86+ kunt laden zonder blokapparaten te gebruiken. Maar helaas: in de native build was er uitvoer van de Linux-kernel naar de console met de optie -nographic, terwijl er vanuit de browser geen uitvoer in de terminal kwam, van waaruit het was gestart emrun, wat niet duidelijk maakte: werkt de processor niet of de grafische uitvoer. En toen kwam het in me op om even te wachten. Het bleek dat "de processor niet slaapt, maar gewoon langzaam knippert", en na vijf minuten gooide de kernel een reeks berichten op de console en ging verder met vastlopen. Het werd duidelijk dat de processor over het algemeen werkt, en dat we moeten graven in de SDL2-code. Deze bibliotheek gebruiken kan ik helaas niet, dus moest ik soms op de gok handelen. Op een gegeven moment flitste de regel parallel0 op een blauwe achtergrond, wat me deed denken aan iets. Uiteindelijk bleek dat Qemu meerdere virtuele vensters opent in één fysiek venster, waar tussen kan worden gewisseld met Ctrl-Alt-n: in de native build werkt het, in Emscripten niet. Na het verwijderen van de overbodige vensters met de opties -monitor none -parallel none -serial none en het dwingen van een volledige hertekening van het scherm bij elk frame, begon alles ineens te werken.

Coroutines

De browseremulatie werkt, maar er is niets interessants met een enkele schijf te starten, omdat er geen blok-input-output is – ondersteuning voor coroutines moet worden geïmplementeerd. In Qemu zijn er al verschillende coroutine backends, maar vanwege de kenmerken van JavaScript en de Emscripten-codegenerator is het niet simpel om gewoon met stacks te jongleren. Het lijkt misschien alsof 'alles verloren is, het gips wordt verwijderd', maar de ontwikkelaars van Emscripten hebben al voor alles gezorgd. Dit is op een vrij grappige manier geïmplementeerd: laten we een verdachte functie-aanroep zoals emscripten_sleep en nog een paar andere, die gebruikmaken van het Asyncify-mechanisme, evenals aanroepen via pointers en aanroepen van elke functie waarbij verderop in de stack een van de eerdere twee gevallen kan optreden. En nu, voor elke verdachte aanroep, reserveren we een async context, en direct na de aanroep controleren we of er een asynchrone aanroep heeft plaatsgevonden, en als dat het geval is, bewaren we alle lokale variabelen in deze async context, geven we aan welke functie doorgang moet krijgen wanneer het nodig is om de uitvoering voort te zetten, en verlaten we de huidige functie. Hier ligt een enorme ruimte voor het bestuderen van het effect van verpulvering – voor de behoeften van het voortzetten van de code-uitvoering na een terugkeer van een asynchrone aanroep genereert de compiler 'stubs' van functies die beginnen na de verdachte aanroep – zo: als er n verdachte aanroepen zijn, dan wordt de functie ergens in n/2 verminderd – dit is nog meer het geval, als men niet rekening houdt met het feit dat er na elke potentieel asynchrone aanroep een deel van de lokale variabelen moet worden opgeslagen in de oorspronkelijke functie. Uiteindelijk moest ik zelfs een eenvoudige script in Python schrijven, die, op basis van een opgegeven verzameling bijzonder verpulverde functies, die vermoedelijk 'geen asynchroniteit doorlaten' (dat wil zeggen, waarin de stack niet wordt gedraaid en alles wat ik net heb beschreven niet gebeurt), aangeeft welke aanroepen via pointers in welke functies door de compiler moeten worden genegeerd, zodat deze functies niet als asynchroon worden beschouwd. Want JS-bestanden van bijna 60 MB – dat is echt te veel – laten we het dan tenminste naar 30 terugbrengen. Hoewel, ooit heb ik een build-script ingesteld en per ongeluk de linkeropties weggegooid, waaronder ook -O3. Ik start de gegenereerde code en Chromium slurpt geheugen en crasht. Ik keek later toevallig naar wat het probeerde te laden... Nou, wat kan ik zeggen, ik zou ook vastlopen als ik de opdracht kreeg om JavaScript van 500+ MB grondig te analyseren en te optimaliseren.

Helaas werkte de controle in de Asyncify-ondersteuningsbibliotheek niet goed samen met longjmp-s die worden gebruikt in de virtual processor code, maar na een kleine patch die deze controles uitschakelde en de contexten geforceerd herstelde alsof alles goed was, werkte de code. Toen begon het vreemde: soms werden de controles in de synchronisatiecode geactiveerd - die controles die de code dwingen om te stoppen als het volgens de logica vast zou moeten lopen - iemand probeerde een al geclaimde mutex te blokkeren. Gelukkig was dit geen logisch probleem in de geserialiseerde code - ik gebruikte gewoon de standaard functionaliteit van de main loop die door Emscripten wordt geleverd, maar soms maakte een asynchrone oproep de stack volledig vrij, en op dat moment wordt setTimeout van de main loop geactiveerd - op die manier ging de code de iteratie van de hoofdcyclus in, zonder de vorige iteratie te verlaten. Ik herschreef het naar een oneindige lus en emscripten_sleep, en de problemen met de mutexen hielden op. De code werd zelfs logischer - immers, in feite heb ik geen specifieke code die het volgende animatieframe voorbereidt - de processor rekent gewoon iets uit en het scherm wordt periodiek bijgewerkt. Echter, de problemen hielden daar niet op: soms stopte Qemu gewoon stilletjes zonder ook maar enige uitzonderingen of fouten. Op dat moment negeerde ik het, maar vooruitkijkend kan ik zeggen dat het probleem hierin zat: de coroutine code gebruikt in feite helemaal geen setTimeout (tenzij, althans, niet zo vaak als je zou denken): de functie emscripten_yield stelt gewoon een vlag voor een asynchrone oproep in. De essentie is dat emscripten_coroutine_next geen asynchrone functie is: deze controleert de vlag, reset deze en geeft de controle daar waar nodig door. Dit betekent dat de unwrap van de stack daar eindigt. Het probleem was dat door een use-after-free, dat zich voordeed wanneer de coroutine pool was uitgeschakeld omdat ik een belangrijke regel code uit de bestaande coroutine backend niet had gekopieerd, de functie qemu_in_coroutine terugkeerde true terwijl deze in werkelijkheid false had moeten retourneren. Dit leidde tot een oproep emscripten_yield, boven welke het stack niet verder ging emscripten_coroutine_next, de stack liep tot de top, maar er waren geen setTimeout, zoals ik al zei, werd niet ingesteld.

JavaScript-codegeneratie

En hier is dan de beloofde "teruggang van het gehakt". Eigenlijk niet. Natuurlijk, als je Qemu in de browser draait, en daarin Node.js, dan krijgen we na de codegeneratie in Qemu absoluut niet dezelfde JavaScript. Maar toch, enigszins, een soort omgekeerde transformatie.

Laten we beginnen met een beetje uitleg over hoe Qemu werkt. Vergeef me dat ik het zeg: ik ben geen professionele Qemu-ontwikkelaar en mijn conclusies kunnen op sommige punten verkeerd zijn. Zoals men zegt, "de mening van een student hoeft niet overeen te stemmen met die van de docent, de axioma's van Peano en gezond verstand". Qemu heeft een aantal ondersteunde gastarchitecturen en voor elke architectuur is er een catalogus zoals target-i386. Bij de compilatie kan ondersteuning voor meerdere gastarchitecturen worden opgegeven, maar dit resulteert eenvoudigweg in verschillende binaire bestanden. De code voor de ondersteuning van de gastarchitectuur genereert op zijn beurt een aantal interne operaties van Qemu, die TCG (Tiny Code Generator) vervolgens omzet in machinecode voor de hostarchitectuur. Zoals vermeld in het readme-bestand in de tcg-catalogus, was dit oorspronkelijk een onderdeel van een gewone C-compiler, die later is aangepast voor JIT. Daarom is de doelaarchitectuur in termen van dit document niet langer de gastarchitectuur, maar de hostarchitectuur. Op een gegeven moment is er ook nog een component bijgekomen - de Tiny Code Interpreter (TCI), die de code (praktisch dezelfde interne operaties) moet uitvoeren in de afwezigheid van een codegenerator voor de specifieke hostarchitectuur. In feite, zoals in de documentatie staat, kan deze interpreter niet altijd zo goed presteren als de JIT-codegenerator, niet alleen in kwantitatieve zin qua snelheid, maar ook in kwalitatieve zin. Hoewel ik niet zeker weet of de beschrijving nog steeds helemaal actueel is.

In het begin probeerde ik een volwaardige TCG-backend te maken, maar ik raakte snel verward in de bronbestanden en de niet helemaal duidelijke beschrijving van de bytecode-instructies, dus besloot ik de TCI-interpreter te wikkelen. Dit bood verschillende voordelen:

  • bij de implementatie van de codegenerator kon ik niet naar de beschrijving van de instructies kijken, maar naar de code van de interpreter.
  • Je kunt functies genereren niet voor elk blok van transformatie dat je tegenkomt, maar bijvoorbeeld alleen na de honderdste uitvoering.
  • In het geval van wijzigingen in de gegenereerde code (wat blijkbaar mogelijk is, gezien de functies met de naam patch), moet ik de gegenereerde JS-code ongeldig maken, maar heb ik in ieder geval iets om het opnieuw te genereren.

Over het derde punt ben ik niet zeker of patching mogelijk is nadat de code voor de eerste keer is uitgevoerd, maar de eerste twee punten zijn al voldoende.

Oorspronkelijk werd de code gegenereerd in de vorm van een grote switch op het adres van de oorspronkelijke bytecode-instructie, maar later, herinnerend aan een artikel over Emscripten, de optimalisatie van de gegenereerde JS en reloop, besloot ik meer menselijke code te genereren, vooral omdat empirisch bleek dat het enige toegangspunt naar het transformatiebblok zijn begin was. Zeggen is doen, na enige tijd had ik een codegenerator die code genereerde met if-statements (hoewel zonder lussen). Maar er was een probleem, hij viel en gaf een foutmelding dat de instructie een verkeerde lengte had. Hierbij was de laatste instructie op dit niveau van recursie brcond. Goed, ik zal een identieke controle toevoegen in de generatie van deze instructie voor en na de recursieve aanroep en… geen van beiden werd uitgevoerd, maar na de switch op assert faalden we toch. Uiteindelijk, na het bestuderen van de gegenereerde code, realiseerde ik me dat de pointer naar de huidige instructie na de switch wordt vernieuwd vanuit de stack en waarschijnlijk wordt overschreven door de gegenereerde JavaScript-code. En dat bleek inderdaad zo te zijn. Het vergroten van de buffer van één megabyte naar tien leidde tot niets, en het werd duidelijk dat de codegenerator in een lus draaide. Ik moest controleren of we de grenzen van het huidige TB niet hadden overschreden en, als we dat deden, het adres van het volgende TB met een minteken geven, zodat de uitvoering kon worden voortgezet. Bovendien lost dit het probleem op "welke gegenereerde functies moeten ongeldig worden gemaakt als dit stukje bytecode verandert?" — alleen de functie die overeenkomt met dit transformatieblok moet ongeldig worden gemaakt. Trouwens, hoewel ik alles debugde in Chromium (aangezien ik Firefox gebruik en het voor mij eenvoudiger is om een aparte browser voor experimenten te gebruiken), hielp Firefox me om incompatibiliteiten met de asm.js-standaard op te lossen, waarna de code sneller werkte in Chromium.

Voorbeeld van de gegenereerde code

Compiling 0x15b46d0:
CompiledTB[0x015b46d0] = function(stdlib, ffi, heap) {
"use asm";
var HEAP8 = new stdlib.Int8Array(heap);
var HEAP16 = new stdlib.Int16Array(heap);
var HEAP32 = new stdlib.Int32Array(heap);
var HEAPU8 = new stdlib.Uint8Array(heap);
var HEAPU16 = new stdlib.Uint16Array(heap);
var HEAPU32 = new stdlib.Uint32Array(heap);

var dynCall_iiiiiiiiiii = ffi.dynCall_iiiiiiiiiii;
var getTempRet0 = ffi.getTempRet0;
var badAlignment = ffi.badAlignment;
var _i64Add = ffi._i64Add;
var _i64Subtract = ffi._i64Subtract;
var Math_imul = ffi.Math_imul;
var _mul_unsigned_long_long = ffi._mul_unsigned_long_long;
var execute_if_compiled = ffi.execute_if_compiled;
var getThrew = ffi.getThrew;
var abort = ffi.abort;
var qemu_ld_ub = ffi.qemu_ld_ub;
var qemu_ld_leuw = ffi.qemu_ld_leuw;
var qemu_ld_leul = ffi.qemu_ld_leul;
var qemu_ld_beuw = ffi.qemu_ld_beuw;
var qemu_ld_beul = ffi.qemu_ld_beul;
var qemu_ld_beq = ffi.qemu_ld_beq;
var qemu_ld_leq = ffi.qemu_ld_leq;
var qemu_st_b = ffi.qemu_st_b;
var qemu_st_lew = ffi.qemu_st_lew;
var qemu_st_lel = ffi.qemu_st_lel;
var qemu_st_bew = ffi.qemu_st_bew;
var qemu_st_bel = ffi.qemu_st_bel;
var qemu_st_leq = ffi.qemu_st_leq;
var qemu_st_beq = ffi.qemu_st_beq;

function tb_fun(tb_ptr, env, sp_value, depth) {
  tb_ptr = tb_ptr|0;
  env = env|0;
  sp_value = sp_value|0;
  depth = depth|0;
  var u0 = 0, u1 = 0, u2 = 0, u3 = 0, result = 0;
  var r0 = 0, r1 = 0, r2 = 0, r3 = 0, r4 = 0, r5 = 0, r6 = 0, r7 = 0, r8 = 0, r9 = 0;
  var r10 = 0, r11 = 0, r12 = 0, r13 = 0, r14 = 0, r15 = 0, r16 = 0, r17 = 0, r18 = 0, r19 = 0;
  var r20 = 0, r21 = 0, r22 = 0, r23 = 0, r24 = 0, r25 = 0, r26 = 0, r27 = 0, r28 = 0, r29 = 0;
  var r30 = 0, r31 = 0, r41 = 0, r42 = 0, r43 = 0, r44 = 0;
    r14 = env|0;
    r15 = sp_value|0;
  START: do {
    r0 = HEAPU32[((r14 + (-4))|0) >> 2] | 0;
    r42 = 0;
    result = ((r0|0) != (r42|0))|0;
    HEAPU32[1445307] = r0;
    HEAPU32[1445321] = r14;
    if(result|0) {
    HEAPU32[1445322] = r15;
    return 0x0345bf93|0;
    }
    r0 = HEAPU32[((r14 + (16))|0) >> 2] | 0;
    r42 = 8;
    r0 = ((r0|0) - (r42|0))|0;
    HEAPU32[(r14 + (16)) >> 2] = r0;
    r1 = 8;
    HEAPU32[(r14 + (44)) >> 2] = r1;
    r1 = r0|0;
    HEAPU32[(r14 + (40)) >> 2] = r1;
    r42 = 4;
    r0 = ((r0|0) + (r42|0))|0;
    r2 = HEAPU32[((r14 + (24))|0) >> 2] | 0;
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    HEAPU32[1445309] = r2;
    HEAPU32[1445321] = r14;
    HEAPU32[1445322] = r15;
    qemu_st_lel(env|0, r0|0, r2|0, 34, 22759218);
if(getThrew() | 0) abort();
    r0 = 3241038392;
    HEAPU32[1445307] = r0;
    r0 = qemu_ld_leul(env|0, r0|0, 34, 22759233)|0;
if(getThrew() | 0) abort();
    HEAPU32[(r14 + (24)) >> 2] = r0;
    r1 = HEAPU32[((r14 + (12))|0) >> 2] | 0;
    r2 = HEAPU32[((r14 + (40))|0) >> 2] | 0;
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    HEAPU32[1445309] = r2;
    qemu_st_lel(env|0, r2|0, r1|0, 34, 22759265);
if(getThrew() | 0) abort();
    r0 = HEAPU32[((r14 + (24))|0) >> 2] | 0;
    HEAPU32[(r14 + (40)) >> 2] = r0;
    r1 = 24;
    HEAPU32[(r14 + (52)) >> 2] = r1;
    r42 = 0;
    result = ((r0|0) == (r42|0))|0;
    if(result|0) {
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    }
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    return execute_if_compiled(22759392|0, env|0, sp_value|0, depth|0) | 0;
    return execute_if_compiled(23164080|0, env|0, sp_value|0, depth|0) | 0;
    break;
  } while(1); abort(); return 0|0;
}
return {tb_fun: tb_fun};
}(window, CompilerFFI, Module.buffer)["tb_fun"]

Conclusie

Dus, het werk is nog niet af, maar het was geheimzinnig en het perfectioneren van dit langlopende project begon me te vervelen. Daarom heb ik besloten om voorlopig te publiceren wat er is. De code is hier en daar angstaanjagend, omdat dit een experiment is en het vooraf niet duidelijk is wat er moet gebeuren. Misschien moet ik later normale atomische commits maken bovenop een modernere versie van Qemu. Voorlopig is er een tak in git in blogformaat: voor elk "niveau" dat ik ten minste enigszins heb bereikt, is er een uitgebreide opmerking in het Russisch toegevoegd. Eigenlijk is dit artikel in grote mate een samenvatting van de output. git log.

Je kunt dit allemaal uitproberen hier (voorzichtig, verkeer).

Wat nu al werkt:

  • Een virtuele x86-processor werkt
  • Er is een werkend prototype van de JIT-codegenerator van machinecode naar JavaScript
  • Er is een basis voor het samenstellen van andere 32-bits gastarchitecturen: je kunt nu kijken naar de vastlopende Linux voor de MIPS-architectuur in je browser tijdens het opstarten

Wat je nog meer kunt doen

  • De emulatie versnellen. Zelfs in JIT-modus lijkt het langzamer te werken dan Virtual x86 (maar er is in potentie wel een hele Qemu met veel meer emuleerbare hardware en architecturen)
  • Een normale interface maken - ik ben geen geweldige webontwikkelaar, dus heb ik de standaard Emscripten-shell aangepast zoals ik kon
  • Probeer meer geavanceerde Qemu-functies uit te voeren - netwerk, VM-migratie, enz.
  • UPD: Ik zal mijn weinig onderzoek en bugrapporten aan Emscripten moeten geven, zoals eerdere porters van Qemu en andere projecten hebben gedaan. Dank aan hen voor de mogelijkheid om stilzwijgend gebruik te maken van hun bijdrage aan Emscripten in het kader van mijn opdracht.

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster