Qemu.js JIT-toe abil: hakkliha saab ikkagi tagasi keerata

MĂ”ned aastat tagasi kirjutas Fabrice Bellard jslinuxi — PC emulaatori, mis on kirjutatud JavaScriptis. PĂ€rast seda on olnud veel vĂ€hemalt Virtual x86. Kuid nii palju kui mina tean, olid need kĂ”ik tĂ”lgendajad, samas kui palju varem sama Fabrice Bellardi poolt kirjutatud Qemu ja tĂ”enĂ€oliselt iga enesest lugupidav kaasaegne emulaator kasutab JIT-kompileerimist kĂŒlalisekoodi host-sĂŒsteemi koodiks. Tundus, et on kĂ”ige Ă”igem tegeleda vastupidise ĂŒlesande realiseerimisega vĂ”rreldes sellega, mida brauserid lahendavad: JIT-kompileerimine masinakoodist JavaScripti, mille jaoks tundus kĂ”ige loogilisem portida Qemu. Tundub, et miks just Qemu, kuna on olemas lihtsamaid ja kasutajasĂ”bralikumaid emulaatoreid — nĂ€iteks VirtualBox, mis lihtsalt installitakse ja töötab. Kuid Qemu-l on mĂ”ned huvitavad omadused

  • avatud lĂ€htekood
  • vĂ”ime töötada ilma kerneli draiverita
  • vĂ”ime töötada tĂ”lgendaja reĆŸiimis
  • toetab suurt hulka nii hosti kui ka kĂŒlalisarhitektuure

Kolmanda punkti osas saan nĂŒĂŒd juba selgitada, et TCI reĆŸiimis tĂ”lgendatakse tegelikult mitte kĂŒlalismasinate kĂ€ske, vaid nendest saadud baytkoodi, kuid see ei muuda asja sisu — uuel arhitektuuril Qemu kokkupanemiseks ja kĂ€ivitamiseks piisab, kui vedamine on Ă”nnelik, C kompilaatorist — koodigeneraatori kirjutamine vĂ”ib oodata.

Ja nii, pĂ€rast kahte aastat aeglast askeldamist Qemu lĂ€htekoodide kallal, on valmis töötav prototĂŒĂŒp, milles saab nĂ€iteks kĂ€ivitada Kolibri OS-i.

Mis on Emscripten

Meie ajastul on tekkinud palju kompilaatoreid, mille lĂ”pp-tulemus on JavaScript. MĂ”ned, nagu TypeScript, on algselt mĂ”eldud parimaks viisiks veebis kirjutada. Samas on Emscripten viis vĂ”tta olemasolev C vĂ”i C++ kood ja kompileerida see brauserile arusaadavasse vormi. On sellel lehel kogutud mitmeid tuntud programmide porte: siit, nĂ€iteks vĂ”ib vaadata PyPy-d — muide, nagu vĂ€idetakse, on neil juba JIT. Tegelikult ei saa iga programmi lihtsalt kompileerida ja bra useris kĂ€ivitada — on rida erilised omadused, millega tuleb leppida, nagu ĂŒtleb sellel samal lehel olev kirjutis "Emscripteni saab kasutada peaaegu igasuguse portatiivne C/C++ koodi JavaScripti". See tĂ€hendab, et on olemas rida operatsioone, mis on standardi jĂ€rgi mÀÀramatud, kuid tavaliselt töötavad x86-l — nĂ€iteks mitteĂŒhtlane muutujate juurdepÀÀs, mis mĂ”nel arhitektuuril on tĂ€iesti keelatud. ÜhesĂ”naga, Qemu on platvormidevaheline programm ja, nagu lootsime, ei sisalda see palju mÀÀramatut kĂ€itumist — vĂ”ta ja kompila, siis natuke mĂ€ngida JIT-iga — ja valmis! Aga mitte siin


Esimene katse

Üldiselt ei ole ma esimene, kellel on tulnud idee Qemu JavaScripti portida. ReactOS foorumis kĂŒsiti, kas see on vĂ”imalik Emscripteni abil. Veel varem levis kuulujutt, et selle tegi isiklikult Fabrice Bellard, kuid jutt kĂ€is jslinuxist, mis, kui ma mitte ei eksi, on justkui katse saavutada JS-s piisavat jĂ”udlust kĂ€sitsi ja on kirjutatud nullist. Hiljem kirjutati Virtual x86 — selle kohta avaldati obfuskeerimata lĂ€htekood ja nagu vĂ€ideti, vĂ”imaldas suurem "realistlikkus" emulatsiooni kasutada SeaBIOS-i firmware'ina. Lisaks on olnud vĂ€hemalt ĂŒks katse portida Qemu Emscripteni abil — see ĂŒritati teha socketpair, kuid arendus, nii palju kui ma sain aru, oli kĂŒlmutatud.

Nii et tundub, et siin on lĂ€htekood, siin on Emscripten — vĂ”ta ja kompileeri. Kuid on ka raamatukogusid, millest Qemu sĂ”ltub, ja raamatukogudest, millest need raamatukogud sĂ”ltuvad jne, ning ĂŒks neist on — libffi, millest sĂ”ltub glib. Internetis levisid kuuldused, et Emscripteni jaoks on suurte teekide kogu seas ka see, kuid sellele oli raske uskuda: esiteks, uut kompilaatorit ei olnud, teiseks, see on liiga madala tasemega teek, et lihtsalt vĂ”tta ja JS-iks kompileerida. Ja asi pole ainult assembleri sisestustes — tĂ”enĂ€oliselt on vĂ”imalik mĂ”nel calling conventionil vajalikud argumendid virna topeltada ja funktsiooni kutsuda ka ilma nendeta. Kuid Emscripten on keeruline tööriist: et genereeritud kood nĂ€eks vĂ€lja tuttav, kasutavad nad mĂ”ned trikid. EelkĂ”ige nn relooping — koodigeneraator ĂŒritab saadud LLVM IR-l rekonstrueerida usutavaid if-sid, sildu jne. Noh, ja kuidas argumente funktsioonidele edastatakse? Loomulikult nagu JS-funktsioonide argumendid, st kui vĂ”imalik, siis mitte virna kaudu.

Alguses polnud mĂ”ttes muud, kui lihtsalt kirjutada libffi asendamine JS-iga ja lĂ€bida standardtestid, kuid lĂ”puks ajasin ma end segadusse, kuidas teha oma pealkirjafailid, et need töötaksid olemasoleva koodiga — mis teha, nagu öeldakse, "Kas ĂŒlesanded on nii keerulised vĂ”i oleme meie nii tobedad". Tuli portida libffi veel ĂŒhe arhitektuuri peale, kui nii vĂ”ib öelda — Ă”nneks on Emscriptenil nii makrode tugi inline-assembleri jaoks (JavaScriptis, jah — noh, mis arhitektuur, selline ka assembler) kui ka vĂ”imalus kĂ€ivitada genereeritud kood otse. ÜhesĂ”naga, olles aega viibinud libffi platvormispetsiifiliste fragmentidega, sain ma mingisuguse kompileeritava koodi ja lasin selle lĂ€bida esimeselt ettejuhtuvast testist. Minu ĂŒllatuseks lĂ€bis test edukalt. Olles oma geniaalsusest ĆĄokeeritud — naljakas, kuid see töötas esimesel katsel — vaatasin ma uuesti saadud koodi, et hinnata, kuhu edasi kaevata. Siin olin ma taas jahmunud — ainus, mida mu funktsioon tegi. ffi_call — seda raporteeriti edukast kutset. Kutset endaga ei olnud. Nii et ma saatsin oma esimese pull request'i, mis parandas olympiadel arusaadavat viga testis — reaalarveid ei tohiks vĂ”rrelda nagu a == b ja isegi nagu a - b < EPS — ei tohi unustada ka moduuli, muidu 0 on tĂ”epoolest vĂ”rreldav 1/3-ga... ÜhesĂ”naga, mul sai valmis mingi libffi port, mis lĂ€bib kĂ”ige lihtsamad testid, ja millega kompileerub glib — mĂ”tlesin, et hiljem, kui on aega, kirjutan rohkem. Osa sellest koodist libffi funktsiooni, nagu selgus, ei kuulunud isegi kompilaatorisse.

Kuid nagu ma juba ĂŒtlesin, on mĂ”ned piirangud ja mitmekesise mÀÀratlemata kĂ€itumise seas on ĂŒks ebameeldiv joon — JavaScript ei toeta disaini pĂ”hjal mĂ€llu jagatud mitme haru töötlemist. Üldiselt vĂ”ib seda isegi nimendada hea ideena, kuid mitte koodi portimiseks, mille arhitektuur tugineb C keele harudele. Üldiselt katsetatakse Firefoxis jagatud töötajate (shared workers) toetust ning Emscriptenis on olemas pthread realiseerimine, kuid sellest sĂ”ltuda ei soovinud. Pidin jĂ€rk-jĂ€rgult eemaldama mitme haru töötlemise Qemu koodist — st leidma, kus harud kĂ€ivitatakse, viima harus kĂ€ivitatava tsĂŒkli sisu eraldi funktsiooni ja vaheldumisi kutsuma sellised funktsioonid pĂ”hitsĂŒklist.

Teine katse

Mingil hetkel sai selgeks, et olukord ei ole muutunud ning et sĂŒsteemitu koodi impostimine ei toota soovitud tulemusi. JĂ€reldus: tuleb leida viis, kuidas sĂŒsteematiseerida koodeksite lisamise protsessi. SeetĂ”ttu valiti toona uus versioon 2.4.1 (mitte 2.5.0, kuna seal vĂ”ivad olla veel lahendamata bugid ning mul on juba oma bugide jagu), ning esimesena kirjutati see ohutult ĂŒmber. thread-posix.c. Noh, kuidas ohutult: kui keegi proovis teha operatsiooni, mis viis lukustumiseni, kutsuti koheselt vĂ€lja funktsioon abort() — muidugi ei lahendanud see kohe kĂ”iki probleeme, aga vĂ€hemalt oli see kuidagi meeldivam, kui vaikimisi andmete jĂ€rjepidevuse probleemide saamine.

Üldiselt aitab JavaScripti koodi portimisel Emscripteni valikud vĂ€ga palju. -s ASSERTIONS=1 -s SAFE_HEAP=1 — need tuvastavad mĂ”ningaid undefined behavior tĂŒĂŒpe, nĂ€iteks vale jĂ€rjepidevuse aadressi juurde pÀÀsemist (mis ei ĂŒhti absoluutselt typed arraysi koodiga, nagu HEAP32[addr >> 2] = 1) vĂ”i funktsiooni vale arvu argumentidega kutsumise.

Üldiselt on joondusvead eraldi teema. Nagu ma juba ĂŒtlesin, on Qemu-s "degenereeritud" tĂ”lgendav tagajĂ€rje koodigeneerimise TCI (tiny code interpreter), ja et Qemu uuel arhitektuuril ĂŒles seada ja kĂ€ivitada, piisab, kui on kĂ€epĂ€rast C kompilaator. MĂ€rksĂ”nad "kui hĂ€sti lĂ€heb". Mina ei saanud nii hĂ€sti hakkama ja selgus, et TCI oma baitkoodi tĂ”lgendamisel kasutab joondamata ligipÀÀsu. See tĂ€hendab, et igasugustel ARM-i ja muudel arhitektuuridel, kus on kohustuslik joondatud ligipÀÀs, kompileeritakse Qemu, kuna neile on olemas normatiivne TCG-tagaehitus, mis genereerib natiivkoodi, aga kas TCI töötab nendel arhitektuuridel — see on veel kĂŒsimus. Siiski, nagu selgus, mainiti TCI dokumentatsioonis midagi sellist selgelt. LĂ”ppkokkuvĂ”ttes lisati koodile funktsioonikutsed joondamata lugemise jaoks, mis leiti Qemu-s teises osas.

MĂ€lu purunemine

LĂ”ppkokkuvĂ”ttes anti TCI joondamata juurdepÀÀs korda, loodi peatsĂŒkkel, mis vaheldumisi kutsus esile protsessori, RCU ja veel mĂ”ned pisiasjad. Ja nĂŒĂŒd kĂ€ivitan Qemu valikuga -d exec,in_asm,out_asm, mis tĂ€hendas, et tuleb rÀÀkida, millised koodiplokid tĂ€idetakse, ning samal ajal tĂ”lkida, milline oli kĂŒlaliskood ja milline on hostikood (antud juhul bytecode). See kĂ€ivitatakse, tĂ€idab mitu tĂ”lkimisplokki, kirjutab minu poolt jÀÀnud tĂ”rkeotsingu sĂ”numi, et RCU kĂ€ivitatakse ja... kukub kokku, abort() funktsiooni sees free(). Kui funktsiooni uurimise teel free() Ă”nnestus vĂ€lja selgitada, et mĂ€luploki pea, mis asub kaheksas baitis, mis eelneb eraldatud mĂ€lule, ei olnud ploki suurus ega midagi sellist, vaid prĂŒgikogum.

Virna hĂ€vimine - kui armas... Sarnases olukorras on kasulik lahendus - (kui vĂ”imalik) koguda samad lĂ€htekoodid ja koostada natiivne binaar ning kĂ€itada seda Valgrindis. MĂ”ne aja pĂ€rast oli binaar valmis. KĂ€itan sama valikute kombinatsiooni - see kukub ikka veel algatamise ajal, ilma et jĂ”uaks Ă”igesse tĂ€ideviimise etappi. Muidugi on see ebameeldiv - ilmselt ei olnud lĂ€htekoodid tĂ€pselt samad, mis pole ka ĂŒllatav, kuna configure tuvastas veidi teised valikud, aga mul on ju olemas Valgrind - esmalt lahendan selle vea ja seejĂ€rel, kui Ă”nnestub, vĂ”ib-olla ilmub ĂŒles originaal. KĂ€itan kĂ”ik sama Valgrindis... Üh-Ă€-Ă€, ĂŒ-ĂŒ-ĂŒ, e-e-e, see kĂ€ivitati, lĂ€bisin algatamise normaalselt ja edasi minnes ei kohanud ma algset viga, ilma ĂŒhegi mĂ€luaadressimist kĂ€sitleva hoiatuseta, rÀÀkimata kukkumisest. Sellise asja jaoks ei olnud elu mind nagu öeldakse ette valmistanud - langev programm lĂ”petab kukkumise Valgrindis kĂ€ivitamisel. Mis see oli - mĂ”istatus. Minu hĂŒpotees on, et kuna praeguse kĂ€su ĂŒmbruses pĂ€rast algatamise kukkumist nĂ€itas gdb tööd memset-le kehtiva nĂ€idikuga kasutades kas mmx, vĂ”i xmm kuid, see vĂ”ib olla mingi joondamisviga, kuigi see tundub siiski kahtlane.

Okei, Valgrind ei nĂ€ytĂ€ olevan apuna. Ja sitten alkoi se kaikkein epĂ€miellytĂ€vin — kaikki tuntuu jopa kĂ€ynnistyvĂ€n, mutta romahtaa tĂ€ysin tuntemattomista syistĂ€ tapahtuman vuoksi, joka saattoi tapahtua miljoonia ohjeita sitten. PitkÀÀn aikaa ei ollut edes selvÀÀ, mistĂ€ aloittaa. Lopulta oli pakko istua alas ja debuggata. Tulostus siitĂ€, mitĂ€ otsikko oli kirjoitettu, osoitti, ettĂ€ tĂ€mĂ€ ei vaikuttanut olevan numero, vaan pikemminkin jotakin binaaridataa. Ja, ihme ja kumma, tĂ€mĂ€ binaarijono löytyi BIOS-tiedostosta — eli nyt voitiin varmuudella sanoa, ettĂ€ kyseessĂ€ oli puskurin ylivuoto, ja ymmĂ€rrettiin jopa, mitĂ€ tĂ€hĂ€n puskuriin kirjoitettiin. Ja sitten, kuten sanottu — EmscriptenissĂ€ onneksi ei ole osoiteavaruuden randomisointia, eikĂ€ siinĂ€ ole reikiĂ€, joten voidaan kirjoittaa jonnekin koodin keskelle datan tulostus edellisestĂ€ kĂ€ynnistyksestĂ€, tarkistaa data, katsoa osoittinta, ja jos se ei ole muuttunut, saada tietoa pohdittavaksi. Totta, linkittĂ€miseen menee pari minuuttia jokaisen muutoksen jĂ€lkeen, mutta mitĂ€pĂ€ tuolle voi. Lopputuloksena löytyi tarkka rivi, joka kopioi BIOSin vĂ€litilasta vieraaseen muistiin — ja totisesti, puskurissa ei ollut riittĂ€vĂ€sti tilaa. Sen outojen puskuriosoitteiden lĂ€hteen etsiminen johti funktioon. qemu_anon_ram_alloc failis oslib-posix.c — loogika oli selline: mĂ”nikord vĂ”ib olla kasulik joondada aadress huge page suurusega 2 MB, selleks kĂŒsime mmap esialgu pisut rohkem ja siis tagastame ĂŒleliigse munmap. Ja kui selline joondamine ei ole vajalik, siis mÀÀrame selle asemel 2 MB tulemuse getpagesize() — mmap siiski tagastab joondatud aadressi... Nii et Emscriptendis mmap lihtsalt kutsub malloc, mis loomulikult ei joonda lehe jĂ€rgi. ÜhesĂ”naga, viga, mis mind paar kuud hĂ€iris, lahendati muudatusega kahe ridade osas.

Funktsioonide kutsumise iseÀrasused

Ja nĂŒĂŒd juba protsessor midagi arvutab, Qemu ei langema, aga ekraan ei sĂŒtti ja protsessor jÀÀb kiiresti lĂ”putuks tsĂŒklisse, nagu nĂ€htub vĂ€ljundist -d exec,in_asm,out_asm. Ilmus hĂŒpotees: katkestused ajastaja (noh vĂ”i ĂŒldiselt kĂ”ik katkestused) ei jĂ”ua. Ja tĂ”epoolest, kui vĂ”tta native kogumisest, mis kuidagi töötas, katkestused vĂ€lja keerata, siis saadakse sarnane pilt. Kuid mĂ”istatus osutus hoopis mitte selles: salvestatud jĂ€lgimisvĂ”rkude vĂ”rdlus, mis anti eristatav valikuga, nĂ€itas, et tĂ€itmisrada erineb vĂ€ga varakult. Siinkohal tuleb öelda, et vĂ”rdsustamine, mis on salvestatud kĂ€ivitaja emrun native build debugging output is not really a mechanical process. I don't know exactly how the program running in the browser connects to emrun, but some lines in the output end up swapped, so a difference in the diff is not yet a reason to think the trajectories have diverged. Overall, it became clear that according to the instructions ljmpl the transition occurs through different addresses, and the bytecode generated is fundamentally different: one has a call instruction for a C helper function, while the other does not. After googling the instructions and studying the code that translates these instructions, it became clear that, firstly, a write was made directly to the register cr0 also using a helper — which switches the processor to protected mode, and secondly, the JS version never transitioned to protected mode. The fact is that another feature of Emscripten is its reluctance to accept code like the implementation of the instruction call in TCI, which converts any pointer to a function into the type long long f(int arg0, .. int arg9) — funktsioonid peavad olema kutsutud Ă”igete argumentide arvuga. Kui seda reeglit rikutakse, siis sĂ”ltuvalt silumiseseadetest kas programm kukub kokku (mis on hea) vĂ”i kutsub vĂ€ga vale funktsiooni (mida on kuradi keeruline siluda). On veel kolmas variant — lubada ĂŒmbrikute genereerimist, mis lisavad/viskavad argumendid vĂ€lja, kuid need ĂŒmbrikud vĂ”tavad kokku vĂ€ga palju ruumi, samas kui tegelikult vajan ma vaid natuke rohkem kui sada ĂŒmbrikku. AinuĂŒksi see on ĂŒsna kurb, kuid ilmnes tĂ”sisem probleem: genereeritud funktsioonide ĂŒmbrike koodis konverteeriti argumendid, kuid vahel ei kutsutud funktsiooni genereeritud argumentidega vĂ€lja — just nagu minu rakenduses libffi. See tĂ€hendab, et mĂ”ned abifunktsioonid lihtsalt ei töödanud.

Õnneks on Qemus masinaga loetavad abifunktsioonide loendid, mis on nagu pĂ€isefail, nĂ€iteks

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

Need kasutatakse ĂŒsna lĂ”busalt: kĂ”ige kummalisemal viisil ĂŒlekirjutatakse makrosid DEF_HELPER_n, ja siis lisatakse helper.h. Kuni selleni, et makro avatakse struktuuri initsialiseerijas ja komaga ning seejĂ€rel mÀÀratletakse massiiv, kus kohale on #include <helper.h> LĂ”puks oli aeg proovida kasutada raamatukogu pyparsing, ja kirjutati skript, mis genereerib tĂ€pselt need ĂŒmbrikud ja tĂ€pselt neile funktsioonidele, mis on vajalikud.

Ja nĂŒĂŒd tundub, et protsessor on nagu tööle hakanud. Nagu, kuna ekraan ei initsialiseerunud, kuigi kohaliku versiooniga Ă”nnestus kĂ€ivitada memtest86+. Siinkohal on vaja tĂ€psustada, et Qemu plokisisendi kood on kirjutatud koroutines. Emscriptenil on oma ĂŒsna keeruline rakendus, kuid selle toetamine Qemu koodis on veel vajalik, samas kui protsessorit on vĂ”imalik juba nĂŒĂŒd tĂ”rkeotsinguks kasutada: Qemu toetab valikuid -kernel, -initrd, -append, mille abil saab laadida Linuxi vĂ”i nĂ€iteks memtest86+, kasutamata ĂŒldse plokiseadmeid. Kuid oht: kohaliku versiooniga oli vĂ”imalik jĂ€lgida Linuxi kerneli vĂ€ljundit konsoolile valikuga -nographic, aga brauserist ei olnud mingit vĂ€ljundit terminali, kust see kĂ€ivitati. emrun, ei tulnud. See tĂ€hendab, et pole selge, kas protsessor ei tööta vĂ”i graafika vĂ€ljund. Siis tuli mulle mĂ”te veidi oodata. Selgus, et "protsessor ei maga, vaid lihtsalt vilgub aeglaselt", ja umbes viie minuti pĂ€rast viskas sĂŒdamik konsoolile hunniku teateid ja jĂ€i jĂ€lle kinni. Oli selge, et protsessor töötab, ja tuleb sĂŒveneda SDL2-ga seotud koodidesse. Kahjuks ei oska ma seda teeki kasutada, seetĂ”ttu tuli kohati tegutseda ĂŒle meele. Ühel hetkel vilkus ekraanil rida parallel0 sinisel taustal, mis tekitas mĂ”tteid. LĂ”puks selgus, et asi on selles, et Qemu avab mitu virtuaalset akent ĂŒhes fĂŒĂŒsilises aknas, mille vahel saab vahetada Ctrl-Alt-n kombot kasutades: natiivses versioonis töötab see, Emscriptenis — ei. PĂ€rast ĂŒleliigsete akende kĂ”rvaldamist valikutega -monitor none -parallel none -serial none ja sundides iga kaadriga kogu ekraani uuesti joonistama, töötab kĂ”ik Ă€kitselt.

Koruud

Seega, brauseris emuleerimine toimib, kuid ĂŒhekettalisi mĂ€nge ei saa seal kĂ€ivitada, kuna puudub plokk-sisend- ja -vĂ€ljund — tuleb rakendada korutiinide tuge. Qemu-s on juba mitu korutiinide tausta, kuid JavaScripti ja Emscripteni koodi genereerimise eripĂ€rade tĂ”ttu ei saa lihtsalt vĂ”tta ja alustada stekkide ĆŸongleerimist. Tundub, et "kĂ”ik on kadunud, gips vĂ”etakse maha", kuid Emscripteni arendajad on juba kĂ”ik ette valmistanud. See on ellu viidud ĂŒsna naljakalt: miks mitte nimetada kahtlaseks funktsiooni kutsumist nagu emscripten_sleep ja mitmeid teisi, mis kasutavad Asyncify mehhanismi, samuti kĂ”nesid pöördumisnĂ€idikute kaudu ja kĂ”nesid mis tahes funktsiooni, kus steki all vĂ”ib toimuda ĂŒks kahest eelnevalt mainitud juhtumist. NĂŒĂŒd eraldame iga kahtlase kĂ”ne eel async konteksti ning kohe pĂ€rast kĂ”net kontrollime, kas toimus asĂŒnkroonne kĂ”ne. Kui jah, siis salvestame kĂ”ik lokaalsed muutujad sellesse async konteksti, mÀÀrame Ă€ra, millisele funktsioonile juhtimist edastada, kui teostamist tuleb jĂ€tkata, ja lahkume praegusest funktsioonist. Siin on tĂ”eline ruum efektide uurimiseks tĂŒkeldamiseks — koodi jĂ€tkuva tĂ€itmise vajaduseks genereerib kompilaator "katkiseid" funktsioone, mis algavad pĂ€rast kahtlase kĂ”ne tegemist — nagu niimoodi: kui kahtlasi kĂ”nesid on n, siis funktsioon on jaganenud kuskil n/2 vĂ”rra — ja see on veel enne, kui arvestada, et algsesse funktsiooni tuleb iga potentsiaalselt asĂŒnkroonse kĂ”ne jĂ€rel lisada osa kohalike muutujate salvestamine. Hiljem tuli isegi kirjutada lihtne skript Pythonis, mis antud mitme eriti jagatud funktsiooni puhul, mis eeldatavalt "ei lase asĂŒnkroonsust end lĂ€bi", (see tĂ€hendab, et neis ei toimi virna pööramine ja kĂ”ik see, mida ma just kirjeldasin), nĂ€itab, millistele funktsioonidele nĂ€idatud viidete kaudu tehtavaid kĂ”nesid kompilaator ignoreerida peaks, et andmeid ei kĂ€sitletaks asĂŒnkroonsetena. Sest JS-failid 60 MB — see on juba selgelt liialdus — olgu vĂ€hemalt 30. Kuigi kord seadsin kokkukutsumise skripti ja kogemata viskasin vĂ€lja lingija vĂ”imalused, sealhulgas oli see -O3KĂ€ivitades genereeritud koodi, neelab Chromium mĂ€lu ja kukkub kokku. Vahetasin pĂ€rast juhuslikult sellele, mida ta proovis laadida... Mis ma saan öelda, ma oleksin samuti kĂŒlmunud, kui mul palutaks pĂ”hjalikult uurida ja optimeerida JavaScripti 500+ MB peal.

Kahjuks ei olnud Asyncify toetava koodi kontrollid just kĂ”ige paremini ĂŒhilduvad longjmp-idega, mida kasutati virtuaalse protsessori koodis, kuid pĂ€rast vĂ€ikest plaastrit, mis keelas need kontrollid ja sunni taastama kontekstid nagu kĂ”ik oleks hĂ€sti, töötas kood. Ja siis algas kummaline: mĂ”nikord aktiveerusid sĂŒnkroonimise koodi kontrollid — need, mis lĂ”petavad koodi, kui see peaks logic'i kohaselt lukustuma — keegi proovis haarata juba haaratud muteksti. Õnneks polnud see loogiline probleem serialiseeritud koodis — lihtsalt kasutasin Emscripteni pakutavat tavalist main loop funktsionaalsust, kuid mĂ”nikord pöördus asĂŒnkroonne kutsung tĂ€ielikult tagasi kuhja, ja sel hetkel aktiveerus setTimeout main loop'ist — seega, kood sisenes peamise tsĂŒkli iteratsiooni ilma eelmisest iteratsioonist vĂ€ljumata. Kirjutasin ĂŒmber lĂ”putuks tsĂŒkliks ja emscripten_sleep, ning probleemid muteksitega lakkasid. Kood muutus isegi loogilisemaks — sest pĂ”himĂ”tteliselt ei ole mul mingit koodi, mis valmistab ette jĂ€rgmist animatsiooni kaadrit — lihtsalt protsessor arvutab midagi ja ekraan uuendab end perioodiliselt. Siiski ei saanud probleemid sellega aga otsa: mĂ”nikord lĂ”petas Qemu lihtsalt vaikselt tĂ€itmise ilma mistahes erandite ja vigadeta. Sel hetkel ma sellele ei pööranud tĂ€helepanu, kuid, et ette Ă€ra öelda, probleem seisnes selles: kood korutine, tegelikult ei kasuta setTimeout (noh vĂ”i vĂ€hemalt mitte nii tihti, kui vĂ”iks arvata): funktsioon emscripten_yield lihtsalt seab asĂŒnkroonse kutsumise lipu. Kogu point on selles, et emscripten_coroutine_next ei ole asĂŒnkroonne funktsioon: see kontrollib lippu, lĂ€htestab selle ja suunab juhtimise Ă”igesse kohta. See tĂ€hendab, et virna rullimine lĂ”ppeb sellel. Probleem oli selles, et kasutamata vaba mĂ€lu tĂ”ttu, mis ilmus korutinkogumi vĂ€ljalĂŒlitamisel, kuna ma ei kopeerinud olemasolevast coroutine'i tagakĂŒljest olulist koodirida, funktsioon qemu_in_coroutine tagastas true, kui see tegelikult pidi tagastama false. See viis meetodi kutsumiseni emscripten_yield, mille ĂŒle polnud virnas emscripten_coroutine_next, virna avardumine ulatus otsani, kuid mingeid setTimeout, nagu ma juba ĂŒtlesin, ei olnud seatud.

JavaScripti koodigeneerimine

Ja siin see on, lubatud "liha tagurpidi pööramine". Tegelikult ei ole. Loomulikult, kui kÀivitada Qemu brauseris ja selles Node.js, siis pÀrast Qemu-s koodigeneerimist saame absoluutselt mitte seda JavaScripti. Aga siiski, mingil mÀÀral on tagasi pöördumine.

Alustuseks rÀÀgime natuke sellest, kuidas Qemu töötab. Palun vabandage mind, ma ei ole Qemu professionaalne arendaja ja minu jĂ€reldused vĂ”ivad olla osaliselt valed. Nagu öeldakse: "teise ĂŒliĂ”pilase arvamus ei pea kattuma Ă”ppejĂ”u, Peano aksioomide ja hea mĂ”istusega". Qemu-l on teatud hulk toetatud kĂŒlalisarhitektuure ja igaĂŒhe jaoks on kataloog nagu target-i386. Kogumise kĂ€igus saab nĂ€idata toetust mitmele kĂŒlaliste arhitektuurile, kuid selle tulemusena saadakse lihtsalt mitu binaarset faili. Koodi kĂŒlalisme arhitektuuri toetamiseks genereerib omakorda teatud siseoperatsioonid Qemu, mille TCG (Tiny Code Generator) muudab juba hosti arhitektuuri masinkoodiks. Kuidas on mĂ€rgitud readme-failis, mis asub tcg kataloogis, oli see algselt osa tavalisest C-kompilaatorist, mis hiljem kohandati JIT-i jaoks. SeetĂ”ttu on nĂ€iteks sihtarhitektuur selle dokumendi kontekstis juba mitte kĂŒlaline, vaid hosti arhitektuur. Mingil hetkel ilmus veel ĂŒks komponent — Tiny Code Interpreter (TCI), mis peaks tĂ€itma koodi (praktiliselt samu siseoperatsioone) konkreetses hosti arhitektuuri koodigeneraatori puudumisel. Tegelikult, nagu on mĂ€rgitud tema dokumentatsioonis, ei pruugi see tĂ”lkija alati töötada sama hĂ€sti kui JIT-koodigeneraator, mitte ainult kvantitatiivselt kiiruselt, vaid ka kvalitatiivselt. Kuigi ma ei ole kindel, et tema kirjeldus on tĂ€ielikult ajakohane.

Alguses proovisin luua tĂ€isvÀÀrtuslikku TCG backend'i, kuid segadusse ajas mind kiiresti lĂ€htefailide keerukus ja arusaamatud bytecode'i juhised, mistĂ”ttu otsustasin TCI tĂ”lgendi ĂŒmbritsemise kasuks. See andis mitmeid eeliseid:

  • koodigeneraatori rakendamisel sain tugineda mitte juhistele, vaid tĂ”lgendi koodile
  • funktsioone sai genereerida mitte iga ettejuhtuva tĂ”lkeploki jaoks, vaid nĂ€iteks ainult pĂ€rast saja tĂ€itmise korda
  • kui genereeritud koodi muudetakse (mis nĂ€ib olevat vĂ”imalik, arvestades funktsioone, mille nimedes on sĂ”na 'patch'), pean ma invalideerima genereeritud JS-koodi, kuid vĂ€hemalt on mul midagi, millest seda uuesti genereerida

Kolmanda punkti osas ei ole ma kindel, et patchimine on vÔimalik pÀrast seda, kui kood on esmakordselt tÀidetud, kuid esimesed kaks punkti on piisavad.

Alguses genereeriti kood suure switch'i kujul bytecode'i algse aadressi jĂ€rgi, kuid pĂ€rast Emscripteni artikli meenutamist, genereeritava JS-i optimeerimist ja reloopingut, otsustasin genereerida inimlikuma koodi. EmapĂ€ra jĂ€rgi nĂ€is, et ainus sisendpunkt tĂ”lkeplokki oli selle algus. NĂŒĂŒd, pĂ€rast mĂ”ningast tööd, valmistasin koodigeneraatori, mis genereeris koodi if-idega (kuigi tsĂŒkleid polnud). Siiski tekkis probleem, kood kukkus kokku, tehes teatavaks, et kĂ€sk oli vale pikkusega. Sel ajal oli viimase taseme rekursiooni kĂ€sk brcond. HĂ€sti, lisame identse kontrolli selle juhendi genereerimise juurde enne rekursiivset vĂ€ljakutset ja pĂ€rast seda ja... mitte ĂŒkski neist ei tĂ€itunud, kuid pĂ€rast assert ’iga switchimist kĂ”ik siiski kukkusid. LĂ”puks, uurides genereeritud koodi, sain aru, et pĂ€rast switchi taastatakse viidatud praegusele instruktsoonile virnast ja tĂ”enĂ€oliselt kirjutatakse ĂŒle genereeritava JavaScript koodiga. Nii see oligi. Puhvri suurendamine ĂŒhest megabaitist kĂŒmnele ei aidanud ning selgeks sai, et koodigeneraator joob ringi. Pidin kontrollima, et me ei ĂŒletaks praegust TB-d ja kui me seda teeme, siis anname jĂ€rgmise TB aadressi negatiivse mĂ€rgiga, et saaks jĂ€tkata tĂ€itmist. Lisaks lahendab see probleemi: "milliseid genereeritud funktsioone invalidida, kui see bytecode'i tĂŒkk on muutunud?" — invalidida tuleb ainult see funktsioon, mis vastab sellele translatsiooni plokile. Muide, kuigi ma tĂ”rkeotsingut tegin kĂ”ik Chromiumis (kuna kasutan Firefoxi ja mul on mugavam kasutada eraldi brauserit katsetamiseks), aitas Firefox mul parandada ĂŒhilduvuse probleeme asm.js standardiga, pĂ€rast mida töötas kood Chromiums kiiremini.

NĂ€idatud genereeritud kood

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

KokkuvÔte

Nii et töö pole endiselt lĂ”petatud, aga selle pikaajalise projekti saladuses viimistlemine on mulle tĂŒĂŒtuks muutunud. SeetĂ”ttu otsustasin avaldada senise tulemuse. Kood on kohati kohutav, kuna see on katsetus ja ei ole selge, mida tĂ€pselt teha. Ilmselt peaks hiljem vormistama normaalsed atomaarset commit'id mĂ”ne uuema Qemu versiooni peal. Praegu on olemas haru gitis blogi formaadis: igale vĂ€hemalt kuidagi lĂ€bitud "tasemele" on lisatud ulatuslik kommentaar vene keeles. Sisuliselt on see artikkel mĂ€rkimisvÀÀrses osas - kokkuvĂ”te vĂ€ljundist git log.

Seda kÔike saab proovida siit (ole ettevaatlik, liiklus).

Mis juba praegu töötab:

  • Töötab x86 virtuaalne protsessor
  • On töötav prototĂŒĂŒp JIT-koodigeneraatorist masinkoodist JavaScripti jaoks
  • On ettevalmistus teiste 32-bitiste kĂŒlaliste arhitektuuride kogumiseks: vĂ”ite praegu imetleda Linuxi arhitektuurile MIPS, mis hangub brauseris laadimise etapis

Mida veel teha

  • Emulatsiooni kiirus. Isegi JIT-reĆŸiimis töötab see nĂ€iliselt aeglasemalt kui Virtual x86 (aga potentsiaalselt on olemas terve Qemu suure hulga emuleeritud riistvarade ja arhitektuuridega)
  • Luua normaalne liides — ma ei ole just veebiarendaja, seega olen seni ĂŒmber teinud Emscripteni vaikimisi liidese nii hĂ€sti, kui oskasin
  • Proovida kĂ€ivitada keerulisemaid Qemu funktsioone — vĂ”rk, VM-i rĂ€nne jne.
  • UPD: Pean oma vĂ€hesed saavutused ja veateated Emscripteni ĂŒleminekusse edastama, nagu eelnevad Qemu ja teiste projektide porteerijad tegid. AitĂ€h neile, et mul oli vĂ”imalus nende panust Emscriptenis oma ĂŒlesande raames varjatult kasutada.

Allikas: habr.com

Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster