Qemu.js med JIT-støtte: fylling kan fortsatt slås tilbake

For noen år siden Fabrice Bellard skrevet av jslinux er en PC-emulator skrevet i JavaScript. Etter det var det i hvert fall mer Virtuell x86. Men alle av dem, så vidt jeg vet, var tolker, mens Qemu, skrevet mye tidligere av den samme Fabrice Bellard, og sannsynligvis enhver moderne emulator med respekt for seg selv, bruker JIT-kompilering av gjestekode til vertssystemkode. Det virket for meg som om det var på tide å implementere den motsatte oppgaven i forhold til den nettlesere løser: JIT-kompilering av maskinkode til JavaScript, som det virket mest logisk å portere Qemu for. Det ser ut til, hvorfor Qemu, det er enklere og brukervennlige emulatorer - den samme VirtualBox, for eksempel - installert og fungerer. Men Qemu har flere interessante funksjoner

  • åpen kilde
  • evne til å jobbe uten en kjernedriver
  • evne til å arbeide i tolkemodus
  • støtte for et stort antall både verts- og gjestearkitekturer

Når det gjelder det tredje punktet, kan jeg nå forklare at i TCI-modus er det faktisk ikke gjestemaskininstruksjonene i seg selv som tolkes, men bytekoden hentet fra dem, men dette endrer ikke essensen - for å bygge og kjøre Qemu på en ny arkitektur, hvis du er heldig, er A C-kompilator nok - å skrive en kodegenerator kan utsettes.

Og nå, etter to år med rolig fiksing med Qemu-kildekoden på fritiden, dukket det opp en fungerende prototype, der du allerede kan kjøre for eksempel Kolibri OS.

Hva er Emscripten

I dag har det dukket opp mange kompilatorer, sluttresultatet av disse er JavaScript. Noen, som Type Script, var opprinnelig ment å være den beste måten å skrive for nettet på. Samtidig er Emscripten en måte å ta eksisterende C- eller C++-kode og kompilere den til en nettleserlesbar form. På denne siden Vi har samlet mange porter av kjente programmer: herDu kan for eksempel se på PyPy – de hevder forresten at de allerede har JIT. Faktisk kan ikke alle programmer bare kompileres og kjøres i en nettleser - det er en rekke funksjoner, som du imidlertid må tåle, da inskripsjonen på samme side sier «Emscripten kan brukes til å kompilere nesten alle bærbar C/C++-kode til JavaScript". Det vil si at det er en rekke operasjoner som er udefinert atferd i henhold til standarden, men som vanligvis fungerer på x86 - for eksempel ujustert tilgang til variabler, som generelt er forbudt på enkelte arkitekturer. Generelt , Qemu er et program på tvers av plattformer og , ville jeg tro, og det inneholder ikke allerede mye udefinert oppførsel - ta det og kompiler, så tukle litt med JIT - og du er ferdig! Men det er ikke sak...

Første forsøk

Generelt sett er jeg ikke den første personen som kommer på ideen om å portere Qemu til JavaScript. Det ble stilt et spørsmål på ReactOS-forumet om dette var mulig med Emscripten. Enda tidligere gikk det rykter om at Fabrice Bellard gjorde dette personlig, men vi snakket om jslinux, som så vidt jeg vet bare er et forsøk på å manuelt oppnå tilstrekkelig ytelse i JS, og ble skrevet fra bunnen av. Senere ble Virtual x86 skrevet - uklare kilder ble lagt ut for det, og som nevnt gjorde den større "realismen" i emuleringen det mulig å bruke SeaBIOS som fastvare. I tillegg var det minst ett forsøk på å portere Qemu ved hjelp av Emscripten - jeg prøvde å gjøre dette stikkontakt, men utviklingen, så vidt jeg forstår, var frosset.

Så, ser det ut til, her er kildene, her er Emscripten - ta den og kompiler. Men det er også biblioteker som Qemu er avhengig av, og biblioteker som disse bibliotekene er avhengig av osv., og ett av dem er libffi, som glib avhenger av. Det gikk rykter på Internett om at det var en i den store samlingen av bibliotekporter for Emscripten, men det var på en eller annen måte vanskelig å tro: for det første var det ikke ment å være en ny kompilator, for det andre var det for lavt nivå. biblioteket for å bare plukke opp og kompilere til JS. Og det er ikke bare et spørsmål om monteringsinnsatser - sannsynligvis, hvis du vrir på det, kan du for noen kallekonvensjoner generere de nødvendige argumentene på stabelen og kalle funksjonen uten dem. Men Emscripten er en vanskelig ting: for å få den genererte koden til å se kjent ut for nettleseren JS-motoroptimereren, brukes noen triks. Spesielt den såkalte relooping - en kodegenerator som bruker den mottatte LLVM IR med noen abstrakte overgangsinstruksjoner prøver å gjenskape plausible hvis, løkker, etc. Vel, hvordan overføres argumentene til funksjonen? Naturligvis, som argumenter til JS-funksjoner, det vil si, hvis mulig, ikke gjennom stabelen.

I begynnelsen var det en idé å ganske enkelt skrive en erstatning for libffi med JS og kjøre standardtester, men til slutt ble jeg forvirret over hvordan jeg skulle lage headerfilene mine slik at de ville fungere med den eksisterende koden - hva kan jeg gjøre, som de sier, "Er oppgavene så komplekse "Er vi så dumme?" Jeg måtte portere libffi til en annen arkitektur, for å si det sånn – heldigvis har Emscripten både makroer for inline-montering (i Javascript, ja – vel, uansett arkitektur, altså assembler), og muligheten til å kjøre kode generert i farten. Generelt, etter å ha puslet med plattformavhengige libffi-fragmenter en stund, fikk jeg kompilerbar kode og kjørte den på den første testen jeg kom over. Til min overraskelse var testen vellykket. Forbløffet over genialiteten min - ingen spøk, det fungerte fra første lansering - jeg, fortsatt ikke troende mine øyne, gikk for å se på den resulterende koden igjen, for å vurdere hvor jeg skulle grave videre. Her ble jeg gal for andre gang - det eneste funksjonen min gjorde var ffi_call - Dette rapporterte en vellykket samtale. Det var ingen samtale i seg selv. Så jeg sendte min første pull-forespørsel, som korrigerte en feil i testen som er tydelig for enhver Olympiade-elev - reelle tall skal ikke sammenlignes som a == b og til og med hvordan a - b < EPS - du må også huske modulen, ellers vil 0 vise seg å være veldig lik 1/3... Generelt kom jeg opp med en viss port av libffi, som består de enkleste testene, og som glib er med kompilert - jeg bestemte meg for at det ville være nødvendig, jeg legger det til senere. Når jeg ser fremover, vil jeg si at kompilatoren, som det viste seg, ikke en gang inkluderte libffi-funksjonen i den endelige koden.

Men, som jeg allerede har sagt, er det noen begrensninger, og blant gratis bruk av ulike udefinerte atferd, har en mer ubehagelig funksjon blitt skjult - JavaScript ved design støtter ikke multithreading med delt minne. I prinsippet kan dette vanligvis til og med kalles en god idé, men ikke for porteringskode hvis arkitektur er knyttet til C-tråder. Generelt sett eksperimenterer Firefox med å støtte delte arbeidere, og Emscripten har en pthread-implementering for dem, men jeg ville ikke være avhengig av den. Jeg måtte sakte rote ut multithreading fra Qemu-koden - det vil si finne ut hvor trådene kjører, flytte kroppen til løkken som kjører i denne tråden inn i en egen funksjon, og kalle opp slike funksjoner en etter en fra hovedløkken.

Andre forsøk

På et tidspunkt ble det klart at problemet fortsatt var der, og at tilfeldig dytting av krykker rundt koden ikke ville føre til noe godt. Konklusjon: vi må på en eller annen måte systematisere prosessen med å legge til krykker. Derfor ble versjon 2.4.1, som var fersk på den tiden, tatt (ikke 2.5.0, for hvem vet, det vil være feil i den nye versjonen som ennå ikke er fanget, og jeg har nok av mine egne feil ), og det første var å omskrive det trygt thread-posix.c. Vel, det vil si like trygt: Hvis noen prøvde å utføre en operasjon som førte til blokkering, ble funksjonen umiddelbart kalt opp abort() - Dette løste selvfølgelig ikke alle problemene på en gang, men det var i det minste på en eller annen måte mer behagelig enn å stille og stille motta inkonsekvente data.

Generelt er Emscripten-alternativer veldig nyttige for å portere kode til JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - de fanger opp noen typer udefinert atferd, for eksempel anrop til en ujustert adresse (som ikke i det hele tatt stemmer overens med koden for innskrevne matriser som HEAP32[addr >> 2] = 1) eller kalle en funksjon med feil antall argumenter.

Justeringsfeil er forresten en egen sak. Som jeg allerede har sagt, har Qemu en "degenerert" tolkende backend for kodegenerering TCI (liten kodetolk), og for å bygge og kjøre Qemu på en ny arkitektur, hvis du er heldig, er en C-kompilator nok. "hvis du er heldig". Jeg var uheldig, og det viste seg at TCI bruker ujustert tilgang når den analyserer bytekoden. Det vil si at på alle slags ARM- og andre arkitekturer med nødvendigvis nivårettet tilgang, kompilerer Qemu fordi de har en vanlig TCG-backend som genererer innfødt kode, men om TCI vil fungere på dem er et annet spørsmål. Det viste seg imidlertid at TCI-dokumentasjonen tydelig indikerte noe lignende. Som et resultat ble funksjonskall for ujustert lesing lagt til koden, som ble funnet i en annen del av Qemu.

Ødeleggelse av hauger

Som et resultat ble ujustert tilgang til TCI korrigert, en hovedsløyfe ble opprettet som igjen kalte prosessoren, RCU og noen andre småting. Og så starter jeg Qemu med alternativet -d exec,in_asm,out_asm, som betyr at du må si hvilke kodeblokker som kjøres, og også ved sendingstidspunktet skrive hva gjestekoden var, hvilken vertskode ble (i dette tilfellet bytekode). Den starter, utfører flere oversettelsesblokker, skriver feilsøkingsmeldingen jeg la igjen om at RCU nå vil starte og... krasjer abort() inne i en funksjon free(). Ved å fikle med funksjonen free() Vi klarte å finne ut at i overskriften til heap-blokken, som ligger i de åtte bytene foran det tildelte minnet, i stedet for blokkstørrelsen eller noe lignende, var det søppel.

Ødeleggelse av haugen - hvor søt ... I et slikt tilfelle er det et nyttig middel - fra (hvis mulig) de samme kildene, sett sammen en innfødt binær og kjør den under Valgrind. Etter en tid var binæren klar. Jeg starter den med de samme alternativene - den krasjer selv under initialisering, før den faktisk når utførelse. Det er selvfølgelig ubehagelig - tilsynelatende var ikke kildene helt de samme, noe som ikke er overraskende, fordi konfigurere speidet ut litt forskjellige alternativer, men jeg har Valgrind - først skal jeg fikse denne feilen, og så, hvis jeg er heldig , vises den originale. Jeg kjører det samme under Valgrind ... Åååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååååå. Livet, som de sier, forberedte meg ikke på dette - et krasjprogram slutter å krasje når det lanseres under Walgrind. Hva det var er et mysterium. Min hypotese er at en gang i nærheten av gjeldende instruksjon etter en krasj under initialisering, viste gdb arbeid memset-a med en gyldig peker ved å bruke enten mmx, eller xmm registre, så var det kanskje en slags justeringsfeil, selv om det fortsatt er vanskelig å tro.

Ok, Valgrind ser ikke ut til å hjelpe her. Og her begynte det mest ekle - alt ser ut til å starte, men krasjer av absolutt ukjente årsaker på grunn av en hendelse som kunne ha skjedd for millioner av instruksjoner siden. I lang tid var det ikke engang klart hvordan man skulle nærme seg. Til slutt måtte jeg fortsatt sette meg ned og feilsøke. Utskrift av hva overskriften ble skrevet om med viste at den ikke så ut som et tall, men snarere en slags binær data. Og se og se, denne binære strengen ble funnet i BIOS-filen - det vil si at nå var det mulig å si med rimelig sikkerhet at det var et bufferoverløp, og det er til og med klart at det ble skrevet til denne bufferen. Vel, så noe sånt som dette - i Emscripten er det heldigvis ingen randomisering av adresserommet, det er heller ingen hull i det, så du kan skrive et sted midt i koden for å sende ut data ved peker fra siste lansering, se på dataene, se på pekeren, og hvis den ikke har endret seg, få mat til ettertanke. Riktignok tar det et par minutter å koble til etter en endring, men hva kan du gjøre? Som et resultat ble det funnet en spesifikk linje som kopierte BIOS fra den midlertidige bufferen til gjesteminnet - og det var faktisk ikke nok plass i bufferen. Å finne kilden til den merkelige bufferadressen resulterte i en funksjon qemu_anon_ram_alloc i fil oslib-posix.c - logikken der var denne: noen ganger kan det være nyttig å justere adressen til en stor side på 2 MB i størrelse, for dette vil vi spørre mmap først litt til, og så returnerer vi det overskytende med hjelp munmap. Og hvis slik justering ikke er nødvendig, vil vi indikere resultatet i stedet for 2 MB getpagesize() - mmap den vil fortsatt gi ut en justert adresse... Så i Emscripten mmap bare ringer malloc, men det stemmer selvfølgelig ikke på siden. Generelt ble en feil som frustrerte meg i et par måneder rettet av en endring i двух linjer.

Funksjoner av ringefunksjoner

Og nå teller prosessoren noe, Qemu krasjer ikke, men skjermen slår seg ikke på, og prosessoren går raskt inn i løkker, bedømt etter utgangen -d exec,in_asm,out_asm. En hypotese har dukket opp: timeravbrudd (eller generelt alle avbrudd) kommer ikke. Og faktisk, hvis du skru av avbruddene fra den innfødte forsamlingen, som av en eller annen grunn fungerte, får du et lignende bilde. Men dette var ikke svaret i det hele tatt: en sammenligning av sporene utstedt med alternativet ovenfor viste at henrettelsesbanene divergerte veldig tidlig. Her må det sies at sammenligning av hva som ble tatt opp ved hjelp av bæreraketten emrun feilsøking av utdata med utdata fra den opprinnelige sammenstillingen er ikke en fullstendig mekanisk prosess. Jeg vet ikke nøyaktig hvordan et program som kjører i en nettleser kobles til emrun, men noen linjer i utgangen viser seg å være omorganisert, så forskjellen i diff er ennå ikke en grunn til å anta at banene har divergert. Generelt ble det klart at i henhold til instruksjonene ljmpl det er en overgang til forskjellige adresser, og bytekoden som genereres er fundamentalt forskjellig: den ene inneholder en instruksjon om å kalle en hjelpefunksjon, den andre ikke. Etter å ha googlet instruksjonene og studert koden som oversetter disse instruksjonene, ble det klart at for det første rett før den i registeret cr0 det ble gjort et opptak - også ved hjelp av en hjelper - som byttet prosessoren til beskyttet modus, og for det andre at js-versjonen aldri byttet til beskyttet modus. Men faktum er at en annen funksjon ved Emscripten er dens motvilje mot å tolerere kode som implementering av instruksjoner call i TCI, som enhver funksjonspeker resulterer i type long long f(int arg0, .. int arg9) - funksjoner må kalles med riktig antall argumenter. Hvis denne regelen brytes, avhengig av feilsøkingsinnstillingene, vil programmet enten krasje (noe som er bra) eller kalle feil funksjon i det hele tatt (noe som vil være trist å feilsøke). Det er også et tredje alternativ - aktiver generering av wrappers som legger til / fjerner argumenter, men totalt tar disse wrappers mye plass, til tross for at jeg faktisk bare trenger litt mer enn hundre wrappers. Dette alene er veldig trist, men det viste seg å være et mer alvorlig problem: i den genererte koden til wrapper-funksjonene ble argumentene konvertert og konvertert, men noen ganger ble ikke funksjonen med de genererte argumentene kalt - vel, akkurat som i min libffi-implementering. Det vil si at noen hjelpere rett og slett ikke ble henrettet.

Heldigvis har Qemu maskinlesbare lister over hjelpere i form av en header-fil som

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

De brukes ganske morsomt: For det første blir makroer redefinert på den mest bisarre måten DEF_HELPER_n, og slås deretter på helper.h. I den grad makroen utvides til en strukturinitialiserer og et komma, og deretter defineres en matrise, og i stedet for elementer - #include <helper.h> Som et resultat fikk jeg endelig en sjanse til å prøve biblioteket på jobben pyparsing, og et skript ble skrevet som genererer akkurat de innpakningene for akkurat funksjonene de er nødvendige for.

Og så etter det så det ut til at prosessoren virket. Det ser ut til å være fordi skjermen aldri ble initialisert, selv om memtest86+ var i stand til å kjøre i den opprinnelige forsamlingen. Her er det nødvendig å klargjøre at Qemu-blokk I/O-koden er skrevet i koroutiner. Emscripten har sin egen veldig vanskelige implementering, men den måtte fortsatt støttes i Qemu-koden, og du kan feilsøke prosessoren nå: Qemu støtter alternativer -kernel, -initrd, -append, som du kan starte opp Linux eller for eksempel memtest86+ med, uten å bruke blokkenheter i det hele tatt. Men her er problemet: i den opprinnelige forsamlingen kunne man se Linux-kjerneutgangen til konsollen med alternativet -nographic, og ingen utgang fra nettleseren til terminalen der den ble lansert emrun, kom ikke. Det vil si at det ikke er klart: prosessoren fungerer ikke eller grafikkutgangen fungerer ikke. Og så falt det meg å vente litt. Det viste seg at "prosessoren ikke sover, men bare blinker sakte," og etter omtrent fem minutter kastet kjernen en haug med meldinger på konsollen og fortsatte å henge. Det ble klart at prosessoren generelt fungerer, og vi må grave i koden for å jobbe med SDL2. Dessverre vet jeg ikke hvordan jeg skal bruke dette biblioteket, så noen steder måtte jeg handle tilfeldig. På et tidspunkt blinket linjen parallel0 på skjermen på blå bakgrunn, noe som antydet noen tanker. Til slutt viste det seg at problemet var at Qemu åpner flere virtuelle vinduer i ett fysisk vindu, som du kan bytte mellom ved å bruke Ctrl-Alt-n: det fungerer i den opprinnelige builden, men ikke i Emscripten. Etter å ha blitt kvitt unødvendige vinduer ved hjelp av alternativer -monitor none -parallel none -serial none og instruksjoner for å kraftig tegne hele skjermen på hver ramme, alt fungerte plutselig.

Korutiner

Så emulering i nettleseren fungerer, men du kan ikke kjøre noe interessant single-floppy i den, fordi det ikke er noen blokk I/O - du må implementere støtte for coroutines. Qemu har allerede flere coroutine-backends, men på grunn av JavaScript og Emscripten-kodegeneratoren, kan du ikke bare begynne å sjonglere med stabler. Det ser ut til at "alt er borte, gipset blir fjernet," men Emscripten-utviklerne har allerede tatt seg av alt. Dette er implementert ganske morsomt: la oss kalle et funksjonskall som dette mistenkelig emscripten_sleep og flere andre som bruker Asyncify-mekanismen, samt pekeranrop og anrop til enhver funksjon der ett av de to foregående tilfellene kan forekomme lenger ned i stabelen. Og nå, før hvert mistenkelig anrop, vil vi velge en asynkronkontekst, og umiddelbart etter anropet vil vi sjekke om et asynkront anrop har skjedd, og hvis det har skjedd, vil vi lagre alle lokale variabler i denne asynkrone konteksten, angi hvilken funksjon for å overføre kontrollen til når vi må fortsette kjøringen, og avslutte gjeldende funksjon. Det er her det er rom for å studere effekten sløseri - for behovene for å fortsette kodekjøring etter retur fra et asynkront anrop, genererer kompilatoren "stubber" av funksjonen som starter etter et mistenkelig anrop - slik: hvis det er n mistenkelige anrop, vil funksjonen utvides et sted n/2 ganger — dette er fortsatt, hvis ikke. Husk at etter hvert potensielt asynkront anrop, må du legge til lagring av noen lokale variabler til den opprinnelige funksjonen. Deretter måtte jeg til og med skrive et enkelt skript i Python, som, basert på et gitt sett med spesielt overbrukte funksjoner som visstnok "ikke tillater asynkroni å passere gjennom seg selv" (det vil si stabelpromotering og alt jeg nettopp beskrev ikke arbeid i dem), indikerer kall gjennom pekere der funksjoner skal ignoreres av kompilatoren slik at disse funksjonene ikke anses som asynkrone. Og så er JS-filer under 60 MB helt klart for mye - la oss si minst 30. Selv om jeg en gang satte opp et monteringsskript og ved et uhell kastet ut linkeralternativene, blant annet -O3. Jeg kjører den genererte koden, og Chromium spiser opp minne og krasjer. Jeg så ved et uhell på det han prøvde å laste ned... Vel, hva kan jeg si, jeg ville også ha frosset hvis jeg hadde blitt bedt om å studere og optimalisere et 500+ MB Javascript.

Dessverre var ikke sjekkene i Asyncify-støttebibliotekskoden helt vennlige med longjmp-er som brukes i den virtuelle prosessorkoden, men etter en liten patch som deaktiverer disse sjekkene og kraftgjenoppretter kontekster som om alt var bra, fungerte koden. Og så begynte en merkelig ting: noen ganger ble sjekker i synkroniseringskoden utløst - de samme som krasjer koden hvis den, ifølge utførelseslogikken, skulle blokkeres - noen prøvde å ta tak i en allerede fanget mutex. Heldigvis viste dette seg å ikke være et logisk problem i den serialiserte koden - jeg brukte ganske enkelt standard hovedsløyfefunksjonalitet levert av Emscripten, men noen ganger ville den asynkrone samtalen pakke ut stabelen fullstendig, og i det øyeblikket ville den mislykkes setTimeout fra hovedsløyfen - dermed gikk koden inn i hovedsløyfen uten å forlate forrige iterasjon. Omskrev på en uendelig sløyfe og emscripten_sleep, og problemene med mutexes stoppet. Koden har til og med blitt mer logisk - jeg har tross alt ikke noen kode som forbereder neste animasjonsramme - prosessoren beregner bare noe, og skjermen oppdateres med jevne mellomrom. Men problemene stoppet ikke der: noen ganger ville Qemu-utførelsen ganske enkelt avsluttes stille uten unntak eller feil. I det øyeblikket ga jeg opp på det, men når jeg ser fremover, vil jeg si at problemet var dette: Coroutine-koden bruker faktisk ikke setTimeout (eller i det minste ikke så ofte som du kanskje tror): funksjon emscripten_yield setter ganske enkelt det asynkrone anropsflagget. Hele poenget er det emscripten_coroutine_next er ikke en asynkron funksjon: internt sjekker den flagget, tilbakestiller det og overfører kontrollen dit det trengs. Det vil si at promoteringen av stabelen slutter der. Problemet var at på grunn av bruk-etter-fri, som dukket opp da coroutine-poolen ble deaktivert på grunn av det faktum at jeg ikke kopierte en viktig kodelinje fra den eksisterende coroutine-backend, funksjonen qemu_in_coroutine returnerte sant når det faktisk skulle ha returnert usann. Dette førte til en samtale emscripten_yield, over hvilken det ikke var noen på stabelen emscripten_coroutine_next, stabelen foldet seg ut til toppen, men nei setTimeout, som jeg allerede sa, ble ikke utstilt.

Generering av JavaScript-kode

Og her er faktisk det lovede "å snu kjøttdeigen tilbake." Ikke egentlig. Selvfølgelig, hvis vi kjører Qemu i nettleseren, og Node.js i den, vil vi naturligvis etter kodegenerering i Qemu få helt feil JavaScript. Men likevel, en slags omvendt transformasjon.

Først litt om hvordan Qemu fungerer. Vennligst tilgi meg med en gang: Jeg er ikke en profesjonell Qemu-utvikler, og konklusjonene mine kan være feilaktige noen steder. Som de sier, "elevens mening trenger ikke å falle sammen med lærerens mening, Peanos aksiomatikk og sunn fornuft." Qemu har et visst antall støttede gjestearkitekturer og for hver er det en katalog som target-i386. Når du bygger kan du spesifisere støtte for flere gjestearkitekturer, men resultatet blir bare flere binærfiler. Koden for å støtte gjestearkitekturen genererer på sin side noen interne Qemu-operasjoner, som TCG (Tiny Code Generator) allerede gjør om til maskinkode for vertsarkitekturen. Som det står i readme-filen som ligger i tcg-katalogen, var denne opprinnelig en del av en vanlig C-kompilator, som senere ble tilpasset JIT. Derfor er for eksempel målarkitektur i forhold til dette dokumentet ikke lenger en gjestearkitektur, men en vertsarkitektur. På et tidspunkt dukket det opp en annen komponent - Tiny Code Interpreter (TCI), som skulle utføre kode (nesten de samme interne operasjonene) i fravær av en kodegenerator for en bestemt vertsarkitektur. Faktisk, som dokumentasjonen sier, kan det hende at denne tolken ikke alltid yter like bra som en JIT-kodegenerator, ikke bare kvantitativt når det gjelder hastighet, men også kvalitativt. Selv om jeg ikke er sikker på at beskrivelsen hans er helt relevant.

Først prøvde jeg å lage en fullverdig TCG-backend, men ble raskt forvirret i kildekoden og en ikke helt klar beskrivelse av bytekode-instruksjonene, så jeg bestemte meg for å pakke inn TCI-tolken. Dette ga flere fordeler:

  • Når du implementerer en kodegenerator, kan du ikke se på beskrivelsen av instruksjonene, men på tolkekoden
  • du kan generere funksjoner ikke for hver oversettelsesblokk som påtreffes, men for eksempel bare etter den hundrede utførelse
  • hvis den genererte koden endres (og dette ser ut til å være mulig, etter funksjonene med navn som inneholder ordet patch), må jeg ugyldiggjøre den genererte JS-koden, men i det minste vil jeg ha noe å regenerere den fra

Når det gjelder det tredje punktet, er jeg ikke sikker på at oppdatering er mulig etter at koden er utført for første gang, men de to første punktene er nok.

Opprinnelig ble koden generert i form av en stor svitsj på adressen til den originale bytekode-instruksjonen, men da jeg husket artikkelen om Emscripten, optimalisering av generert JS og relooping, bestemte jeg meg for å generere mer menneskelig kode, spesielt siden det empirisk viste seg at det eneste inngangspunktet til oversettelsesblokken er Start. Ikke før sagt enn gjort, etter en stund hadde vi en kodegenerator som genererte kode med ifs (riktignok uten loops). Men uflaks, den krasjet, og ga beskjed om at instruksjonene var av feil lengde. Dessuten var den siste instruksjonen på dette rekursjonsnivået brcond. Ok, jeg vil legge til en identisk sjekk for genereringen av denne instruksjonen før og etter det rekursive anropet og ... ikke en av dem ble utført, men etter påstandsbryteren mislyktes de fortsatt. Til slutt, etter å ha studert den genererte koden, innså jeg at etter svitsjen, blir pekeren til den gjeldende instruksjonen lastet på nytt fra stabelen og sannsynligvis overskrevet av den genererte JavaScript-koden. Og slik ble det. Å øke bufferen fra én megabyte til ti førte ikke til noe, og det ble klart at kodegeneratoren kjørte i sirkler. Vi måtte sjekke at vi ikke gikk utover grensene for gjeldende TB, og hvis vi gjorde det, så utsted adressen til neste TB med et minustegn slik at vi kunne fortsette henrettelsen. I tillegg løser dette problemet "hvilke genererte funksjoner bør ugyldiggjøres hvis denne biten med bytekode har endret seg?" — bare funksjonen som tilsvarer denne oversettelsesblokken må ugyldiggjøres. Forresten, selv om jeg feilsøkte alt i Chromium (siden jeg bruker Firefox og det er lettere for meg å bruke en egen nettleser for eksperimenter), hjalp Firefox meg med å rette opp inkompatibiliteter med asm.js-standarden, hvoretter koden begynte å fungere raskere i Krom.

Eksempel på generert 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"]

Konklusjon

Så arbeidet er fortsatt ikke fullført, men jeg er lei av å i all hemmelighet bringe denne langsiktige konstruksjonen til perfeksjon. Derfor bestemte jeg meg for å publisere det jeg har for nå. Koden er noen steder litt skummel, fordi dette er et eksperiment, og det er ikke klart på forhånd hva som må gjøres. Sannsynligvis, da er det verdt å utstede normale atomforpliktelser på toppen av en mer moderne versjon av Qemu. I mellomtiden er det en tråd i Gita i et bloggformat: for hvert "nivå" som i det minste på en eller annen måte har blitt passert, er det lagt til en detaljert kommentar på russisk. Egentlig er denne artikkelen i stor grad en gjenfortelling av konklusjonen git log.

Du kan prøve alt her (pass opp for trafikk).

Hva fungerer allerede:

  • x86 virtuell prosessor kjører
  • Det er en fungerende prototype av en JIT-kodegenerator fra maskinkode til JavaScript
  • Det er en mal for å sette sammen andre 32-bits gjestearkitekturer: akkurat nå kan du beundre Linux for MIPS-arkitekturen som fryser i nettleseren ved innlastingsstadiet

Hva annet kan du gjøre

  • Få fart på emuleringen. Selv i JIT-modus ser det ut til å kjøre tregere enn Virtual x86 (men det er potensielt en hel Qemu med mye emulert maskinvare og arkitekturer)
  • For å lage et normalt grensesnitt - ærlig talt, jeg er ikke en god nettutvikler, så foreløpig har jeg laget om standard Emscripten-skallet så godt jeg kan
  • Prøv å lansere mer komplekse Qemu-funksjoner - nettverk, VM-migrering, etc.
  • OPP: du må sende inn dine få utviklinger og feilrapporter til Emscripten oppstrøms, slik tidligere portører av Qemu og andre prosjekter gjorde. Takk til dem for at de implisitt kunne bruke deres bidrag til Emscripten som en del av oppgaven min.

Kilde: www.habr.com

Legg til en kommentar