Qemu.js met JIT-ondersteuning: je kunt het gehakt nog steeds achteruit draaien

Een paar jaar geleden Fabrice Bellard geschreven door jslinux is een pc-emulator geschreven in JavaScript. Daarna volgde er in ieder geval meer Virtuele x86. Maar voor zover ik weet waren ze allemaal tolken, terwijl Qemu, veel eerder geschreven door dezelfde Fabrice Bellard, en waarschijnlijk elke zichzelf respecterende moderne emulator, JIT-compilatie van gastcode in hostsysteemcode gebruikt. Het leek mij dat het tijd was om de tegenovergestelde taak te implementeren in vergelijking met de taak die browsers oplossen: JIT-compilatie van machinecode in JavaScript, waarvoor het het meest logisch leek om Qemu te porten. Het lijkt erop dat Qemu eenvoudiger en gebruiksvriendelijkere emulators heeft - dezelfde VirtualBox bijvoorbeeld - geïnstalleerd en werkt. Maar Qemu heeft verschillende interessante kenmerken

  • open source
  • mogelijkheid om te werken zonder een kerneldriver
  • mogelijkheid om in de tolkmodus te werken
  • ondersteuning voor een groot aantal host- en gastarchitecturen

Wat het derde punt betreft, kan ik nu uitleggen dat het in de TCI-modus in feite niet de instructies van de gastmachine zelf zijn die worden geïnterpreteerd, maar de bytecode die daaruit wordt verkregen, maar dit verandert niets aan de essentie - om te bouwen en uit te voeren Qemu op een nieuwe architectuur, als je geluk hebt, is een C-compiler voldoende - het schrijven van een codegenerator kan worden uitgesteld.

En nu, na twee jaar rustig sleutelen aan de Qemu-broncode in mijn vrije tijd, verscheen er een werkend prototype, waarin je bijvoorbeeld Kolibri OS al kunt draaien.

Wat is Emscripten

Tegenwoordig zijn er veel compilers verschenen, waarvan het eindresultaat JavaScript is. Sommige, zoals Type Script, waren oorspronkelijk bedoeld als de beste manier om voor internet te schrijven. Tegelijkertijd is Emscripten een manier om bestaande C- of C++-code te nemen en deze in een browserleesbare vorm te compileren. Op deze pagina We hebben veel ports van bekende programma's verzameld: hierJe kunt bijvoorbeeld naar PyPy kijken - ze beweren trouwens al over JIT te beschikken. In feite kan niet elk programma eenvoudigweg worden gecompileerd en in een browser worden uitgevoerd - er zijn er een aantal Kenmerken, waar je echter rekening mee moet houden, zoals de inscriptie op dezelfde pagina zegt: “Emscripten kunnen worden gebruikt om vrijwel elk draagbare C/C++-code naar JavaScript". Dat wil zeggen dat er een aantal bewerkingen zijn die volgens de standaard ongedefinieerd gedrag vertonen, maar meestal werken op x86 - bijvoorbeeld niet-uitgelijnde toegang tot variabelen, wat over het algemeen verboden is op sommige architecturen. , Qemu is een platformonafhankelijk programma en , wilde ik geloven, en het bevat nog niet veel ongedefinieerd gedrag - neem het en compileer het, sleutel dan een beetje aan JIT - en je bent klaar! geval...

Eerste poging

Over het algemeen ben ik niet de eerste persoon die op het idee komt om Qemu naar JavaScript te porten. Er werd op het ReactOS-forum een ​​vraag gesteld of dit mogelijk was met Emscripten. Zelfs eerder waren er geruchten dat Fabrice Bellard dit persoonlijk deed, maar we hadden het over jslinux, wat, voor zover ik weet, slechts een poging is om handmatig voldoende prestaties te bereiken in JS, en helemaal opnieuw geschreven. Later werd Virtual x86 geschreven - er werden niet-verhulde bronnen voor gepost, en, zoals gezegd, maakte het grotere "realisme" van de emulatie het mogelijk om SeaBIOS als firmware te gebruiken. Bovendien was er minstens één poging om Qemu te porten met Emscripten - ik heb dit geprobeerd socketpaar, maar de ontwikkeling was, voor zover ik het begrijp, bevroren.

Dus het lijkt erop dat hier de bronnen zijn, hier is Emscripten - neem het en compileer. Maar er zijn ook bibliotheken waarvan Qemu afhankelijk is, en bibliotheken waarvan die bibliotheken afhankelijk zijn, enz., en een daarvan is libffi, waar de glib van afhangt. Er gingen geruchten op internet dat er een was in de grote verzameling ports van bibliotheken voor Emscripten, maar het was op de een of andere manier moeilijk te geloven: ten eerste was het niet bedoeld als een nieuwe compiler, ten tweede was het een te laag niveau. bibliotheek om gewoon op te halen en te compileren naar JS. En het is niet alleen een kwestie van assemblage-inserts - als je het verdraait, kun je waarschijnlijk voor sommige aanroepconventies de nodige argumenten op de stapel genereren en de functie zonder deze aanroepen. Maar Emscripten is een lastig ding: om de gegenereerde code er bekend uit te laten zien voor de browser JS engine optimizer, worden enkele trucjes gebruikt. In het bijzonder probeert de zogenaamde relooping - een codegenerator die de ontvangen LLVM IR gebruikt met enkele abstracte overgangsinstructies, plausibele ifs, lussen, enz. te recreëren. Hoe worden de argumenten doorgegeven aan de functie? Uiteraard als argumenten voor JS-functies, dat wil zeggen, indien mogelijk, niet via de stapel.

In het begin was er een idee om eenvoudigweg een vervanging voor libffi te schrijven met JS en standaardtests uit te voeren, maar uiteindelijk raakte ik in de war over hoe ik mijn headerbestanden zo moest maken dat ze met de bestaande code zouden werken - wat kan ik doen, zoals ze zeggen: “Zijn de taken zo complex? “Zijn we zo dom?” Ik moest libffi om zo te zeggen overzetten naar een andere architectuur - gelukkig heeft Emscripten zowel macro's voor inline-assemblage (in Javascript, ja - nou ja, ongeacht de architectuur, dus de assembler), en de mogelijkheid om code uit te voeren die ter plekke is gegenereerd. Over het algemeen kreeg ik, nadat ik een tijdje aan platformafhankelijke libffi-fragmenten had gesleuteld, wat compileerbare code en voerde deze uit bij de eerste test die ik tegenkwam. Tot mijn verbazing was de test succesvol. Verbijsterd door mijn genialiteit - geen grap, het werkte vanaf de eerste lancering - ging ik, nog steeds mijn ogen niet gelovend, opnieuw naar de resulterende code kijken, om te evalueren waar ik verder moest graven. Hier werd ik voor de tweede keer gek - het enige wat mijn functie deed was ffi_call - dit meldde een succesvolle oproep. Er is zelf niet gebeld. Dus stuurde ik mijn eerste pull-verzoek, waarmee een fout in de test werd gecorrigeerd die voor elke Olympiade-student duidelijk is - echte cijfers mogen niet worden vergeleken zoals a == b en zelfs hoe a - b < EPS - je moet ook de module onthouden, anders zal 0 vrijwel gelijk blijken te zijn aan 1/3... Over het algemeen heb ik een bepaalde port van libffi bedacht, die de eenvoudigste tests doorstaat, en waarmee glib is gecompileerd - Ik besloot dat het nodig zou zijn, ik zal het later toevoegen. Vooruitkijkend zal ik zeggen dat, zoals later bleek, de compiler de libffi-functie niet eens in de uiteindelijke code had opgenomen.

Maar zoals ik al zei, er zijn enkele beperkingen, en onder het gratis gebruik van verschillende ongedefinieerde gedragingen is een onaangenamere functie verborgen: JavaScript ondersteunt standaard geen multithreading met gedeeld geheugen. In principe kan dit meestal zelfs een goed idee worden genoemd, maar niet voor het porten van code waarvan de architectuur is gekoppeld aan C-threads. Over het algemeen experimenteert Firefox met het ondersteunen van gedeelde werkers, en Emscripten heeft een pthread-implementatie voor hen, maar ik wilde er niet afhankelijk van zijn. Ik moest multithreading langzaam uit de Qemu-code verwijderen - dat wil zeggen, uitzoeken waar de threads lopen, de body van de lus die in deze thread loopt naar een aparte functie verplaatsen en dergelijke functies één voor één vanuit de hoofdlus aanroepen.

Tweede poging

Op een gegeven moment werd het duidelijk dat het probleem er nog steeds was, en dat het lukraak met krukken rond de code schuiven niet tot iets goeds zou leiden. Conclusie: we moeten op de een of andere manier het proces van het toevoegen van krukken systematiseren. Daarom werd versie 2.4.1, die toen vers was, gebruikt (niet 2.5.0, want wie weet zullen er bugs in de nieuwe versie zitten die nog niet zijn opgelost, en ik heb genoeg van mijn eigen bugs ), en het eerste was om het veilig te herschrijven thread-posix.c. Welnu, dat is net zo veilig: als iemand een bewerking probeerde uit te voeren die tot blokkering leidde, werd de functie onmiddellijk aangeroepen abort() - dit loste natuurlijk niet alle problemen in één keer op, maar het was op de een of andere manier prettiger dan stilletjes inconsistente gegevens ontvangen.

Over het algemeen zijn Emscripten-opties erg nuttig bij het porten van code naar JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - ze vangen bepaalde soorten ongedefinieerd gedrag op, zoals oproepen naar een niet-uitgelijnd adres (wat helemaal niet consistent is met de code voor getypte arrays zoals HEAP32[addr >> 2] = 1) of het aanroepen van een functie met het verkeerde aantal argumenten.

Uitlijningsfouten zijn trouwens een apart probleem. Zoals ik al zei, heeft Qemu een “gedegenereerde” interpretatieve backend voor het genereren van code TCI (kleine code-interpreter), en om Qemu op een nieuwe architectuur te bouwen en uit te voeren, is, als je geluk hebt, een C-compiler voldoende. "als je geluk hebt". Ik had pech en het bleek dat TCI niet-uitgelijnde toegang gebruikt bij het parseren van de bytecode. Dat wil zeggen, op allerlei ARM- en andere architecturen met noodzakelijkerwijs genivelleerde toegang compileert Qemu omdat ze een normale TCG-backend hebben die native code genereert, maar of TCI daarop zal werken is een andere vraag. Het bleek echter dat de TCI-documentatie duidelijk iets soortgelijks aangaf. Als gevolg hiervan werden functieaanroepen voor niet-uitgelijnd lezen aan de code toegevoegd, die in een ander deel van Qemu werden ontdekt.

Hoopvernietiging

Als gevolg hiervan werd niet-uitgelijnde toegang tot TCI gecorrigeerd en werd een hoofdlus gecreëerd die op zijn beurt de processor, RCU en enkele andere kleine dingen noemde. En dus lanceer ik Qemu met de optie -d exec,in_asm,out_asm, wat betekent dat je moet zeggen welke codeblokken worden uitgevoerd, en ook op het moment van uitzending om te schrijven wat gastcode was, welke hostcode werd (in dit geval bytecode). Het start, voert verschillende vertaalblokken uit, schrijft het foutopsporingsbericht dat ik heb achtergelaten dat RCU nu zal starten en... crasht abort() binnen een functie free(). Door te sleutelen aan de functie free() We zijn erin geslaagd om erachter te komen dat er in de header van het heap-blok, die in de acht bytes voorafgaand aan het toegewezen geheugen ligt, in plaats van de blokgrootte of iets dergelijks, rommel zat.

Vernietiging van de heap - hoe schattig... In zo'n geval is er een nuttige oplossing: van (indien mogelijk) dezelfde bronnen, een native binary samenstellen en deze uitvoeren onder Valgrind. Na enige tijd was het binaire bestand klaar. Ik start het met dezelfde opties - het crasht zelfs tijdens de initialisatie, voordat het daadwerkelijk wordt uitgevoerd. Het is natuurlijk onaangenaam - blijkbaar waren de bronnen niet precies hetzelfde, wat niet verrassend is, omdat Configure enigszins verschillende opties heeft verkend, maar ik heb Valgrind - eerst zal ik deze bug oplossen, en dan, als ik geluk heb , de originele verschijnt. Ik voer hetzelfde uit onder Valgrind... Y-y-y, y-y-y, uh-uh, het begon, ging normaal door de initialisatie en ging voorbij de oorspronkelijke bug zonder een enkele waarschuwing over onjuiste geheugentoegang, om nog maar te zwijgen van valpartijen. Het leven heeft me, zoals ze zeggen, hier niet op voorbereid: een crashend programma stopt met crashen wanneer het onder Walgrind wordt gelanceerd. Wat het was, is een mysterie. Mijn hypothese is dat gdb, eenmaal in de buurt van de huidige instructie na een crash tijdens de initialisatie, werk liet zien memset-a met een geldige aanwijzer die een van beide gebruikt mmx, of xmm registers, dan was het misschien een uitlijningsfout, hoewel het nog steeds moeilijk te geloven is.

Oké, Valgrind lijkt hier niet te helpen. En hier begon het meest walgelijke: alles lijkt zelfs te starten, maar crasht om absoluut onbekende redenen als gevolg van een gebeurtenis die miljoenen instructies geleden had kunnen plaatsvinden. Lange tijd was het niet eens duidelijk hoe we het moesten aanpakken. Uiteindelijk moest ik nog steeds gaan zitten en debuggen. Als u afdrukt waarmee de header is herschreven, bleek dat het er niet uitzag als een getal, maar eerder als een soort binaire gegevens. En zie, deze binaire string werd gevonden in het BIOS-bestand - dat wil zeggen, nu was het mogelijk om met redelijk vertrouwen te zeggen dat het een bufferoverflow was, en het is zelfs duidelijk dat het naar deze buffer werd geschreven. Nou, dan zoiets als dit - in Emscripten is er gelukkig geen randomisatie van de adresruimte, er zitten ook geen gaten in, dus je kunt ergens in het midden van de code schrijven om gegevens uit te voeren via een aanwijzer van de laatste lancering, kijk naar de gegevens, kijk naar de aanwijzer en, als deze niet is veranderd, krijg stof tot nadenken. Het is waar dat het na elke wijziging een paar minuten duurt om te linken, maar wat kunt u doen? Als resultaat werd een specifieke regel gevonden die het BIOS van de tijdelijke buffer naar het gastgeheugen kopieerde - en er was inderdaad niet genoeg ruimte in de buffer. Het vinden van de bron van dat vreemde bufferadres resulteerde in een functie qemu_anon_ram_alloc in bestand oslib-posix.c - de logica daar was deze: soms kan het handig zijn om het adres uit te lijnen op een enorme pagina van 2 MB groot, hiervoor zullen we vragen mmap eerst wat meer, en dan geven wij het eigen risico met de hulp terug munmap. En als een dergelijke uitlijning niet vereist is, geven we het resultaat aan in plaats van 2 MB getpagesize() - mmap er wordt nog steeds een uitgelijnd adres weergegeven... Dus in Emscripten mmap belt gewoon malloc, maar het wordt natuurlijk niet uitgelijnd op de pagina. Over het algemeen werd een bug die mij een paar maanden frustreerde, gecorrigeerd door een verandering in двух lijnen.

Kenmerken van belfuncties

En nu telt de processor iets, Qemu crasht niet, maar het scherm gaat niet aan en de processor gaat snel in loops, te oordelen naar de uitvoer -d exec,in_asm,out_asm. Er is een hypothese naar voren gekomen: timer-interrupts (of, in het algemeen, alle interrupts) komen niet aan. En inderdaad, als je de interrupts van de native assembly losschroeft, wat om de een of andere reden werkte, krijg je een soortgelijk beeld. Maar dit was helemaal niet het antwoord: een vergelijking van de sporen die met de bovenstaande optie zijn uitgegeven, toonde aan dat de uitvoeringstrajecten al heel vroeg uiteenliepen. Hier moet gezegd worden dat er een vergelijking is van wat er is opgenomen met behulp van de draagraket emrun het debuggen van de uitvoer met de uitvoer van de native assembly is geen volledig mechanisch proces. Ik weet niet precies hoe een programma dat in een browser draait verbinding maakt emrun, maar sommige lijnen in de uitvoer blijken opnieuw te zijn gerangschikt, dus het verschil in de diff is nog geen reden om aan te nemen dat de trajecten zijn uiteengelopen. Over het algemeen werd het duidelijk dat volgens de instructies ljmpl er is een overgang naar verschillende adressen, en de gegenereerde bytecode is fundamenteel anders: de ene bevat een instructie om een ​​helperfunctie aan te roepen, de andere niet. Na het googlen van de instructies en het bestuderen van de code die deze instructies vertaalt, werd het duidelijk dat, ten eerste, onmiddellijk ervoor in het register cr0 er is een opname gemaakt - ook met behulp van een helper - die de processor naar de beschermde modus schakelde, en ten tweede dat de js-versie nooit naar de beschermde modus schakelde. Maar feit is dat een ander kenmerk van Emscripten de onwil is om code zoals de implementatie van instructies te tolereren call in TCI, waarbij elke functieaanwijzer resulteert in type long long f(int arg0, .. int arg9) - functies moeten worden aangeroepen met het juiste aantal argumenten. Als deze regel wordt overtreden, zal het programma, afhankelijk van de foutopsporingsinstellingen, crashen (wat goed is) of de verkeerde functie aanroepen (wat jammer is om te debuggen). Er is ook een derde optie: schakel het genereren van wrappers in die argumenten toevoegen/verwijderen, maar in totaal nemen deze wrappers veel ruimte in beslag, ondanks het feit dat ik in feite maar iets meer dan honderd wrappers nodig heb. Dit alleen al is erg triest, maar er bleek een ernstiger probleem te zijn: in de gegenereerde code van de wrapper-functies werden de argumenten geconverteerd en geconverteerd, maar soms werd de functie met de gegenereerde argumenten niet aangeroepen - nou ja, net als in mijn libffi-implementatie. Dat wil zeggen, sommige helpers werden eenvoudigweg niet geëxecuteerd.

Gelukkig heeft Qemu machinaal leesbare lijsten met 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 nogal grappig gebruikt: ten eerste worden macro's op de meest bizarre manier opnieuw gedefinieerd DEF_HELPER_nen wordt vervolgens ingeschakeld helper.h. In de mate dat de macro wordt uitgebreid tot een structuurinitialisator en een komma, en vervolgens een array wordt gedefinieerd, en in plaats van elementen - #include <helper.h> Het resultaat was dat ik eindelijk de kans kreeg om de bibliotheek op het werk uit te proberen pyparsing, en er is een script geschreven dat precies die wrappers genereert voor precies de functies waarvoor ze nodig zijn.

En dus leek de processor daarna te werken. Het lijkt erop dat het scherm nooit is geïnitialiseerd, hoewel memtest86+ wel in de native assembly kon draaien. Hier is het nodig om te verduidelijken dat de I/O-code van het Qemu-blok in coroutines is geschreven. Emscripten heeft zijn eigen zeer lastige implementatie, maar het moest nog steeds worden ondersteund in de Qemu-code, en je kunt nu de processor debuggen: Qemu ondersteunt opties -kernel, -initrd, -append, waarmee je Linux of bijvoorbeeld memtest86+ kunt opstarten, zonder überhaupt gebruik te maken van block devices. Maar hier is het probleem: in de native assembly kon je de Linux-kerneluitvoer naar de console zien met de optie -nographic, en geen uitvoer van de browser naar de terminal vanwaar deze werd gestart emrun, kwam niet. Dat wil zeggen, het is niet duidelijk: de processor werkt niet of de grafische uitvoer werkt niet. En toen kwam het bij me op om even te wachten. Het bleek dat "de processor niet slaapt, maar gewoon langzaam knippert", en na ongeveer vijf minuten gooide de kernel een aantal berichten naar de console en bleef hangen. Het werd duidelijk dat de processor over het algemeen werkt, en we moeten in de code duiken om met SDL2 te werken. Helaas weet ik niet hoe ik deze bibliotheek moet gebruiken, dus op sommige plaatsen moest ik willekeurig handelen. Op een gegeven moment flitste de lijn parallel0 op het scherm op een blauwe achtergrond, wat enkele gedachten suggereerde. Uiteindelijk bleek het probleem te zijn dat Qemu meerdere virtuele vensters in één fysiek venster opent, waartussen je kunt schakelen met Ctrl-Alt-n: het werkt in de native build, maar niet in Emscripten. Na het verwijderen van onnodige vensters met behulp van opties -monitor none -parallel none -serial none en instructies om het hele scherm op elk frame krachtig opnieuw te tekenen, alles werkte plotseling.

Coroutines

Emulatie in de browser werkt dus, maar je kunt er niets interessants op single-floppy in draaien, omdat er geen blok-I/O is - je moet ondersteuning voor coroutines implementeren. Qemu heeft al verschillende coroutine-backends, maar vanwege de aard van JavaScript en de Emscripten-codegenerator kun je niet zomaar met stapels beginnen jongleren. Het lijkt erop dat “alles weg is, het pleisterwerk wordt verwijderd”, maar de Emscripten-ontwikkelaars hebben al voor alles gezorgd. Dit is best grappig geïmplementeerd: laten we een functieaanroep als deze verdacht noemen emscripten_sleep en verschillende andere die het Asyncify-mechanisme gebruiken, evenals pointer-oproepen en oproepen naar elke functie waarbij een van de vorige twee gevallen verderop in de stapel kan voorkomen. En nu zullen we vóór elke verdachte oproep een asynchrone context selecteren, en onmiddellijk na de oproep zullen we controleren of er een asynchrone oproep heeft plaatsgevonden, en als dat het geval is, zullen we alle lokale variabelen in deze asynchrone context opslaan en aangeven welke functie om de controle over te dragen naar wanneer we de uitvoering moeten voortzetten en de huidige functie moeten verlaten. Dit is waar er ruimte is om het effect te bestuderen verkwisten — voor de behoeften van het voortzetten van de uitvoering van de code na terugkeer van een asynchrone oproep, genereert de compiler “stubs” van de functie die begint na een verdachte oproep — zoals dit: als er n verdachte oproepen zijn, wordt de functie ergens n/2 uitgebreid times — dit is nog steeds, zo niet. Houd er rekening mee dat u na elke potentieel asynchrone aanroep het opslaan van enkele lokale variabelen aan de oorspronkelijke functie moet toevoegen. Vervolgens moest ik zelfs een eenvoudig script in Python schrijven, dat, gebaseerd op een gegeven reeks bijzonder veelgebruikte functies die zogenaamd “niet toelaten dat asynchronie zichzelf doorlaat” (dat wil zeggen, stapelpromotie en alles wat ik zojuist heb beschreven niet werk erin), geeft aanroepen via pointers aan waarin functies door de compiler moeten worden genegeerd, zodat deze functies niet als asynchroon worden beschouwd. En dan zijn JS-bestanden van minder dan 60 MB duidelijk te veel - laten we zeggen minstens 30. Hoewel ik, toen ik eenmaal een assemblagescript aan het opzetten was, per ongeluk de linker-opties weggooide, waaronder -O3. Ik voer de gegenereerde code uit en Chromium vreet geheugen op en crasht. Ik heb toen per ongeluk gekeken naar wat hij probeerde te downloaden... Nou, wat kan ik zeggen, ik zou ook bevroren zijn als mij was gevraagd om zorgvuldig een Javascript van meer dan 500 MB te bestuderen en te optimaliseren.

Helaas waren de controles in de Asyncify-ondersteuningsbibliotheekcode niet helemaal vriendelijk longjmp-s die worden gebruikt in de virtuele processorcode, maar na een kleine patch die deze controles uitschakelt en contexten krachtig herstelt alsof alles in orde was, werkte de code. En toen begon er iets vreemds: soms werden controles in de synchronisatiecode geactiveerd - dezelfde die de code laten crashen als deze volgens de uitvoeringslogica zou moeten worden geblokkeerd - iemand probeerde een reeds vastgelegde mutex te pakken. Gelukkig bleek dit geen logisch probleem te zijn in de geserialiseerde code - ik gebruikte eenvoudigweg de standaard hoofdlusfunctionaliteit van Emscripten, maar soms pakte de asynchrone aanroep de stapel volledig uit, en op dat moment mislukte deze. setTimeout vanuit de hoofdlus - dus ging de code de iteratie van de hoofdlus binnen zonder de vorige iteratie te verlaten. Herschreven op een oneindige lus en emscripten_sleep, en de problemen met mutexen stopten. De code is zelfs logischer geworden - ik heb tenslotte geen code die het volgende animatieframe voorbereidt - de processor berekent gewoon iets en het scherm wordt periodiek bijgewerkt. De problemen hielden echter niet op: soms eindigde de uitvoering van Qemu gewoon stil, zonder uitzonderingen of fouten. Op dat moment gaf ik het op, maar vooruitkijkend zal ik zeggen dat het probleem dit was: de coroutinecode maakt in feite geen gebruik van setTimeout (of in ieder geval niet zo vaak als je zou denken): functie emscripten_yield stelt eenvoudigweg de asynchrone oproepvlag in. Het hele punt is dat emscripten_coroutine_next is geen asynchrone functie: intern controleert het de vlag, reset deze en draagt ​​de controle over naar waar deze nodig is. Dat wil zeggen, de promotie van de stapel eindigt daar. Het probleem was dat als gevolg van use-after-free, dat verscheen toen de coroutinepool werd uitgeschakeld omdat ik geen belangrijke coderegel uit de bestaande coroutine-backend had gekopieerd, de functie qemu_in_coroutine retourneerde true, terwijl het eigenlijk false had moeten retourneren. Dit leidde tot een oproep emscripten_yield, waarboven niemand op de stapel zat emscripten_coroutine_next, de stapel ontvouwde zich helemaal naar boven, maar nee setTimeout, zoals ik al zei, werd niet tentoongesteld.

Genereren van JavaScript-code

En hier is in feite het beloofde ‘het gehakt terugdraaien’. Niet echt. Als we Qemu in de browser draaien, en Node.js daarin, dan zullen we natuurlijk na het genereren van code in Qemu volledig verkeerde JavaScript krijgen. Maar toch, een soort omgekeerde transformatie.

Eerst iets over hoe Qemu werkt. Vergeef me alsjeblieft meteen: ik ben geen professionele Qemu-ontwikkelaar en mijn conclusies kunnen op sommige plaatsen onjuist zijn. Zoals ze zeggen: “hoeft de mening van de leerling niet samen te vallen met de mening van de leraar, Peano’s axiomatiek en gezond verstand.” Qemu heeft een bepaald aantal ondersteunde gastarchitecturen en voor elk is er een map zoals target-i386. Bij het bouwen kunt u ondersteuning voor verschillende gastarchitecturen opgeven, maar het resultaat zal slechts enkele binaire bestanden zijn. De code ter ondersteuning van de gastarchitectuur genereert op zijn beurt enkele interne Qemu-bewerkingen, die de TCG (Tiny Code Generator) al omzet in machinecode voor de hostarchitectuur. Zoals vermeld in het leesmij-bestand in de tcg-directory, was dit oorspronkelijk onderdeel van een reguliere C-compiler, die later werd aangepast voor JIT. Daarom is de doelarchitectuur in de zin van dit document bijvoorbeeld niet langer een gastarchitectuur, maar een hostarchitectuur. Op een gegeven moment verscheen er een ander onderdeel: Tiny Code Interpreter (TCI), dat code zou moeten uitvoeren (bijna dezelfde interne bewerkingen) bij afwezigheid van een codegenerator voor een specifieke hostarchitectuur. Zoals in de documentatie wordt vermeld, presteert deze tolk niet altijd even goed als een JIT-codegenerator, niet alleen kwantitatief in termen van snelheid, maar ook kwalitatief. Hoewel ik er niet zeker van ben dat zijn beschrijving helemaal relevant is.

In eerste instantie probeerde ik een volwaardige TCG-backend te maken, maar raakte al snel in de war in de broncode en een niet geheel duidelijke beschrijving van de bytecode-instructies, dus besloot ik de TCI-tolk in te pakken. Dit gaf verschillende voordelen:

  • bij het implementeren van een codegenerator zou je niet naar de beschrijving van instructies kunnen kijken, maar naar de tolkcode
  • u kunt niet voor elk aangetroffen vertaalblok functies genereren, maar bijvoorbeeld pas na de honderdste uitvoering
  • als de gegenereerde code verandert (en dit lijkt mogelijk te zijn, te oordelen naar de functies met namen die het woord patch bevatten), zal ik de gegenereerde JS-code ongeldig moeten maken, maar ik zal tenminste iets hebben om het opnieuw te genereren

Wat het derde punt betreft: ik weet niet zeker of patchen mogelijk is nadat de code voor de eerste keer is uitgevoerd, maar de eerste twee punten zijn voldoende.

Aanvankelijk werd de code gegenereerd in de vorm van een grote schakelaar op het adres van de oorspronkelijke bytecode-instructie, maar toen ik me het artikel over Emscripten, optimalisatie van gegenereerde JS en relooping herinnerde, besloot ik om meer menselijke code te genereren, vooral omdat het empirisch gezien bleek dat het enige toegangspunt tot het vertaalblok de Start is. Zo gezegd, zo gedaan, na een tijdje hadden we een codegenerator die code genereerde met ifs (zij het zonder loops). Maar pech, het crashte en gaf een bericht dat de instructies een onjuiste lengte hadden. Bovendien was de laatste instructie op dit recursieniveau brcond. Oké, ik zal een identieke controle toevoegen aan het genereren van deze instructie voor en na de recursieve aanroep en... niet één ervan werd uitgevoerd, maar na de assert-switch faalden ze nog steeds. Uiteindelijk, na het bestuderen van de gegenereerde code, realiseerde ik me dat na de overstap de pointer naar de huidige instructie opnieuw van de stapel wordt geladen en waarschijnlijk wordt overschreven door de gegenereerde JavaScript-code. En zo bleek. Het vergroten van de buffer van één megabyte naar tien leidde nergens toe en het werd duidelijk dat de codegenerator in cirkels draaide. We moesten controleren of we de grenzen van de huidige TB niet overschreden, en als we dat wel deden, moesten we het adres van de volgende TB voorzien van een minteken, zodat we door konden gaan met de uitvoering. Bovendien lost dit het probleem op “welke gegenereerde functies moeten ongeldig worden gemaakt als dit stukje bytecode is gewijzigd?” — alleen de functie die overeenkomt met dit vertaalblok hoeft ongeldig te worden gemaakt. Trouwens, hoewel ik alles in Chromium heb gedebugd (aangezien ik Firefox gebruik en het voor mij gemakkelijker is om een ​​aparte browser te gebruiken voor experimenten), heeft Firefox me geholpen incompatibiliteiten met de asm.js-standaard te corrigeren, waarna de code sneller begon te werken Chroom.

Voorbeeld van 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

Het werk is dus nog steeds niet voltooid, maar ik ben het beu om deze langetermijnconstructie in het geheim tot in de puntjes te perfectioneren. Daarom heb ik besloten om te publiceren wat ik nu heb. De code is hier en daar een beetje eng, omdat dit een experiment is en het van tevoren niet duidelijk is wat er moet gebeuren. Waarschijnlijk is het dan de moeite waard om normale atomaire commits uit te voeren bovenop een modernere versie van Qemu. In de tussentijd is er een draad in de Gita in blogformaat: voor elk ‘niveau’ dat op zijn minst op de een of andere manier is behaald, is een gedetailleerd commentaar in het Russisch toegevoegd. Eigenlijk is dit artikel voor een groot deel een hervertelling van de conclusie git log.

Je kunt het allemaal proberen hier (let op het verkeer).

Wat werkt al:

  • x86 virtuele processor actief
  • Er is een werkend prototype van een JIT-codegenerator van machinecode naar JavaScript
  • Er is een sjabloon voor het samenstellen van andere 32-bits gastarchitecturen: op dit moment kun je Linux bewonderen omdat de MIPS-architectuur vastloopt in de browser tijdens het laden

Wat kan je nog meer doen

  • Versnel de emulatie. Zelfs in de JIT-modus lijkt het langzamer te werken dan Virtual x86 (maar er is mogelijk een hele Qemu met veel geëmuleerde hardware en architecturen)
  • Om een ​​normale interface te maken - eerlijk gezegd ben ik geen goede webontwikkelaar, dus voorlopig heb ik de standaard Emscripten-shell zo goed mogelijk opnieuw gemaakt
  • Probeer complexere Qemu-functies te starten - netwerken, VM-migratie, enz.
  • UPD: u zult uw weinige ontwikkelingen en bugrapporten stroomopwaarts aan Emscripten moeten voorleggen, zoals eerdere porters van Qemu en andere projecten deden. Ik dank hen dat ik hun bijdrage aan Emscripten impliciet mocht gebruiken als onderdeel van mijn taak.

Bron: www.habr.com

Voeg een reactie