For flere år siden skrev Fabrice Bellard — en emulator til PC, skrevet i JavaScript. Derefter kom der endnu mindst . Men i modsætning til disse, så vidt jeg ved, var de alle fortolkere, mens Qemu, som også blev skrevet betydeligt tidligere af Fabrice Bellard, og som sikkert enhver respektabel moderne emulator, bruger JIT-kompilering af gæstekode til værts-systemkode. Jeg mente, at nu var det tid til at realisere den omvendte opgave af den, som browsere løser: JIT-kompilering af maskinkode til JavaScript, hvorfor det mest logiske ville være at portere Qemu. Man kunne spørge, hvorfor netop Qemu, der findes mere enkle og brugervenlige emulatorer — for eksempel VirtualBox — installer det, og så fungerer det. Men Qemu har nogle interessante funktioner
- åbne kildekoder
- mulighed for at arbejde uden kerne-driver
- mulighed for at arbejde i interpretator-tilstand
- support for en bred vifte af både værts- og gæstearkitekturer
Om det tredje punkt kan jeg nu forklare, at i TCI-tilstand fortolkes der faktisk ikke gæsternes maskininstruktioner, men den bytekode, der genereres af dem, men det ændrer ikke essensen — for at samle og starte Qemu på en ny arkitektur, hvis man er heldig, er det nok med en C-kompilator — skrivningen af en kodegenerator kan udsættes.
Og efter to års langsom skraben i fritiden med Qemu's kildekode kom der en fungerende prototype, hvor man allerede kan starte for eksempel Kolibri OS.
Hvad er Emscripten
I vores tid er der opstået mange kompilatorer, hvis endelige output er JavaScript. Nogle, som TypeScript, blev oprindeligt designet som den bedste måde at skrive til weben. Samtidig er Emscripten en måde at tage eksisterende kode i C eller C++ og kompilere den til et format, der er forståeligt for browseren. På er der lavet mange porteringer af kendte programmer: , for eksempel kan man se på PyPy — som påstås at have JIT. Faktisk er der ikke alle programmer, der simpelthen kan kompilere og køre i browseren — der er en række , som man må leve med, men som det står på denne side "Emscripten can be used to compile almost any til fælles anvendelse af et fælles katalog med automatisk generering af loginoplysninger til tilslutning, annonceret via multicast DNS. C/C++ kode til JavaScript. Der er en række operationer, der er udefineret adfærd ifølge standarden, men som normalt fungerer på x86 - for eksempel, misaligned adgang til variabler, som på nogle arkitekturer slet ikke er tilladt. Generelt er Qemu et tværplatformsprogram, og man kunne håbe, at det ikke indeholder meget udefineret adfærd - tag det og kompiler det, så skal man bare arbejde lidt med JIT - og så er det klart! Men sådan skulle det ikke være...
Første forsøg
Generelt set er jeg ikke den første, der har fået ideen om at portere Qemu til JavaScript. På ReactOS-forummet blev spørgsmålet rejst, om det kunne lade sig gøre ved hjælp af Emscripten. Endnu tidligere gik der rygter om, at Fabrice Bellard personligt havde gjort dette, men det drejede sig om jslinux, som efter hvad jeg ved, er et forsøg på manuelt at opnå tilstrækkelig ydeevne på JS, og det er skrevet fra bunden. Senere blev Virtual x86 skrevet - kildekoden blev offentliggjort uden obfuskering, og det blev hævdet, at den store "realisme" i emuleringen gjorde det muligt at bruge SeaBIOS som firmware. Derudover var der mindst et forsøg på at portere Qemu ved hjælp af Emscripten - det blev forsøgt... , men udviklingen, som jeg har forstået det, blev sat på pause.
Så det skulle være enkelt, her er koden, her er Emscripten - tag det og kompiler. Men der er også biblioteker, som Qemu afhænger af, samt de biblioteker, som disse biblioteker afhænger af osv., hvoraf et af dem - , som afhænger af glib. På internettet var der rygter om, at den store samling af biblioteksporte til Emscripten også indeholdt den, men det var svært at tro: for det første kunne den ikke kompileres med den nye compiler, for det andet er det et alt for lavt niveau bibliotek til bare at blive taget og kompileret til JS. Og det handler ikke kun om assemblerindsatser — måske, hvis man virkelig prøver, kan man for nogle calling conventions danne de nødvendige argumenter på stakken og kalde funktionen uden dem. Men Emscripten er klogt konstrueret: for at den genererede kode skal se bekendt ud for browserens JS-motoroptimering, bruges der nogle tricks. Især såkaldt relooping — kodegeneratoren forsøger at genskabe troværdige if-sætninger, loops osv. fra den modtagne LLVM IR med nogle abstrakte kontrolinstruktioner. Og hvordan bliver argumenterne til funktionen givet? Selvfølgelig som argumenter i JS-funktioner, det vil sige, i det omfang det er muligt, ikke via stakken.
I starten var tanken faktisk bare at skrive en erstatning til libffi i JS og køre de standard tests, men til sidst blev jeg forvirret over, hvordan jeg skulle lave mine header-filer, så de kunne arbejde med den eksisterende kode — hvad kan man sige, "Enten så er opgaverne så svære, eller også er vi så dumme". Jeg måtte portere libffi til endnu en arkitektur, hvis man kan sige det sådan — heldigvis har Emscripten både makroer til inline assembly (i JavaScript, ja — nå, hvilken arkitektur, sådan er assembleren) og muligheden for at køre den genererede kode i realtid. Kort sagt, efter at have rodet lidt med de plattformspecifikke fragmenter af libffi, fik jeg en slags kompilerbar kode, og jeg kørte den på den første test, jeg fandt. Til min overraskelse bestod testen. Stolt af min egen genialitet — for sjov, det virkede fra første gang — kiggede jeg igen på den resulterende kode for at vurdere, hvor jeg skulle grave videre. Her blev jeg overrasket igen — det eneste, min funktion gjorde ffi_call — var at rapportere om et succesfuldt kald. Selve kaldet fandtes ikke. Så jeg sendte min første pull request, der rettede en fejl, som enhver olympiade deltager ville forstå — reelle tal bør ikke sammenlignes som a == b og ikke engang som a - b < EPS — husk også at modulet ikke bliver glemt, ellers er 0 faktisk lig med 1/3… Generelt har jeg lavet en slags libffi-port, som klarer de enkleste tests, og som kan kompileres med glib — jeg besluttede, at jeg senere vil tilføje mere. For at komme til pointen, så fandt jeg ud af, at kompilatoren overhovedet ikke har inkluderet den endelige kode for libffi-funktionen.
Men som jeg allerede har sagt, er der nogle begrænsninger, og blandt det frie brug af forskelligt udefineret adfærd er der en uheldig egenskab — JavaScript understøtter ikke multithreading med delt hukommelse af design. Grundlæggende kan man endda kalde det en god idé, men ikke for porteringen af kode, hvis arkitektur er bundet til C-tråde. Generelt er der eksperimenter i gang med at støtte delte arbejdere i Firefox, og implementeringen af pthread er tilgængelig i Emscripten, men jeg ville ikke afhænge af det. Jeg måtte langsomt fjerne multithreading fra Qemu-koden — det vil sige finde steder, hvor tråde oprettes, flytte kroppen af den løkke, der kører i den tråd, til en separat funktion og kalde sådanne funktioner sekventielt fra hovedloopet.
Andet forsøg
På et tidspunkt blev det klart, at situationen stadig er den samme, og at det kaotiske placering af midlertidige løsninger i koden ikke ville føre til noget godt. Konklusion: processen med at tilføje midlertidige løsninger skal systematiseres på en eller anden måde. Derfor blev den seneste version 2.4.1 (ikke 2.5.0, fordi, hvem ved, om der er uudfasede fejl i den nye version, og jeg har allerede nok af mine egne fejl) taget, og det første, der blev gjort, var at skrive det om på en sikker måde. thread-posix.c. Så godt som muligt: hvis nogen forsøgte at udføre en operation, der ville føre til en blokering, blev funktionen abort() — selvfølgelig løste det ikke alle problemer med det samme, men i det mindste er det bedre end stille at opleve inkonsistens i dataene.
Generelt hjælper Emscripten-opsætninger mig meget i kodeportering til JS. -s ASSERTIONS=1 -s SAFE_HEAP=1 — de fanger nogle former for udefineret adfærd som accessing ved en ikke-aligneret adresse (hvilket slet ikke stemmer overens med koden for typed arrays som HEAP32[addr >> 2] = 1) eller opkald af en funktion med forkert antal argumenter.
Forresten, justering af fejl er et særskilt emne. Som jeg allerede har nævnt, har Qemu en "degenereret" fortolkende backend til TCI (tiny code interpreter), og for at samle og køre Qemu på en ny arkitektur, hvis man er heldig, er en C-kompilator ofte nok. Nøgleord "hvis man er heldig". Jeg var ikke heldig, og det viste sig, at TCI ved tolkning af sin bytekode bruger ikke-justeret adgang. Det vil sige, at på forskellige arkitekturer som ARM og andre, hvor justeret adgang er påkrævet, kompileres Qemu, fordi der findes en ordentlig TCG-backend, der genererer native kode, men om TCI vil fungere på dem — det er endnu et spørgsmål. Det viste sig imidlertid, at noget lignende tydeligt blev angivet i dokumentationen til TCI. I sidste ende blev der tilføjet funktionskald til ikke-justeret læsning i koden, som blev opdaget i en anden del af Qemu.
Brok af bunken
Til sidst blev den ikke-justerede adgang i TCI rettet, og der blev lavet en hovedløkke, der sekventielt kaldte processoren, RCU og lidt andet. Og så kører jeg Qemu med indstillingen -d exec,in_asm,out_asm, hvilket betyder, at man skal angive, hvilke kodeblokke der udføres, og på tidspunktet for transmissionen skrive, hvilken gæstekode der var, og hvilken vært kode der er blevet (i dette tilfælde bytekode). Det starter, udfører flere blokke af transmission, skriver den fejlfindingsbesked, jeg efterlod, at RCU nu vil starte, og… det falder i abort() indeni funktionen free(). Ved at pille i funktionen free() blev det opdaget, at i headeren af bunken, som ligger i de otte byte, der går forud for den tildelte hukommelse, var der skrald i stedet for størrelsen af blokken eller noget lignende.
At destruere bunken — hvordan sødt... I sådanne tilfælde er der et nyttigt værktøj — at samle en native binær fra de (hvis muligt) samme kilder og køre den under Valgrind. Efter noget tid var den binære klar. Jeg kører med de samme indstillinger — den falder igen under initialisering, uden at nå til selve udførelsen. Ubehageligt, selvfølgelig — tydeligvis var kilderne ikke helt de samme, hvilket ikke er overraskende, da configure fandt nogle andre indstillinger, men jeg har jo Valgrind — først vil jeg fikse denne bug, og så, hvis jeg er heldig, vil den oprindelige vise sig. Jeg kører alt det samme under Valgrind... Øv-øv-øv, uh-uh-uh, æh-æh-æh, det startede, gik normalt gennem initialiseringen og fortsatte videre forbi den oprindelige bug uden en eneste advarsel om forkert hukommelsesadgang, for ikke at nævne nedbrud. Sådan noget kunne livet ikke forberede mig på — et nedbrudt program stopper med at bryde sammen, når det køres under Valgrind. Hvad var det — et mysterium. Min hypotese er, at da der i nærheden af den aktuelle instruktion efter nedbruddet under initialiseringen gdb viste aktivitet memset-a med en gyldig peger ved brug af enten mmx, eller xmm registre, så var det muligvis en eller anden slags justeringsfejl, selvom det stadig lyder usandsynligt.
Okay, Valgrind doesn't seem to be a helper here. And here began the most unpleasant part — everything seems to start, but crashes for absolutely unknown reasons due to an event that could have occurred millions of instructions ago. For a long time, it was unclear how to even approach this. In the end, I had to sit down and debug. Printing what the header had been rewritten to showed that it looked more like binary data than a number. And, lo and behold, this binary string was found in the BIOS file — so now it could be said with sufficient certainty that it was a buffer overflow, and it was even clear what had been written into this buffer. Well, moving on — fortunately, there is no address space randomization in Emscripten, there are also no holes in it, so you can write somewhere in the middle of the code outputting data at the pointer from the previous run, look at the data, check the pointer, and if it hasn't changed, get some information for reflection. However, linking takes a couple of minutes after any changes, but what can you do. As a result, a specific line was found that copies the BIOS from the temporary buffer to the guest memory — and indeed, there wasn't enough space in the buffer. Searching for the source of that strange buffer address led to the function qemu_anon_ram_alloc i filen oslib-posix.c — the logic was like this: sometimes it can be useful to align the address to a huge page of size 2 MB, for that we’ll first ask for mmap a little more, and then return the excess using munmap. And if such alignment is not required, we will indicate instead of 2 MB the result getpagesize() — mmap will still give an aligned address... So in Emscripten mmap it simply calls malloc, and that, of course, does not align by page. In general, the bug that frustrated me for a couple of months was fixed with a change in to the lines.
Function calling features
And now the processor is computing something, Qemu does not crash, but the screen does not turn on, and the processor quickly enters a loop, judging by the output. -d exec,in_asm,out_asm. Der opstod en hypotese: timer interrupts (eller generelt alle interrupts) kommer ikke. Og faktisk, hvis man fjerner interrupts fra den native build, der af en eller anden grund fungerede, viser et lignende billede sig. Men gåden viste sig at ligge et helt andet sted: sammenligning af tracing, der er udstedt med den ovennævnte indstilling, viste, at execution paths divergerer meget tidligt. Her skal det siges, at sammenligningen af log-output fra dispatcher emrun debug output med output fra den native build — er ikke en helt mekanisk proces. Jeg ved ikke præcist, hvordan programmet, der køres i browseren, forbinder sig med emrun, men nogle linjer i output viser sig at være ombyttet, så forskellen i diffen — er endnu ikke grund til at tro, at execution paths er divergeret. Generelt blev det klart, at ifølge instruktionen ljmpl skifter man til forskellige adresser, og bytekoden genereres principielt forskelligt: den ene indeholder en instruktion til at kalde en C-hjælpefunktion, den anden gør ikke. Efter at have googlet instruktionen og studeret koden, der translerer disse instruktioner, blev det klart, at for det første, direkte før den, i registeret cr0 blev der skrevet — også ved hjælp af hjælperen —, der overfører processoren til beskyttet tilstand, og for det andet, at js-versionen aldrig kom ind i den beskyttede tilstand. Og sagen er, at en anden funktion ved Emscripten er en modvilje mod at tolerere kode som implementeringen af instruktionen call i TCI, der hver pointer til en funktion fører til typen long long f(int arg0, .. int arg9) — funktioner skal kaldes med det korrekte antal argumenter. Hvis denne regel overtrædes, vil programmet, afhængigt af debug-indstillingerne, enten crashe (hvilket er godt) eller kalde en helt anden funktion (hvilket vil være trist at debugge). Der er også en tredje mulighed — at inkludere genereringen af wrappers, der tilføjer / fjerner argumenter, men samlet set fylder disse wrappers rigtig meget, mens jeg faktisk kun har brug for lige lidt over hundrede wrappers. Kun dette er temmelig trist, men der viste sig at være et mere alvorligt problem: i den genererede kode af wrapper-funktioner blev argumenterne konverteret-konverteret, men funktionen med de genererede argumenter blev nogle gange ikke kaldt — ligesom i min implementering af libffi. Det vil sige, at nogle hjælpere simpelthen ikke blev udført.
Heldigvis har Qemu maskinlæselige lister over hjælpere i form af en headerfil som
DEF_HELPER_0(lock, void)
DEF_HELPER_0(unlock, void)
DEF_HELPER_3(write_eflags, void, env, tl, i32)De bruges på en ret sjov måde: først overstyres makroerne på den mest finurlige måde DEF_HELPER_n, og så inkluderes helper.h. Indtil det punkt, hvor makroen udfolder sig i initialisatoren for strukturen og et komma, hvorefter et array defineres, men i stedet for elementer — #include <helper.h> Som et resultat, kom der endelig en anledning til at prøve biblioteket , og der blev skrevet et script, der genererer wrappers præcis til de funktioner, der er nødvendige.
Og nu, efter dette, ser det ud til, at processoren fungerer. Ser ud til, fordi skærmen aldrig blev initialiseret, selvom det lykkedes at køre memtest86+ i den native version. Her skal det præciseres, at koden til blokindgang/udgang i Qemu er skrevet i koroutine. I Emscripten er der en ret snirklet implementering, men den skulle stadig understøttes i Qemu-koden, og man kan allerede nu fejlsøge processoren: Qemu understøtter mulighederne -kernel, -initrd, -append, ved hjælp af hvilke man kan indlæse Linux eller for eksempel memtest86+, uden overhovedet at bruge blok enheder. Men her er problemet: i den native version kunne man observere uddata fra Linux kernel på konsollen med muligheden -nographic, men der kom ingen uddata i terminalen, hvorfra den blev startet emrun, så det var uklart: fungerer processoren ikke, eller fungerer grafikoutput. Så kom jeg i tankerne om at vente lidt. Det viste sig, at "processoren ikke sover, men bare blinker langsomt", og efter fem minutter kørte kernen en bunke meddelelser til konsollen og blev ved med at fryse. Det blev klart, at processoren generelt fungerer, og der måtte graves i koden til SDL2. Jeg kan desværre ikke bruge dette bibliotek, så jeg måtte handle lidt på gefühlen. På et tidspunkt blippede linjen parallel0 på en blå baggrund forbi, hvilket gav anledning til nogle tanker. I sidste ende viste det sig, at problemet var, at Qemu åbner flere virtuelle vinduer i et enkelt fysisk vindue, som man kan skifte mellem med Ctrl-Alt-n: det fungerer i den native version, men ikke i Emscripten. Efter at have fjernet overskydende vinduer med mulighederne -monitor none -parallel none -serial none og ved at tvinge en omtegning af hele skærmen ved hver ramme, begyndte alt pludselig at fungere.
Koroutine
Så, browser-emuleringen virker, men der er ikke noget interessant at køre fra en diskette, fordi der ikke er blokindgang/udgang — vi skal implementere understøttelse af korutiner. I Qemu er der allerede nogle coroutine-backends, men grundet de særlige forhold ved JavaScript og Emscripten's kodegenerator kan man ikke bare tage og begynde at jonglere med stacks. Det kunne virke som om, "alt er tabt, gipset tages af", men udviklerne af Emscripten har allerede taget sig af det hele. Det er implementeret på en ret underholdende måde: lad os kaldet en mistænkelig funktionsopkald såsom emscripten_sleep og nogle andre, der bruger Asyncify-mekanismen, samt opkald via pointer og opkald af enhver funktion, hvor der længere nede i stacken kan ske et af de to tidligere tilfælde. Og nu, før hvert mistænkeligt opkald, lad os allokere en async context, og straks efter opkaldet — lad os kontrollere, om der er sket et asynkront opkald, og hvis det er sket, så gemmer vi alle lokale variable i denne async context, angiver hvilken funktion der skal overdrages kontrol til, når udførelsen skal fortsætte, og forlader den nuværende funktion. Her er der virkelig plads til at studere effekten af — til behovene ved fortsættelsen af kodeudførelsen efter retur fra et asynkront opkald genererer compileren "stubber" af funktionen, som starter efter det mistænkelige opkald — sådan: hvis der er n mistænkelige opkald, så udsættes funktionen cirka i n/2 dele — og det er endda, hvis man ikke tager højde for, at der efter hvert potentielt asynkront opkald skal tilføjes en gemmelse af nogle lokale variable til den oprindelige funktion. Senere måtte jeg til og med skrive et simpelt Python-script, der for et givent sæt af især fragmenterede funktioner, som formodentlig "ikke tillader asynkronitet at slippe igennem dem" (det vil sige, at der ikke aktiveres stack-udrulning og alt det, jeg lige har beskrevet), angiver, hvilke opkald via pointer der skal ignoreres af compileren, så disse funktioner ikke betragtes som asynkrone. Ellers er JS-filer på 60 MB bare for meget — lad os i det mindste holde det til 30. Selvom jeg en gang indstillede et build-script og tilfældigvis fjernede linkers indstillinger, blandt hvilke var -O3. Jeg kører den genererede kode, og Chromium bruger al sin hukommelse og crasher. Jeg kiggede tilfældigt på, hvad den forsøgte at indlæse… Nå, hvad kan jeg sige, jeg ville også fryse, hvis jeg blev bedt om at gennemgå og optimere JavaScript på 500+ Mb.
Desværre var tjekene i Asyncify-bibliotekets kode ikke helt kompatible med longjmp-erne, der bruges i koden til den virtuelle processor, men efter et lille patch, der deaktiverede disse tjek og tvang til at genoprette konteksterne, som om alt var godt, fungerede koden. Og så begyndte det mærkelige: nogle gange udløste tjek i synkroniseringskoden – de som afslutter koden hårdt, hvis den logisk skal blokere – nogen forsøgte at gribe et allerede beslaglagt mutex. Heldigvis viste det sig ikke at være et logisk problem i den serialiserede kode – jeg brugte blot den standardfunktionalitet i main loop, der leveres af Emscripten, men nogle gange udfoldede det asynkrone opkald stakken helt, og på det tidspunkt udløste setTimeout fra main loop – således gik koden ind i iterationen af hovedsløjfen, uden at være ude af den forrige iteration. Jeg omskrev det til en uendelig løkke, og emscripten_sleep, og problemerne med mutexerne ophørte. Koden blev endda mere logisk – for i bund og grund har jeg ikke noget kode, der forbereder den næste animationsramme – det er blot processoren, der beregner noget, og skærmen opdateres periodisk. Dog sluttede problemerne ikke der: nogle gange afsluttede Qemu bare stille og roligt uden nogen undtagelser eller fejl. På det tidspunkt gav jeg op, men for at tage forskud på tingene kan jeg sige, at problemet var, at korutinkoden faktisk slet ikke brugte setTimeout (eller i det mindste ikke så ofte som man skulle tro): funktionen emscripten_yield sætter blot en flag for asynkront opkald. Hemmeligheden er, at emscripten_coroutine_next ikke er en asynkron funktion: indeni tjekker den flaget, nulstiller det og overfører kontrol til den rette. Så løber stakkeudfoldningen ud der. Problemet var, at på grund af use-after-free, som opstod, når korutinepuljen var slået fra, fordi jeg ikke havde kopieret en vigtig linje kode fra den eksisterende coroutine backend, returnerede funktionen qemu_in_coroutine true, når den faktisk skulle have returneret false. Dette førte til et opkald emscripten_yield, over hvilken der ikke var en stak emscripten_coroutine_next, stakken strakte sig til toppen, men ingen setTimeout, som jeg allerede har sagt, blev ikke udstillet.
JavaScript kodegenerering
Her er så den lovede "tilbagetrækning af kød". Ikke rigtig. Selvfølgelig, hvis man kører Qemu i browseren, og i den Node.js, får vi straks efter kodegenereringen i Qemu slet ikke den JavaScript. Men alligevel, der er i det mindste en eller anden form for omvendt transformation.
Lad os starte med lidt om, hvordan Qemu fungerer. Jeg beder om undskyldning på forhånd: jeg er ikke en professionel Qemu-udvikler, og mine konklusioner kan være fejlagtige nogle steder. Som man siger: "en studerendes mening behøver ikke at følge lærerens, Peano-axiomerne eller sund fornuft". Qemu har et antal understøttede gæste-arkitekturer, og for hver er der et katalog som target-i386. Ved opbygningen kan man angive understøttelse af flere gæste-arkitekturer, men resultatet vil bare være flere binære filer. Koden til understøttelse af gæste-arkitekturen genereres i sin tur af nogle interne operationer i Qemu, som TCG (Tiny Code Generator) så omdanner til maskinkode for værtsarkitekturen. Som nævnt i readme-filen i kataloget tcg, var dette oprindeligt en del af en almindelig C-kompiler, som senere blev tilpasset til JIT. Derfor er for eksempel målarkitekturen i termerne af dette dokument ikke længere en gæste-, men en værtsarkitektur. På et tidspunkt kom der endnu en komponent — Tiny Code Interpreter (TCI), som skal udføre koden (næsten de samme interne operationer) i fravær af en kodegenerator til en specifik værtsarkitektur. Faktisk, som nævnt i dens dokumentation, kan denne fortolker ikke altid fungere lige så godt som JIT-kodegeneratoren, ikke kun kvantitativt med hensyn til hastighed, men også kvalitativt. Selvom jeg ikke er sikker på, at dens beskrivelse er helt aktuel.
I starten forsøgte jeg at lave en fuldgyldig TCG backend, men blev hurtigt forvirret over kildekoderne og den ikke helt klare beskrivelse af bytekodeinstruktionerne, så jeg besluttede at pakke TCI-fortolkeren ind. Dette gav straks flere fordele:
- ved implementeringen af kodegeneratoren kunne jeg se på fortolkeren i stedet for instruktionsbeskrivelsen
- Man kan generere funktioner ikke for hver enkelt mødt oversættelsesblok, men for eksempel kun efter den hundrede udførelse.
- I tilfælde af ændringer i den genererede kode (hvilket tilsyneladende er muligt, hvis man ser på funktionerne med ordet patch i titlen) bliver jeg nødt til at invalidere den genererede JS-kode, men jeg vil i det mindste have noget at gen-generere den fra.
Vedrørende det tredje punkt er jeg ikke sikker på, at patching er muligt efter at koden første gang er blevet udført, men de første to punkter er tilstrækkelige.
Oprindeligt blev koden genereret som en stor switch baseret på adressen for den originale bytecode-instruktion, men efter at have husket artiklen om Emscripten, optimering af den genererede JS og relooping, besluttede jeg at generere mere menneskelig kode, især fordi det empirisk viste sig, at den eneste indgang til oversættelsesblokken er dens start. Så sagt, så gjort, efter et stykke tid fik jeg en kodegenerator, der genererer kode med if-sætninger (selvom uden løkker). Men skæbnen ville, at den crashede og gav en fejlmeddelelse om, at instruksen havde en forkert længde. I øvrigt var den sidste instruktion på dette rekursionsniveau brcond. Godt, jeg vil tilføje en identisk kontrol i genereringen af denne instruktion før det rekursive kald og efter det, og… ingen af dem blev udført, men efter switchen på assert'en crashede vi stadig. Til sidst, efter at have undersøgt den genererede kode, indså jeg, at efter switchen blev pekeren til den aktuelle instruktion genindlæst fra stakken og sandsynligvis overskrevet af den genererede JavaScript-kode. Sådan var det. At øge bufferen fra én megabyte til ti førte ikke til noget, og det blev klart, at kodegeneratoren kørte i ring. Jeg måtte sikre mig, at vi ikke var gået uden for grænserne for det nuværende TB, og hvis vi gjorde, skulle jeg returnere adressen for den næste TB med et minus, så man kunne fortsætte udførelsen. Desuden løser det problemet med "hvilke genererede funktioner skal invalidere, hvis denne del af bytekoden ændres?" — kun den funktion, der svarer til denne oversættelsesblok, skal invalidere. Forresten, selvom jeg debuggede alt i Chromium (da jeg bruger Firefox og det er lettere for mig at bruge en separat browser til eksperimenter), hjalp Firefox mig med at rette uoverensstemmelser med standarden asm.js, hvorefter koden begyndte at køre hurtigere i Chromium.
Eksempel på genereret kode
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"]Konklusion
Så arbejdet er stadig ikke færdigt, men jeg er træt af at forbedre dette langvarige projekt i hemmelighed. Derfor har jeg besluttet at offentliggøre det, jeg har nu. Koden er til tider skræmmende, da det er et eksperiment, og det er ikke klart på forhånd, hvad der skal gøres. Måske er det værd at lave ordentlige atomiske commits oven på en mere moderne version af Qemu. Indtil videre er der en gren i Git i blogformat: til hver 'niveau', der er blevet bestået, er der tilføjet en udførlig kommentar på russisk. Egentlig er denne artikel i høj grad en genskrivning af konklusionen. git log.
Du kan prøve det hele (vær forsigtig, trafik).
Hvad der allerede fungerer:
- Den virtuelle x86-proto-s processor fungerer
- Der er en fungerende prototype af JIT-kodegenerator fra maskinkode til JavaScript
- Der er en skabelon til at samle andre 32-bit gæstearkitekturer: du kan lige nu beundre det, der hænger i browseren under indlæsningen af Linux til MIPS-arkitekturen
Hvad kan der også gøres
- Accelerere emuleringen. Selv i JIT-tilstand ser det ud til at køre langsommere end Virtual x86 (men der er potentiale for en helt Qemu med mange emulerede hardware og arkitekturer)
- Lave et ordentligt interface - jeg er ikke akkurat en fantastisk webudvikler, så jeg har indtil videre genarbejdet standardgrænsefladen for Emscripten, som jeg kunne
- Prøve at starte mere komplekse funktioner i Qemu - netværk, VM-migration osv.
- OPD: Jeg skal sende mine få bidrag og bugrapporter til Emscripten upstream, ligesom tidligere portere har gjort med Qemu og andre projekter. Tak til dem for muligheden for stille at drage nytte af deres bidrag til Emscripten i mit arbejde.
Kilde: habr.com
