Qemu.js med JIT-understøttelse: du kan stadig vende hakket baglæns

For nogle år siden Fabrice Bellard skrevet af jslinux er en pc-emulator skrevet i JavaScript. Efter det var der i hvert fald mere Virtuel x86. Men alle af dem, så vidt jeg ved, var tolke, mens Qemu, skrevet meget tidligere af den samme Fabrice Bellard, og sandsynligvis enhver moderne emulator med respekt for sig selv, bruger JIT-kompilering af gæstekode til værtssystemkode. Det forekom mig, at det var på tide at implementere den modsatte opgave i forhold til den, browsere løser: JIT-kompilering af maskinkode til JavaScript, som det virkede mest logisk at portere Qemu til. Det ser ud til, hvorfor Qemu, der er enklere og brugervenlige emulatorer - den samme VirtualBox, for eksempel - installeret og virker. Men Qemu har flere interessante funktioner

  • åben kildekode
  • evnen til at arbejde uden en kernedriver
  • evne til at arbejde i tolketilstand
  • support til en lang række både værts- og gæstearkitekturer

Med hensyn til det tredje punkt, kan jeg nu forklare, at det i TCI-tilstand faktisk ikke er gæstemaskinens instruktioner i sig selv, der fortolkes, men bytekoden opnået fra dem, men dette ændrer ikke essensen - for at kunne bygge og køre Qemu på en ny arkitektur, hvis du er heldig, er A C compiler nok - skrivning af en kodegenerator kan udskydes.

Og nu, efter to års afslappet pusling med Qemu-kildekoden i min fritid, dukkede en fungerende prototype op, hvor du allerede kan køre for eksempel Kolibri OS.

Hvad er Emscripten

I dag er der dukket mange compilere op, hvis slutresultat er JavaScript. Nogle, som Type Script, var oprindeligt beregnet til at være den bedste måde at skrive til nettet på. Samtidig er Emscripten en måde at tage eksisterende C- eller C++-kode og kompilere den til en browserlæsbar form. På denne side Vi har samlet mange porte af velkendte programmer: herDu kan for eksempel se på PyPy – de hævder i øvrigt allerede at have JIT. Faktisk kan ikke alle programmer blot kompileres og køres i en browser - der er et antal funktioner, som man dog må finde sig i, da der på samme side står på inskriptionen “Emscripten kan bruges til at kompilere næsten ethvert bærbare C/C++ kode til JavaScript". Det vil sige, at der er en række operationer, der er udefineret adfærd i henhold til standarden, men som normalt fungerer på x86 - for eksempel ujusteret adgang til variabler, som generelt er forbudt på nogle arkitekturer. Generelt , Qemu er et program på tværs af platforme og, ville jeg tro, og det indeholder ikke allerede en masse udefineret adfærd - tag det og kompilér, så pille lidt med JIT - og du er færdig! Men det er ikke sag...

Første forsøg

Generelt set er jeg ikke den første person, der har fundet på ideen om at overføre Qemu til JavaScript. Der blev stillet et spørgsmål på ReactOS-forummet, om dette var muligt ved hjælp af Emscripten. Endnu tidligere var der rygter om, at Fabrice Bellard gjorde dette personligt, men vi talte om jslinux, som, så vidt jeg ved, blot er et forsøg på manuelt at opnå tilstrækkelig ydeevne i JS, og blev skrevet fra bunden. Senere blev Virtual x86 skrevet - uforslørede kilder blev postet til det, og som sagt gjorde emuleringens større "realisme" det muligt at bruge SeaBIOS som firmware. Derudover var der mindst et forsøg på at portere Qemu ved hjælp af Emscripten - jeg prøvede at gøre dette socketpar, men udviklingen var, så vidt jeg forstår, fastfrosset.

Så, ser det ud til, her er kilderne, her er Emscripten - tag den og kompilér. Men der er også biblioteker, som Qemu afhænger af, og biblioteker, som disse biblioteker er afhængige af osv., og et af dem er libffi, som glib afhænger af. Der var rygter på internettet om, at der var en i den store samling af biblioteker til Emscripten, men det var på en eller anden måde svært at tro: for det første var det ikke meningen, at det skulle være en ny compiler, for det andet var det for lavt niveau. bibliotek til bare at hente og kompilere til JS. Og det er ikke kun et spørgsmål om assembly-indsatser - sandsynligvis, hvis du vrider på det, kan du for nogle kaldekonventioner generere de nødvendige argumenter på stakken og kalde funktionen uden dem. Men Emscripten er en vanskelig ting: For at få den genererede kode til at se bekendt ud for browseren JS Engine Optimizer, bruges nogle tricks. Især den såkaldte relooping - en kodegenerator, der bruger den modtagne LLVM IR med nogle abstrakte overgangsinstruktioner, forsøger at genskabe plausible hvis, loops osv. Nå, hvordan overføres argumenterne til funktionen? Naturligvis, som argumenter til JS-funktioner, det vil sige, hvis det er muligt, ikke gennem stakken.

I begyndelsen var der en idé om blot at skrive en erstatning for libffi med JS og køre standardtests, men til sidst blev jeg forvirret over, hvordan jeg skulle lave mine header-filer, så de ville fungere med den eksisterende kode - hvad kan jeg gøre, som de siger, "Er opgaverne så komplekse "Er vi så dumme?" Jeg var så at sige nødt til at portere libffi til en anden arkitektur - heldigvis har Emscripten både makroer til inline assemblering (i Javascript, ja - ja, uanset arkitekturen, så assembleren), og muligheden for at køre kode genereret i farten. Generelt, efter at have puslet med platformsafhængige libffi-fragmenter i nogen tid, fik jeg noget kompilerbar kode og kørte det på den første test, jeg stødte på. Til min overraskelse var testen vellykket. Forbløffet over mit geni - ingen joke, det virkede fra den første lancering - jeg, stadig ikke tro mine egne øjne, gik for at se på den resulterende kode igen for at vurdere, hvor jeg skulle grave næste gang. Her gik jeg amok for anden gang - det eneste min funktion gjorde var ffi_call - dette rapporterede et vellykket opkald. Der var ingen opkald i sig selv. Så jeg sendte min første pull request, som rettede en fejl i testen, som er tydelig for enhver OL-elev - reelle tal skal ikke sammenlignes som a == b og endda hvordan a - b < EPS - du skal også huske modulet, ellers vil 0 vise sig at være meget lig med 1/3... Generelt kom jeg frem til en bestemt port af libffi, som består de simpleste tests, og som glib er med kompileret - jeg besluttede, at det ville være nødvendigt, jeg tilføjer det senere. Når jeg ser fremad, vil jeg sige, at det viste sig, at compileren ikke engang inkluderede libffi-funktionen i den endelige kode.

Men, som jeg allerede har sagt, er der nogle begrænsninger, og blandt den frie brug af forskellig udefineret adfærd er der gemt en mere ubehagelig funktion - JavaScript ved design understøtter ikke multithreading med delt hukommelse. I princippet kan dette som regel endda kaldes en god idé, men ikke for porteringskode, hvis arkitektur er bundet til C-tråde. Generelt eksperimenterer Firefox med at understøtte delte arbejdere, og Emscripten har en pthread-implementering til dem, men jeg ville ikke være afhængig af det. Jeg var nødt til langsomt at udrydde multithreading fra Qemu-koden - det vil sige finde ud af, hvor trådene kører, flytte kroppen af ​​løkken, der kører i denne tråd, ind i en separat funktion og kalde sådanne funktioner en efter en fra hovedløkken.

Andet forsøg

På et tidspunkt blev det klart, at problemet stadig var der, og at tilfældigt skubbe krykker rundt i koden ikke ville føre til noget godt. Konklusion: vi skal på en eller anden måde systematisere processen med at tilføje krykker. Derfor blev version 2.4.1, som var frisk på det tidspunkt, taget (ikke 2.5.0, for hvem ved, der vil være fejl i den nye version, som endnu ikke er fanget, og jeg har nok af mine egne fejl ), og det første var at omskrive det sikkert thread-posix.c. Nå, det vil sige, så sikkert: Hvis nogen forsøgte at udføre en operation, der førte til blokering, blev funktionen straks kaldt abort() - selvfølgelig løste dette ikke alle problemerne på én gang, men det var i hvert fald på en eller anden måde mere behageligt end stille og roligt at modtage inkonsistente data.

Generelt er Emscripten-indstillinger meget nyttige ved portering af kode til JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - de fanger nogle typer af udefineret adfærd, såsom opkald til en ujusteret adresse (hvilket slet ikke stemmer overens med koden for indtastede arrays som f.eks. HEAP32[addr >> 2] = 1) eller kalder en funktion med det forkerte antal argumenter.

Justeringsfejl er i øvrigt et særskilt problem. Som jeg allerede har sagt, har Qemu en "degenereret" fortolkende backend til kodegenerering TCI (lille kodefortolker), og til at bygge og køre Qemu på en ny arkitektur, hvis du er heldig, er en C-compiler nok. "hvis du er heldig". Jeg var uheldig, og det viste sig, at TCI bruger ujusteret adgang, når den parser sin bytekode. Det vil sige, at på alle mulige ARM og andre arkitekturer med nødvendigvis leveled access kompilerer Qemu, fordi de har en normal TCG backend, der genererer native kode, men om TCI vil arbejde på dem er et andet spørgsmål. Men som det viste sig, indikerede TCI-dokumentationen klart noget lignende. Som et resultat blev funktionskald til ujusteret læsning tilføjet til koden, som blev fundet i en anden del af Qemu.

Dyngedestruktion

Som et resultat blev ujusteret adgang til TCI rettet, en hovedsløjfe blev oprettet, der igen kaldte processoren, RCU og nogle andre små ting. Og så starter jeg Qemu med muligheden -d exec,in_asm,out_asm, hvilket betyder, at du skal sige, hvilke kodeblokke, der udføres, og også på tidspunktet for udsendelsen at skrive, hvad gæstekoden var, hvilken værtskode blev til (i dette tilfælde bytekode). Den starter, udfører flere oversættelsesblokke, skriver fejlfindingsmeddelelsen, jeg efterlod, at RCU nu vil starte og... går ned abort() inde i en funktion free(). Ved at pille ved funktionen free() Det lykkedes os at finde ud af, at i headeren af ​​heap-blokken, som ligger i de otte bytes forud for den allokerede hukommelse, i stedet for blokstørrelsen eller noget lignende, var der skrald.

Ødelæggelse af dyngen - hvor sødt... I sådan et tilfælde er der et nyttigt middel - fra (hvis muligt) de samme kilder, saml en indfødt binær og kør den under Valgrind. Efter noget tid var binæren klar. Jeg starter den med de samme muligheder - den går ned selv under initialisering, før den rent faktisk når udførelse. Det er selvfølgelig ubehageligt - tilsyneladende var kilderne ikke helt de samme, hvilket ikke er overraskende, fordi konfigurere spejdede ud af lidt forskellige muligheder, men jeg har Valgrind - først vil jeg rette denne fejl, og derefter, hvis jeg er heldig , vises den originale. Jeg kører det samme under Valgrind... Ååååååååååååååh, det startede, gik normalt igennem initialiseringen og gik videre forbi den originale fejl uden en eneste advarsel om forkert hukommelsesadgang, for ikke at nævne om fald. Livet, som de siger, forberedte mig ikke på dette - et nedbrudsprogram holder op med at gå ned, når det lanceres under Walgrind. Hvad det var, er et mysterium. Min hypotese er, at en gang i nærheden af ​​den aktuelle instruktion efter et nedbrud under initialisering, viste gdb arbejde memset-a med en gyldig pointer ved at bruge enten mmx, eller xmm registre, så var det måske en slags justeringsfejl, selvom det stadig er svært at tro.

Okay, Valgrind ser ikke ud til at hjælpe her. Og her begyndte det mest modbydelige - alt ser ud til at starte, men går ned af absolut ukendte årsager på grund af en begivenhed, der kunne være sket for millioner af instruktioner siden. I lang tid var det ikke engang klart, hvordan man skulle gribe det an. Til sidst skulle jeg stadig sætte mig ned og fejlfinde. Udskrivning af, hvad overskriften blev omskrevet med, viste, at den ikke lignede et tal, men snarere en slags binær data. Og se, denne binære streng blev fundet i BIOS-filen - det vil sige nu var det muligt at sige med rimelig tillid, at det var et bufferoverløb, og det er endda tydeligt, at det blev skrevet til denne buffer. Nå, så sådan noget - i Emscripten er der heldigvis ingen randomisering af adresserummet, der er heller ingen huller i det, så man kan skrive et sted midt i koden for at udlæse data ved pointer fra sidste lancering, se på dataene, se på markøren, og hvis de ikke har ændret sig, få stof til eftertanke. Sandt nok tager det et par minutter at linke efter enhver ændring, men hvad kan du gøre? Som et resultat blev der fundet en specifik linje, der kopierede BIOS'en fra den midlertidige buffer til gæstehukommelsen - og der var faktisk ikke nok plads i bufferen. At finde kilden til den mærkelige bufferadresse resulterede i en funktion qemu_anon_ram_alloc i fil oslib-posix.c - logikken der var denne: nogle gange kan det være nyttigt at justere adressen til en enorm side på 2 MB i størrelse, for dette vil vi spørge mmap først lidt mere, og så returnerer vi det overskydende med hjælpen munmap. Og hvis en sådan justering ikke er påkrævet, vil vi angive resultatet i stedet for 2 MB getpagesize()mmap det vil stadig give en tilpasset adresse... Altså i Emscripten mmap bare opkald malloc, men det passer selvfølgelig ikke på siden. Generelt blev en fejl, der frustrerede mig i et par måneder, rettet af en ændring i двух linjer.

Funktioner ved opkaldsfunktioner

Og nu tæller processoren noget, Qemu går ikke ned, men skærmen tænder ikke, og processoren går hurtigt i sløjfer, at dømme efter outputtet -d exec,in_asm,out_asm. Der er opstået en hypotese: timer-afbrydelser (eller generelt alle afbrydelser) kommer ikke. Og faktisk, hvis du skruer afbrydelserne af den indfødte forsamling, som af en eller anden grund virkede, får du et lignende billede. Men dette var slet ikke svaret: en sammenligning af de spor, der blev udstedt med ovenstående mulighed, viste, at udførelsesbanerne divergerede meget tidligt. Her skal det siges, at sammenligning af, hvad der blev optaget ved hjælp af løfteraketten emrun fejlfinding af output med output fra den native assembly er ikke en fuldstændig mekanisk proces. Jeg ved ikke præcis, hvordan et program, der kører i en browser, forbinder til emrun, men nogle linjer i output viser sig at være omarrangerede, så forskellen i diff er endnu ikke en grund til at antage, at banerne har divergeret. Generelt blev det klart, at i henhold til instruktionerne ljmpl der er en overgang til forskellige adresser, og den genererede bytekode er fundamentalt anderledes: den ene indeholder en instruktion om at kalde en hjælpefunktion, den anden gør det ikke. Efter at have googlet instruktionerne og studeret koden, der oversætter disse instruktioner, blev det klart, at for det første umiddelbart før det i registret cr0 der blev lavet en optagelse - også ved hjælp af en hjælper - som skiftede processoren til beskyttet tilstand, og for det andet at js-versionen aldrig skiftede til beskyttet tilstand. Men faktum er, at en anden egenskab ved Emscripten er dens modvilje mod at tolerere kode, såsom implementering af instruktioner call i TCI, som enhver funktionsmarkør resulterer i type long long f(int arg0, .. int arg9) - funktioner skal kaldes med det korrekte antal argumenter. Hvis denne regel overtrædes, afhængigt af fejlfindingsindstillingerne, vil programmet enten gå ned (hvilket er godt) eller overhovedet kalde den forkerte funktion (hvilket vil være trist at fejlfinde). Der er også en tredje mulighed - aktiver genereringen af ​​wrappers, der tilføjer/fjerner argumenter, men i alt fylder disse wrappers meget, på trods af at jeg faktisk kun har brug for lidt mere end hundrede wrappers. Dette alene er meget trist, men der viste sig at være et mere alvorligt problem: I den genererede kode af wrapper-funktionerne blev argumenterne konverteret og konverteret, men nogle gange blev funktionen med de genererede argumenter ikke kaldt - ja, ligesom i min libffi implementering. Det vil sige, at nogle hjælpere simpelthen ikke blev henrettet.

Heldigvis har Qemu maskinlæsbare lister over hjælpere i form af en header-fil som

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

De bruges ret sjovt: For det første omdefineres makroer på den mest bizarre måde DEF_HELPER_n, og tænder derefter helper.h. I det omfang makroen udvides til en strukturinitialiserer og et komma, og derefter defineres en matrix, og i stedet for elementer - #include <helper.h> Som et resultat havde jeg endelig en chance for at prøve biblioteket på arbejdet pyparsing, og der blev skrevet et script, der genererer præcis de indpakninger til præcis de funktioner, som de er nødvendige for.

Og så efter det så processoren ud til at virke. Det ser ud til at være fordi skærmen aldrig blev initialiseret, selvom memtest86+ var i stand til at køre i den oprindelige forsamling. Her er det nødvendigt at præcisere, at Qemu blok I/O-koden er skrevet i coroutiner. Emscripten har sin egen meget vanskelige implementering, men den skulle stadig understøttes i Qemu-koden, og du kan fejlsøge processoren nu: Qemu understøtter muligheder -kernel, -initrd, -append, hvormed du kan starte Linux eller for eksempel memtest86+, uden overhovedet at bruge blokenheder. Men her er problemet: i den oprindelige forsamling kunne man se Linux-kernens output til konsollen med muligheden -nographic, og intet output fra browseren til terminalen, hvorfra den blev startet emrun, kom ikke. Det vil sige, at det ikke er klart: Processoren virker ikke, eller grafikudgangen virker ikke. Og så faldt det mig ind at vente lidt. Det viste sig, at "processoren ikke sover, men blot blinker langsomt," og efter cirka fem minutter kastede kernen en masse beskeder på konsollen og fortsatte med at hænge. Det blev klart, at processoren generelt fungerer, og vi skal grave i koden for at arbejde med SDL2. Desværre ved jeg ikke, hvordan jeg bruger dette bibliotek, så nogle steder måtte jeg handle tilfældigt. På et tidspunkt blinkede linjen parallel0 på skærmen på en blå baggrund, hvilket antydede nogle tanker. Til sidst viste det sig, at problemet var, at Qemu åbner flere virtuelle vinduer i ét fysisk vindue, som man kan skifte imellem ved hjælp af Ctrl-Alt-n: det virker i den oprindelige build, men ikke i Emscripten. Efter at slippe af med unødvendige vinduer ved hjælp af muligheder -monitor none -parallel none -serial none og instruktioner til kraftigt at tegne hele skærmen på hver frame, alting virkede pludselig.

Coroutiner

Så emulering i browseren virker, men du kan ikke køre noget interessant single-floppy i den, fordi der ikke er nogen blok I/O - du skal implementere understøttelse af coroutines. Qemu har allerede flere coroutine-backends, men på grund af JavaScript og Emscripten-kodegeneratorens natur kan du ikke bare begynde at jonglere med stakke. Det ser ud til, at "alt er væk, gipset bliver fjernet", men Emscripten-udviklerne har allerede taget sig af alt. Dette er implementeret ret sjovt: lad os kalde et funktionskald som dette mistænkeligt emscripten_sleep og flere andre, der bruger Asyncify-mekanismen, samt pointer-kald og opkald til enhver funktion, hvor et af de to foregående tilfælde kan forekomme længere nede i stakken. Og nu, før hvert mistænkeligt opkald, vil vi vælge en asynkronkontekst, og umiddelbart efter opkaldet vil vi kontrollere, om der er sket et asynkront opkald, og hvis det er tilfældet, vil vi gemme alle lokale variabler i denne asynkrone kontekst, angive hvilken funktion at overføre kontrol til, hvornår vi skal fortsætte eksekveringen, og afslutte den aktuelle funktion. Det er her, der er plads til at undersøge effekten sløseri — til behovene for fortsat kodeudførelse efter retur fra et asynkront kald, genererer compileren "stubber" af funktionen, der starter efter et mistænkeligt kald - sådan her: hvis der er n mistænkelige kald, så vil funktionen blive udvidet et sted n/2 gange — dette er stadig, hvis ikke. Husk, at efter hvert potentielt asynkront opkald, skal du tilføje nogle lokale variabler til at gemme den oprindelige funktion. Efterfølgende skulle jeg endda skrive et simpelt script i Python, som, baseret på et givet sæt af særligt overbrugte funktioner, der angiveligt "ikke tillader asynkroni at passere igennem sig selv" (det vil sige, stackpromovering og alt det, jeg lige har beskrevet, ikke arbejde i dem), angiver opkald gennem pointere, hvor funktioner skal ignoreres af compileren, så disse funktioner ikke betragtes som asynkrone. Og så er JS-filer under 60 MB helt klart for meget - lad os sige mindst 30. Selvom jeg engang var ved at opsætte et assembly-script og ved et uheld smed linker-mulighederne ud, bl.a. -O3. Jeg kører den genererede kode, og Chromium spiser hukommelse og går ned. Jeg så ved et uheld på, hvad han prøvede at downloade... Nå, hvad kan jeg sige, jeg ville også have frosset, hvis jeg var blevet bedt om at studere og optimere et 500+ MB Javascript.

Desværre var kontrollerne i Asyncify-supportbibliotekskoden ikke helt venlige med longjmp-s, der bruges i den virtuelle processorkode, men efter en lille patch, der deaktiverer disse kontroller og kraftfuldt gendanner kontekster, som om alt var i orden, virkede koden. Og så begyndte en mærkelig ting: nogle gange blev der udløst checks i synkroniseringskoden - de samme, der crasher koden, hvis den ifølge eksekveringslogikken skulle blokeres - nogen forsøgte at få fat i en allerede fanget mutex. Heldigvis viste dette sig ikke at være et logisk problem i den serialiserede kode - jeg brugte simpelthen standard hovedsløjfe-funktionaliteten leveret af Emscripten, men nogle gange ville det asynkrone opkald fuldstændig pakke stakken ud, og i det øjeblik ville det mislykkes setTimeout fra hovedsløjfen - således kom koden ind i hovedsløjfe-iterationen uden at forlade den forrige iteration. Omskrev på en uendelig løkke og emscripten_sleep, og problemerne med mutexes stoppede. Koden er endda blevet mere logisk - faktisk har jeg ikke en eller anden kode, der forbereder den næste animationsramme - processoren beregner bare noget, og skærmen opdateres med jævne mellemrum. Problemerne stoppede dog ikke der: nogle gange stoppede Qemu-udførelsen ganske enkelt lydløst uden undtagelser eller fejl. I det øjeblik opgav jeg det, men når jeg ser fremad, vil jeg sige, at problemet var dette: Coroutine-koden bruger faktisk ikke setTimeout (eller i hvert fald ikke så ofte, som du måske tror): funktion emscripten_yield indstiller blot det asynkrone opkaldsflag. Hele pointen er det emscripten_coroutine_next er ikke en asynkron funktion: internt kontrollerer den flaget, nulstiller det og overfører kontrol til det sted, hvor det er nødvendigt. Det vil sige, at promoveringen af ​​stakken slutter der. Problemet var, at på grund af use-after-free, som dukkede op, da coroutine-puljen blev deaktiveret på grund af det faktum, at jeg ikke kopierede en vigtig kodelinje fra den eksisterende coroutine-backend, var funktionen qemu_in_coroutine returnerede sandt, når det faktisk burde have returneret falsk. Dette førte til et opkald emscripten_yield, over hvilken der ikke var nogen på stakken emscripten_coroutine_next, stakken foldede sig helt ud til toppen, men nej setTimeout, som jeg allerede sagde, var ikke udstillet.

Generering af JavaScript-kode

Og her er faktisk den lovede "vending af hakket kød tilbage." Ikke rigtig. Selvfølgelig, hvis vi kører Qemu i browseren, og Node.js i den, så vil vi naturligvis efter kodegenerering i Qemu få helt forkert JavaScript. Men alligevel en form for omvendt transformation.

Først lidt om, hvordan Qemu fungerer. Tilgiv mig venligst med det samme: Jeg er ikke en professionel Qemu-udvikler, og mine konklusioner kan være forkerte nogle steder. Som de siger, "elevens mening behøver ikke at falde sammen med lærerens mening, Peanos aksiomatik og sunde fornuft." Qemu har et vist antal understøttede gæstearkitekturer og for hver er der en mappe som target-i386. Når du bygger, kan du angive understøttelse af flere gæstearkitekturer, men resultatet bliver blot flere binære filer. Koden til at understøtte gæstearkitekturen genererer til gengæld nogle interne Qemu-operationer, som TCG (Tiny Code Generator) allerede gør til maskinkode for værtsarkitekturen. Som angivet i readme-filen i tcg-mappen, var dette oprindeligt en del af en almindelig C-compiler, som senere blev tilpasset til JIT. Derfor er for eksempel målarkitektur i forhold til dette dokument ikke længere en gæstearkitektur, men en værtsarkitektur. På et tidspunkt dukkede en anden komponent op - Tiny Code Interpreter (TCI), som skulle udføre kode (næsten de samme interne operationer) i mangel af en kodegenerator til en specifik værtsarkitektur. Faktisk, som det fremgår af dokumentationen, kan denne tolk muligvis ikke altid yde så godt som en JIT-kodegenerator, ikke kun kvantitativt med hensyn til hastighed, men også kvalitativt. Selvom jeg ikke er sikker på, at hans beskrivelse er helt relevant.

Først forsøgte jeg at lave en fuldgyldig TCG-backend, men blev hurtigt forvirret i kildekoden og en ikke helt klar beskrivelse af bytekode-instruktionerne, så jeg besluttede at pakke TCI-fortolkeren ind. Dette gav flere fordele:

  • Når du implementerer en kodegenerator, kan du ikke se på beskrivelsen af ​​instruktioner, men på tolkekoden
  • du kan generere funktioner ikke for hver oversættelsesblok, du støder på, men for eksempel først efter den hundrede udførelse
  • hvis den genererede kode ændres (og det ser ud til at være muligt, at dømme efter funktionerne med navne, der indeholder ordet patch), bliver jeg nødt til at ugyldiggøre den genererede JS-kode, men jeg vil i det mindste have noget at regenerere den fra

Med hensyn til det tredje punkt, er jeg ikke sikker på, at patching er mulig, efter at koden er eksekveret for første gang, men de første to punkter er nok.

Oprindeligt blev koden genereret i form af en stor switch på adressen på den originale bytecode-instruktion, men så, da jeg huskede artiklen om Emscripten, optimering af genereret JS og relooping, besluttede jeg at generere mere menneskelig kode, især da det empirisk viste sig, at det eneste indgangspunkt til oversættelsesblokken er dens Start. Ikke før sagt end gjort, efter et stykke tid havde vi en kodegenerator, der genererede kode med ifs (omend uden loops). Men uheld, det styrtede ned og gav en besked om, at instruktionerne var af en eller anden forkert længde. Desuden var den sidste instruktion på dette rekursionsniveau brcond. Okay, jeg tilføjer en identisk check til genereringen af ​​denne instruktion før og efter det rekursive opkald og... ikke en af ​​dem blev udført, men efter assert-omskifteren mislykkedes de stadig. Til sidst, efter at have studeret den genererede kode, indså jeg, at markøren til den aktuelle instruktion efter skiftet genindlæses fra stakken og sandsynligvis overskrives af den genererede JavaScript-kode. Og sådan blev det. At øge bufferen fra en megabyte til ti førte ikke til noget, og det blev tydeligt, at kodegeneratoren kørte i cirkler. Vi skulle tjekke, at vi ikke gik ud over grænserne for den nuværende TB, og hvis vi gjorde det, så udstede adressen på den næste TB med et minustegn, så vi kunne fortsætte eksekveringen. Derudover løser dette problemet "hvilke genererede funktioner skal ugyldiggøres, hvis dette stykke bytekode er ændret?" — kun den funktion, der svarer til denne oversættelsesblok, skal ugyldiggøres. Forresten, selvom jeg fejlede alt i Chromium (da jeg bruger Firefox og det er nemmere for mig at bruge en separat browser til eksperimenter), hjalp Firefox mig med at rette inkompatibiliteter med asm.js-standarden, hvorefter koden begyndte at arbejde hurtigere i Chrom.

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 afsluttet, men jeg er træt af i al hemmelighed at bringe denne langsigtede konstruktion til perfektion. Derfor besluttede jeg at offentliggøre, hvad jeg har for nu. Koden er nogle steder lidt skræmmende, fordi dette er et eksperiment, og det er ikke klart på forhånd, hvad der skal gøres. Sandsynligvis, så er det værd at udstede normale atomare forpligtelser oven på en mere moderne version af Qemu. I mellemtiden er der en tråd i Gita i et blogformat: For hvert "niveau", der i det mindste på en eller anden måde er blevet bestået, er der tilføjet en detaljeret kommentar på russisk. Egentlig er denne artikel i høj grad en genfortælling af konklusionen git log.

Du kan prøve det hele her (pas på trafik).

Hvad virker allerede:

  • x86 virtuel processor kører
  • Der er en fungerende prototype af en JIT-kodegenerator fra maskinkode til JavaScript
  • Der er en skabelon til at samle andre 32-bit gæstearkitekturer: lige nu kan du beundre Linux for MIPS-arkitekturen, der fryser i browseren under indlæsningsstadiet

Hvad kan du ellers gøre

  • Fremskynd emulering. Selv i JIT-tilstand ser det ud til at køre langsommere end Virtual x86 (men der er potentielt en hel Qemu med en masse emuleret hardware og arkitekturer)
  • For at lave en normal grænseflade - ærligt talt er jeg ikke en god webudvikler, så indtil videre har jeg lavet standard Emscripten-skallen så godt jeg kan
  • Prøv at starte mere komplekse Qemu-funktioner - netværk, VM-migrering osv.
  • UPS: du bliver nødt til at indsende dine få udviklinger og fejlrapporter til Emscripten upstream, som tidligere portører af Qemu og andre projekter gjorde. Tak til dem for implicit at kunne bruge deres bidrag til Emscripten som en del af min opgave.

Kilde: www.habr.com

Tilføj en kommentar