Qemu.js s podporo za JIT: še vedno lahko obrnete mleto meso nazaj

Pred nekaj leti Fabrice Bellard napisal jslinux je računalniški emulator, napisan v JavaScriptu. Potem je bilo vsaj še več Virtualni x86. Toda vsi so bili, kolikor vem, tolmači, medtem ko Qemu, ki ga je veliko prej napisal isti Fabrice Bellard, in verjetno vsak samospoštljiv sodobni emulator, uporablja JIT prevajanje gostujoče kode v sistemsko kodo gostitelja. Zdelo se mi je, da je čas za implementacijo nasprotne naloge v primerjavi s tisto, ki jo rešujejo brskalniki: JIT prevajanje strojne kode v JavaScript, za kar se mi je zdelo najbolj logično prenesti Qemu. Zdi se, zakaj Qemu, obstajajo enostavnejši in uporabniku prijaznejši emulatorji - isti VirtualBox, na primer - nameščen in deluje. Toda Qemu ima več zanimivih funkcij

  • odprtokodno
  • zmožnost dela brez gonilnika jedra
  • sposobnost dela v načinu tolmača
  • podpora za veliko število gostiteljskih in gostujočih arhitektur

V zvezi s tretjo točko lahko zdaj razložim, da se v načinu TCI dejansko ne interpretirajo navodila gostujočega stroja sama, temveč bajtna koda, pridobljena iz njih, vendar to ne spremeni bistva - da se zgradi in zažene Qemu na novi arhitekturi, če imate srečo, je dovolj prevajalnik C - pisanje generatorja kode se lahko odloži.

In zdaj, po dveh letih lagodnega brskanja z izvorno kodo Qemu v prostem času, se je pojavil delujoč prototip, v katerem že lahko zaženete na primer Kolibri OS.

Kaj je Emscripten

Dandanes se je pojavilo veliko prevajalnikov, katerih končni rezultat je JavaScript. Nekateri, kot je Type Script, so bili prvotno mišljeni kot najboljši način pisanja za splet. Hkrati je Emscripten način, kako vzeti obstoječo kodo C ali C++ in jo prevesti v obliko, berljivo brskalniku. Vklopljeno to stran Zbrali smo veliko vrat znanih programov: tukajNa primer, lahko pogledate PyPy - mimogrede, trdijo, da že imajo JIT. Pravzaprav ni mogoče vsakega programa preprosto prevesti in zagnati v brskalniku – obstaja več Lastnosti, s čimer pa se morate sprijazniti, saj napis na isti strani pravi: »Emscripten se lahko uporablja za prevajanje skoraj vseh prenosni Koda C/C++ v JavaScript". To pomeni, da obstajajo številne operacije, ki so nedefinirano glede na standard, vendar običajno delujejo na x86 - na primer neporavnan dostop do spremenljivk, ki je na splošno prepovedan v nekaterih arhitekturah. Na splošno , Qemu je program za več platform in , želel sem verjeti, in že ne vsebuje veliko nedefiniranega vedenja – vzemite ga in prevedite, nato se malo poigrajte z JIT – in končali ste! Vendar to ni Ovitek...

Najprej poskusite

Na splošno nisem prvi, ki je prišel na idejo o prenosu Qemu v JavaScript. Na forumu ReactOS je bilo postavljeno vprašanje, ali je to mogoče z uporabo Emscriptena. Še prej so se pojavile govorice, da je to naredil Fabrice Bellard osebno, vendar smo govorili o jslinuxu, ki je, kolikor vem, samo poskus ročnega doseganja zadostne zmogljivosti v JS in je bil napisan iz nič. Kasneje je bil napisan Virtual x86 - zanj so bili objavljeni nezakriti viri in, kot rečeno, je večji "realizem" emulacije omogočil uporabo SeaBIOS-a kot vdelane programske opreme. Poleg tega je bil vsaj en poskus prenosa Qemu z Emscripten - poskusil sem to narediti socketpair, vendar je bil razvoj, kolikor razumem, zamrznjen.

Torej, zdi se, tukaj so viri, tukaj je Emscripten - vzemite in sestavite. Obstajajo pa tudi knjižnice, od katerih je Qemu odvisen, in knjižnice, od katerih so te knjižnice odvisne itd., in ena izmed njih je libffi, od katerega je glib odvisen. Po internetu so krožile govorice, da obstaja ena v veliki zbirki vrat knjižnic za Emscripten, vendar je bilo nekako težko verjeti: prvič, ni bil mišljen kot nov prevajalnik, drugič, bil je na prenizki ravni. knjižnico, ki jo preprosto poberete in prevedete v JS. In ne gre le za vstavke v sklop - verjetno, če ga zasukate, lahko za nekatere klicne konvencije ustvarite potrebne argumente na skladu in pokličete funkcijo brez njih. Toda Emscripten je zapletena stvar: da bi bila ustvarjena koda videti znana optimizatorju motorja JS brskalnika, se uporabljajo nekateri triki. Zlasti tako imenovano ponovno zanko - generator kode, ki uporablja prejeti LLVM IR z nekaterimi abstraktnimi navodili za prehod, poskuša poustvariti verjetne if-je, zanke itd. No, kako se argumenti posredujejo funkciji? Seveda kot argumente funkcijam JS, torej, če je mogoče, ne prek sklada.

Na začetku je bila ideja, da preprosto napišem zamenjavo za libffi z JS in zaženem standardne teste, a na koncu sem bil zmeden, kako narediti svoje datoteke glave, da bodo delovale z obstoječo kodo - kaj lahko storim, kot pravijo, "Ali so naloge tako zapletene" Ali smo tako neumni? Tako rekoč sem moral libffi prenesti v drugo arhitekturo - na srečo ima Emscripten tako makre za vgrajeno sestavljanje (v Javascriptu, ja - no, ne glede na arhitekturo, torej asembler) in možnost sprotnega izvajanja kode. Na splošno sem po tem, ko sem se nekaj časa ukvarjal s fragmenti libffi, ki so odvisni od platforme, dobil nekaj kode, ki jo je bilo mogoče prevesti, in jo zagnal na prvem testu, na katerega sem naletel. Na moje presenečenje je bil test uspešen. Osupen nad mojo genialnostjo - brez šale, delovalo je od prvega zagona - sem, še vedno ne verjel svojim očem, šel še enkrat pogledat nastalo kodo, da bi ocenil, kam kopati naprej. Tukaj sem že drugič znorel – edino, kar je moja funkcija naredila ffi_call - to poroča o uspešnem klicu. Samega klica ni bilo. Tako sem poslal svojo prvo zahtevo za vleko, ki je popravila napako v testu, ki je jasna vsakemu učencu olimpijade - realnih števil se ne sme primerjati kot a == b in še kako a - b < EPS - zapomniti si morate tudi modul, sicer se bo 0 izkazalo za zelo enako 1/3 ... Na splošno sem prišel do določenega pristanišča libffi, ki prestane najpreprostejše teste in s katerim je glib sestavljeno - odločil sem se, da bo potrebno, dodal ga bom kasneje. Če pogledam naprej, bom rekel, da, kot se je izkazalo, prevajalnik sploh ni vključil funkcije libffi v končno kodo.

Toda, kot sem že rekel, obstajajo nekatere omejitve in med brezplačno uporabo različnih nedefiniranih vedenj se je skrila bolj neprijetna lastnost - JavaScript po zasnovi ne podpira večnitnosti s skupnim pomnilnikom. Načeloma se temu običajno lahko reče celo dobra ideja, vendar ne za prenos kode, katere arhitektura je vezana na niti C. Na splošno Firefox eksperimentira s podporo delavcem v skupni rabi, Emscripten pa ima implementacijo pthread zanje, vendar nisem želel biti odvisen od tega. Počasi sem moral izkoreniniti večnitnost iz kode Qemu - to je, poiskati, kje se niti začnejo, premakniti telo zanke, ki teče v tej niti, v ločeno funkcijo in poklicati takšne funkcije eno za drugo iz glavne zanke.

Drugi poskus

Na neki točki je postalo jasno, da je težava še vedno prisotna in da naključno premetavanje kode ne bo prineslo nič dobrega. Zaključek: proces dodajanja bergel moramo nekako sistematizirati. Zato je bila vzeta takrat sveža verzija 2.4.1 (ne 2.5.0, ker kdo ve, bodo v novi različici hrošči, ki še niso ulovljeni, jaz pa imam svojih hroščov dovolj ), in prva stvar je bila, da ga varno prepišem thread-posix.c. No, to je tako varno: če je nekdo poskušal izvesti operacijo, ki vodi do blokade, je bila funkcija takoj poklicana abort() - seveda to ni rešilo vseh težav naenkrat, je bilo pa vsaj nekako bolj prijetno kot tiho prejemati nedosledne podatke.

Na splošno so možnosti Emscripten zelo koristne pri prenosu kode v JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - ujamejo nekatere vrste nedefiniranega vedenja, kot so klici na neporavnan naslov (kar sploh ni skladno s kodo za tipizirana polja, kot je HEAP32[addr >> 2] = 1) ali klicanje funkcije z napačnim številom argumentov.

Mimogrede, napake pri poravnavi so ločeno vprašanje. Kot sem že rekel, ima Qemu »degenerirano« razlagalno zaledje za generiranje kode TCI (majhni tolmač kode), in če imate srečo, za gradnjo in zagon Qemu na novi arhitekturi zadostuje prevajalnik C. Ključne besede "če imaš srečo". Nisem imel sreče in izkazalo se je, da TCI pri razčlenjevanju svoje bajtne kode uporablja neporavnan dostop. To pomeni, da na vseh vrstah ARM in drugih arhitektur z nujno ravnim dostopom Qemu prevaja, ker imajo običajno zaledje TCG, ki generira izvorno kodo, vendar je drugo vprašanje, ali bo TCI deloval na njih. Vendar, kot se je izkazalo, je dokumentacija TCI jasno pokazala nekaj podobnega. Posledično so bili kodi dodani funkcijski klici za neporavnano branje, ki so bili odkriti v drugem delu Qemu.

Uničenje kopice

Posledično je bil popravljen neporavnan dostop do TCI, ustvarjena je bila glavna zanka, ki je poklicala procesor, RCU in nekatere druge malenkosti. In tako zaženem Qemu z možnostjo -d exec,in_asm,out_asm, kar pomeni, da morate povedati, kateri bloki kode se izvajajo, in tudi v času oddajanja napisati, kaj je bila koda gosta, kaj je postala koda gostitelja (v tem primeru bajtna koda). Zažene se, izvede več prevajalskih blokov, zapiše sporočilo za odpravljanje napak, ki sem ga pustil, da se bo RCU zdaj zagnal in ... se zruši abort() znotraj funkcije free(). S preigravanjem funkcije free() Uspelo nam je ugotoviti, da je v glavi heap bloka, ki leži v osmih bajtih pred dodeljenim pomnilnikom, namesto velikosti bloka ali česa podobnega, smeti.

Uničenje kopice - kako luštno... V takem primeru obstaja uporabno zdravilo - iz (če je možno) istih virov sestaviti domačo dvojiško datoteko in jo pognati pod Valgrindom. Čez nekaj časa je bila binarna datoteka pripravljena. Zaženem ga z enakimi možnostmi - zruši se celo med inicializacijo, preden dejansko doseže izvedbo. Seveda je neprijetno - očitno viri niso bili popolnoma enaki, kar ni presenetljivo, saj je configure izbrskal nekoliko drugačne možnosti, vendar imam Valgrind - najprej bom popravil to napako, nato pa, če bom imel srečo , se bo prikazal izvirnik. Enako poganjam pod Valgrindom ... J-j-j, j-j-j, uh-uh, začelo se je, normalno šlo skozi inicializacijo in šlo naprej mimo prvotne napake brez enega samega opozorila o nepravilnem dostopu do pomnilnika, da o padcih niti ne govorim. Življenje me, kot pravijo, ni pripravilo na to - program, ki se zruši, se preneha zrušiti, ko ga zaženete pod Walgrindom. Kaj je bilo, je skrivnost. Moja hipoteza je, da ko je bil v bližini trenutnega ukaza po zrušitvi med inicializacijo, je gdb pokazal delo memset-a z veljavnim kazalcem, ki uporablja bodisi mmx, oz xmm registrov, potem je morda šlo za kakšno napako pri poravnavi, čeprav je še vedno težko verjeti.

V redu, zdi se, da Valgrind tukaj ne pomaga. In tu se je začela najbolj gnusna stvar - zdi se, da se vse celo zažene, vendar se zruši iz popolnoma neznanih razlogov zaradi dogodka, ki bi se lahko zgodil pred milijoni navodil. Dolgo časa sploh ni bilo jasno, kako pristopiti. Na koncu sem se vseeno moral usesti in odpraviti napake. Tiskanje tega, s čimer je bila glava prepisana, je pokazalo, da ni videti kot številka, temveč nekakšen binarni podatek. In glej, glej, ta binarni niz je bil najden v datoteki BIOS - to je, zdaj je bilo mogoče z razumno gotovostjo reči, da je šlo za prekoračitev medpomnilnika, in celo jasno je, da je bil zapisan v ta medpomnilnik. No, potem pa nekaj takega - v Emscriptenu na srečo ni randomizacije naslovnega prostora, tudi lukenj v njem ni, tako da lahko napišeš nekje na sredino kode, da izpisuje podatke po kazalcu od zadnjega zagona, poglejte podatke, poglejte kazalec in, če se ni spremenil, dobite hrano za razmislek. Res je, da povezava po kakršni koli spremembi traja nekaj minut, toda kaj lahko storite? Posledično je bila najdena posebna vrstica, ki je kopirala BIOS iz začasnega medpomnilnika v pomnilnik za goste - in v medpomnilniku res ni bilo dovolj prostora. Iskanje vira tega nenavadnega naslova medpomnilnika je povzročilo funkcijo qemu_anon_ram_alloc v datoteki oslib-posix.c - logika je bila naslednja: včasih je lahko koristno poravnati naslov z ogromno stranjo velikosti 2 MB, za to bomo vprašali mmap najprej malo več, potem pa bomo s pomočjo vrnili presežek munmap. In če taka poravnava ni potrebna, bomo rezultat navedli namesto 2 MB getpagesize() - mmap še vedno bo dal poravnan naslov... Torej v Emscriptenu mmap samo klici malloc, vendar se seveda ne poravna na strani. Na splošno je bila napaka, ki me je nekaj mesecev frustrirala, odpravljena s spremembo v dva vrstice.

Značilnosti klicnih funkcij

In zdaj procesor nekaj šteje, Qemu se ne zruši, vendar se zaslon ne vklopi in procesor hitro preide v zanke, sodeč po izhodu -d exec,in_asm,out_asm. Pojavila se je hipoteza: prekinitve časovnika (ali na splošno vse prekinitve) ne pridejo. In res, če odvijete prekinitve iz domačega sklopa, ki je iz nekega razloga deloval, dobite podobno sliko. Toda to sploh ni bil odgovor: primerjava sledi, izdanih z zgornjo možnostjo, je pokazala, da sta se izvršilni poti zelo zgodaj razšli. Tukaj je treba povedati tisto primerjavo tega, kar je bilo posneto z zaganjalnikom emrun razhroščevanje izhoda z izhodom izvornega sklopa ni povsem mehanski postopek. Ne vem točno, kako se poveže program, ki se izvaja v brskalniku emrun, vendar se izkaže, da so nekatere vrstice v izhodu preurejene, tako da razlika v razliki še ni razlog za domnevo, da sta se trajektoriji razšli. Na splošno je postalo jasno, da v skladu z navodili ljmpl obstaja prehod na različne naslove in ustvarjena bajtna koda je bistveno drugačna: ena vsebuje navodilo za klic pomožne funkcije, druga pa ne. Po googlanju navodil in preučevanju kode, ki ta navodila prevaja, je postalo jasno, da je najprej tik pred njim v registru cr0 narejen je bil posnetek - tudi s pomočnikom - ki je preklopil procesor v zaščiten način, in drugič, da se js verzija nikoli ni preklopila v zaščiten način. Toda dejstvo je, da je še ena značilnost Emscriptena njegova nepripravljenost do toleriranja kode, kot je izvajanje navodil call v TCI, katerega rezultat katerega koli funkcijskega kazalca je tip long long f(int arg0, .. int arg9) - funkcije morajo biti poklicane s pravilnim številom argumentov. Če je to pravilo kršeno, se bo program, odvisno od nastavitev odpravljanja napak, zrušil (kar je dobro) ali pa sploh poklical napačno funkcijo (kar bo žalostno odpravljati napake). Obstaja tudi tretja možnost - omogočiti generiranje ovojov, ki dodajajo/odstranjujejo argumente, vendar skupaj ti ovoji zavzamejo precej prostora, kljub temu, da dejansko potrebujem le nekaj več kot sto ovojov. Samo to je zelo žalostno, vendar se je izkazalo, da obstaja resnejša težava: v ustvarjeni kodi ovojnih funkcij so bili argumenti pretvorjeni in pretvorjeni, včasih pa funkcija z ustvarjenimi argumenti ni bila poklicana - no, tako kot v moja izvedba libffi. To pomeni, da nekateri pomočniki preprosto niso bili usmrčeni.

Na srečo ima Qemu strojno berljive sezname pomočnikov v obliki datoteke glave, kot je

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

Uporabljajo se precej smešno: najprej so makri redefinirani na najbolj bizaren način DEF_HELPER_n, nato pa se vklopi helper.h. Do te mere, da je makro razširjen v inicializator strukture in vejico, nato pa je definirana matrika in namesto elementov - #include <helper.h> Posledično sem končno imel priložnost preizkusiti knjižnico v službi pyparsing, in napisan je bil skript, ki generira točno tiste ovoje za točno tiste funkcije, za katere so potrebni.

In tako se je po tem zdelo, da procesor deluje. Zdi se, da zato, ker zaslon ni bil nikoli inicializiran, čeprav se je memtest86+ lahko izvajal v izvirnem sestavu. Tukaj je treba pojasniti, da je Qemu blok V/I koda zapisana v korutinah. Emscripten ima svojo lastno zelo zapleteno implementacijo, vendar je še vedno moral biti podprt v kodi Qemu in zdaj lahko razhroščite procesor: Qemu podpira možnosti -kernel, -initrd, -append, s katerim lahko zaženete Linux ali na primer memtest86+, brez uporabe blokovnih naprav. Toda tukaj je težava: v izvornem sestavu je bilo mogoče videti izhod jedra Linuxa v konzolo z možnostjo -nographic, in brez izhoda iz brskalnika v terminal, od koder je bil zagnan emrun, ni prišel. To pomeni, da ni jasno: procesor ne deluje ali grafični izhod ne deluje. In potem mi je prišlo na misel, da bi malo počakala. Izkazalo se je, da "procesor ne spi, ampak preprosto počasi utripa," in po približno petih minutah je jedro na konzolo vrglo kup sporočil in še naprej viselo. Postalo je jasno, da procesor na splošno deluje in moramo se poglobiti v kodo za delo s SDL2. Na žalost ne znam uporabljati te knjižnice, zato sem moral ponekod delovati naključno. V nekem trenutku je na zaslonu na modrem ozadju zasvetila črta parallel0, ki je nakazala nekaj misli. Na koncu se je izkazalo, da je težava v tem, da Qemu odpre več navideznih oken v enem fizičnem oknu, med katerimi lahko preklapljate s kombinacijo tipk Ctrl-Alt-n: v izvorni gradnji deluje, v Emscriptenu pa ne. Ko se znebite nepotrebnih oken z možnostmi -monitor none -parallel none -serial none in navodila za prisilno prerisovanje celotnega zaslona na vsakem okvirju, je nenadoma vse delovalo.

Korutine

Torej, emulacija v brskalniku deluje, vendar v njej ne morete zagnati ničesar zanimivega na eni disketi, ker ni blokovnega V/I - implementirati morate podporo za korutine. Qemu že ima več korutinskih ozadij, vendar zaradi narave JavaScripta in generatorja kode Emscripten ne morete kar začeti žonglirati z nizi. Zdi se, da "vse ni več, mavec se odstrani", vendar so razvijalci Emscripten že poskrbeli za vse. To je implementirano precej smešno: poimenujmo klic funkcije, kot je ta, sumljiv emscripten_sleep in več drugih, ki uporabljajo mehanizem Asyncify, kot tudi klice kazalcev in klice katere koli funkcije, kjer se lahko eden od prejšnjih dveh primerov pojavi nižje v skladu. In zdaj, pred vsakim sumljivim klicem, bomo izbrali asinhroni kontekst in takoj po klicu bomo preverili, ali je prišlo do asinhronega klica, in če je, bomo shranili vse lokalne spremenljivke v tem asinhronem kontekstu, navedli, katera funkcija za prenos nadzora na čas, ko moramo nadaljevati izvajanje, in zapustite trenutno funkcijo. Tu je prostor za preučevanje učinka zapravljanje — za potrebe nadaljevanja izvajanja kode po vrnitvi iz asinhronega klica, prevajalnik generira »škrbine« funkcije, ki se začne po sumljivem klicu — takole: če je sumljivih klicev n, potem bo funkcija razširjena nekje n/2. krat — to je še vedno, če ne Upoštevajte, da morate po vsakem potencialno asinhronem klicu prvotni funkciji dodati shranjevanje nekaterih lokalnih spremenljivk. Kasneje sem moral celo napisati preprost skript v Pythonu, ki na podlagi danega nabora posebej preveč uporabljanih funkcij, ki domnevno »ne dovolijo, da bi asinhronost prešla skozi sebe« (to je promocija skladov in vse, kar sem pravkar opisal, ne delo v njih), označuje klice prek kazalcev, pri katerih naj prevajalnik prezre funkcije, tako da se te funkcije ne štejejo za asinhrone. In potem so datoteke JS pod 60 MB očitno preveč - recimo vsaj 30. Čeprav sem nekoč nastavljal zbirni skript in pomotoma vrgel ven možnosti povezovalnika, med katerimi je bila -O3. Zaženem ustvarjeno kodo in Chromium požre pomnilnik in se zruši. Nato sem slučajno pogledal, kaj je poskušal prenesti ... No, kaj naj rečem, tudi jaz bi zmrznil, če bi me prosili, naj premišljeno preučim in optimiziram 500+ MB velik Javascript.

Na žalost preverjanja v kodi podporne knjižnice Asyncify niso bila povsem prijazna longjmp-s, ki se uporabljajo v kodi navideznega procesorja, vendar po majhnem popravku, ki onemogoči ta preverjanja in na silo obnovi kontekste, kot da je vse v redu, je koda delovala. In potem se je začelo čudno: včasih so se sprožila preverjanja v sinhronizacijski kodi - ista, ki zrušijo kodo, če bi morala biti po izvajalni logiki blokirana - nekdo je poskušal zgrabiti že zajet mutex. Na srečo se je izkazalo, da to ni logična težava v serializirani kodi - preprosto sem uporabljal standardno funkcionalnost glavne zanke, ki jo ponuja Emscripten, a včasih bi asinhroni klic popolnoma odvil sklad in v tistem trenutku ne bi uspel setTimeout iz glavne zanke - tako je koda vstopila v iteracijo glavne zanke, ne da bi zapustila prejšnjo iteracijo. Ponovno zapisano v neskončni zanki in emscripten_sleep, in težave z muteksi so prenehale. Koda je postala celo bolj logična - navsezadnje pravzaprav nimam kode, ki bi pripravila naslednji okvir animacije - procesor samo nekaj izračuna in zaslon se občasno posodablja. Vendar se težave s tem niso ustavile: včasih se je izvajanje Qemu preprosto tiho končalo brez izjem ali napak. V tistem trenutku sem obupal nad tem, a če pogledam naprej, bom rekel, da je bila težava naslednja: koda korutine pravzaprav ne uporablja setTimeout (ali vsaj ne tako pogosto, kot si mislite): funkcija emscripten_yield preprosto nastavi zastavico asinhronega klica. Bistvo je v tem emscripten_coroutine_next ni asinhrona funkcija: interno preveri zastavico, jo ponastavi in ​​prenese nadzor tja, kjer je potreben. To pomeni, da se promocija sklada tam konča. Težava je bila v tem, da je zaradi uporabe po brezplačnem, ki se je pojavil, ko je bil bazen sorutin onemogočen zaradi dejstva, da nisem kopiral pomembne vrstice kode iz obstoječega zaledja soprogramov, funkcija qemu_in_coroutine vrnil true, čeprav bi moral vrniti false. To je vodilo do klica emscripten_yield, nad katerim ni bilo nikogar na skladu emscripten_coroutine_next, se je kup odvil do samega vrha, a ne setTimeout, kot sem že rekel, ni bil razstavljen.

Generiranje kode JavaScript

In tukaj je pravzaprav obljubljeno »obračanje mletega mesa nazaj«. res ne. Seveda, če v brskalniku zaženemo Qemu in v njem Node.js, bomo seveda po generiranju kode v Qemu dobili popolnoma napačen JavaScript. Ampak vseeno, nekakšna obratna transformacija.

Najprej nekaj o tem, kako Qemu deluje. Prosim, da mi takoj oprostite: nisem profesionalni razvijalec Qemu in moji sklepi so lahko na nekaterih mestih napačni. Kot pravijo, "ni nujno, da se učenčevo mnenje ujema z učiteljevim mnenjem, Peanovo aksiomatiko in zdravo pametjo." Qemu ima določeno število podprtih gostujočih arhitektur in za vsako obstaja imenik, kot je target-i386. Pri gradnji lahko določite podporo za več gostujočih arhitektur, vendar bo rezultat samo več binarnih datotek. Koda za podporo gostujoči arhitekturi nato generira nekatere notranje operacije Qemu, ki jih TCG (Tiny Code Generator) že spremeni v strojno kodo za gostiteljsko arhitekturo. Kot je navedeno v datoteki readme, ki se nahaja v imeniku tcg, je bil to prvotno del običajnega prevajalnika C, ki je bil kasneje prilagojen za JIT. Zato na primer ciljna arhitektura v smislu tega dokumenta ni več gostujoča arhitektura, ampak gostiteljska arhitektura. Na neki točki se je pojavila še ena komponenta - Tiny Code Interpreter (TCI), ki bi morala izvajati kodo (skoraj enake notranje operacije) v odsotnosti generatorja kode za določeno arhitekturo gostitelja. Pravzaprav, kot navaja njegova dokumentacija, ta tolmač morda ne bo vedno deloval tako dobro kot generator kode JIT, ne le kvantitativno v smislu hitrosti, ampak tudi kvalitativno. Čeprav nisem prepričan, da je njegov opis povsem relevanten.

Sprva sem poskušal narediti popolno zaledje TCG, a sem se hitro zmedel v izvorni kodi in ne povsem jasnem opisu navodil za bajtno kodo, zato sem se odločil zaviti tolmač TCI. To je prineslo več prednosti:

  • pri izvajanju generatorja kode ne bi smeli gledati na opis navodil, ampak na kodo tolmača
  • ne morete ustvariti funkcij za vsak prevajalski blok, na katerega naletite, ampak na primer šele po stoti izvedbi
  • če se generirana koda spremeni (in to se zdi možno, sodeč po funkcijah z imeni, ki vsebujejo besedo patch), bom moral generirano JS kodo razveljaviti, a jo bom imel vsaj iz česa znova generirati

Glede tretje točke nisem prepričan, da je popravljanje možno po prvi izvedbi kode, vendar sta prvi dve točki dovolj.

Sprva je bila koda ustvarjena v obliki velikega stikala na naslovu originalnega ukaza bajtne kode, potem pa sem se, ko sem se spomnil članka o Emscriptenu, optimizaciji ustvarjenega JS in ponovnem zankanju, odločil ustvariti več človeške kode, še posebej, ker je empirično izkazalo se je, da je edina vstopna točka v prevajalski blok njegov začetek. Rečeno kot storjeno, čez nekaj časa smo imeli generator kode, ki je generiral kodo z if-ji (čeprav brez zank). Toda smola, zrušil se je in dal sporočilo, da so bila navodila napačno dolga. Poleg tega je bilo zadnje navodilo na tej ravni rekurzije brcond. V redu, dodal bom enako preverjanje generiranju tega navodila pred in po rekurzivnem klicu in ... nobeno od njih ni bilo izvedeno, vendar po preklopu assert še vedno ni uspelo. Na koncu sem po preučevanju generirane kode ugotovil, da se po preklopu kazalec na trenutno navodilo znova naloži iz sklada in ga verjetno prepiše generirana koda JavaScript. In tako se je izkazalo. Povečanje medpomnilnika z enega megabajta na deset ni pripeljalo do ničesar in postalo je jasno, da generator kode teče v krogu. Preveriti smo morali, da nismo presegli meje trenutnega TB, in če smo, izdati naslov naslednjega TB z minusom, da smo lahko nadaljevali z izvajanjem. Poleg tega to reši problem "katere generirane funkcije naj bodo razveljavljene, če se je ta del bajtne kode spremenil?" — samo funkcijo, ki ustreza temu bloku prevajanja, je treba razveljaviti. Mimogrede, čeprav sem vse razhroščil v Chromiumu (ker uporabljam Firefox in mi je lažje uporabiti ločen brskalnik za poskuse), mi je Firefox pomagal popraviti nezdružljivosti s standardom asm.js, po katerem je koda začela delovati hitreje v Chromium.

Primer generirane 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"]

Zaključek

Torej, delo še vedno ni končano, vendar sem utrujen od tega, da bi to dolgoletno gradnjo skrivaj pripeljal do popolnosti. Zato sem se odločil, da objavim, kar imam za zdaj. Koda je ponekod malce strašljiva, ker je to eksperiment in vnaprej ni jasno, kaj je treba narediti. Verjetno je potem vredno izdati običajne atomske zaveze poleg kakšne sodobnejše različice Qemu. Medtem je v Giti nit v obliki bloga: za vsako "stopnjo", ki je bila vsaj nekako prehojena, je dodan podroben komentar v ruščini. Pravzaprav je ta članek v veliki meri ponovitev zaključka git log.

Vse lahko poskusiš tukaj (pozor na promet).

Kaj že deluje:

  • virtualni procesor x86 deluje
  • Obstaja delujoč prototip generatorja kode JIT iz strojne kode v JavaScript
  • Obstaja predloga za sestavljanje drugih 32-bitnih gostujočih arhitektur: prav zdaj lahko občudujete Linux za arhitekturo MIPS, ki zamrzne v brskalniku v fazi nalaganja

Kaj drugega lahko storite

  • Pospešite emulacijo. Tudi v načinu JIT se zdi, da deluje počasneje kot Virtual x86 (vendar obstaja potencialno celoten Qemu z veliko emulirane strojne opreme in arhitektur)
  • Za izdelavo običajnega vmesnika - odkrito povedano, nisem dober spletni razvijalec, zato sem za zdaj preoblikoval standardno lupino Emscripten, kolikor sem lahko
  • Poskusite zagnati bolj zapletene funkcije Qemu - mreženje, selitev VM itd.
  • UPS: svojih nekaj razvojnih dogodkov in poročil o napakah boste morali predložiti Emscriptenu navzgor, kot so to storili prejšnji prenašalci Qemu in drugih projektov. Hvala jim, ker so lahko implicitno uporabili svoj prispevek za Emscripten kot del moje naloge.

Vir: www.habr.com

Dodaj komentar