Qemu.js JIT toe toetamine: hakkliha saab tÔesti tagasi keerata

MĂ”ned aastad tagasi kirjoitti Fabrice Bellard jslinuxist — PC simulaator, mis on kirjutatud JavaScriptis. PĂ€rast seda on veel vĂ€hemalt Virtual x86. Kuid kĂ”ik need, kui ma ei eksi, olid tĂ”lgendajad, samas kui palju varem Fabrice Bellardi kirjutatud Qemu, ja ilmselt iga tĂ€napĂ€evane emulaator, kasutab JIT-kompileerimist kĂŒlalisprotsessi koodist hosti sĂŒsteemi koodiks. Minu arvates oli Ă”ige aeg rakendada tagurpidi ĂŒlesanne, mida veebibrauserid lahendavad: JIT-kompileerimine masinkoodist JavaScripti, mille jaoks tundus kĂ”ige mĂ”istlikum portida Qemu. Tundub, et miks just Qemu, on olemas ka lihtsamaid ja kasutajasĂ”bralikumaid emulaatoreid — nĂ€iteks VirtualBox, mille paigaldad ja see töötab. Aga Qemu-l on mitmeid huvitavaid omadusi

  • avatud lĂ€htekood
  • vĂ”imalus töötada ilma tuumadraiverita
  • vĂ”imalus töötada tĂ”lgendajareĆŸiimis
  • toetab suurt hulka nii host- kui ka kĂŒlalisarhitektuure

Kolmanda punkti osas vĂ”in nĂŒĂŒd selgitada, et tegelikult tĂ”lgendatakse TCI reĆŸiimis mitte kĂŒlaliste masininstruktsioone, vaid neist saadud bytecode'i, aga see ei muuda asja — et Qemu uuel arhitektuuril kokku panna ja kĂ€ivitada, on Ă”nne korral piisav C kompilaator — koodigeneraatori kirjutamine vĂ”ib jÀÀda hilisemaks.

Ja nii, pĂ€rast kaht aastat aeglast kohandamist Qemu lĂ€htekoodiga vabamas ajas, ilmus töötav prototĂŒĂŒp, milles on juba vĂ”imalik nĂ€iteks kĂ€ivitada Kolibri OS-i.

Mis on Emscripten

Meie ajal on ilmunud palju kompilaatoritest, mille lĂ”pptulemus on JavaScript. MĂ”ned, nagu TypeScript, mĂ”eldi algselt parimaks viisiks veebis kirjutamiseks. Samal ajal on Emscripten viis vĂ”tta olemasolev C vĂ”i C++ kood ja kompileerida see vormi, mis on brauserile arusaadav. On sellel lehekĂŒljel kogutud mitmeid tuntud programmide portte: siin, nĂ€iteks vĂ”ib vaadata PyPy-d — muide, nagu vĂ€idetakse, on neil juba JIT. Tegelikult ei saa iga programmi lihtsalt kompileerida ja brauseris kĂ€ivitada — on mitmeid eriti, millega tuleb leppida, nagu ĂŒtleb see leht "Emscripten saab kasutada peaaegu igasuguste portable C/C++ koodi JavaScriptiks. See tĂ€hendab, et on olemas rida operatsioone, mis on standardi jĂ€rgi mÀÀratlemata kĂ€itumine, kuid tavaliselt töötavad x86-l — nĂ€iteks, korraldamata juurdepÀÀs muutujaile, mis mĂ”nel arhitektuuril on tĂ€iesti keelatud. ÜhesĂ”naga, Qemu on platvormidevaheline programm ja, mida sooviksin uskuda, ei sisalda see palju mÀÀratlemata kĂ€itumist — vĂ”ta ja kompileeri, siis veidi vaeva JIT-i ning valmis! Kuid mitte nii kiiresti


Esimene katse

Üldiselt ei ole ma esimene, kellele tuli mĂ”te Qemu JavaScripti portida. ReactOS foorumis esitati kĂŒsimus, kas see on vĂ”imalik teha Emscripteni abil. Varem ilmusid kuulujutud, et selle tegi isiklikult Fabrice Bellard, kuid jutt kĂ€is jslinuxist, mis, niipalju kui mina tean, on katse kĂ€sitsi saavutada JS-is piisavat jĂ”udlust ja on kirjutatud nullist. Hiljem kirjutati Virtual x86 — sellele olid vĂ€lja pandud mitteobfuseeritud lĂ€htekoodid ning vĂ€idetavalt vĂ”imaldas suurem "realistlikkus" emulatsioonis kasutada SeaBIOS-i firmware’ina. Lisaks oli vĂ€hemalt ĂŒks katse portida Qemu Emscripteni abil — seda ĂŒritas teha socketpair, kuid arendamine, niipalju kui ma aru sain, jĂ€eti seisma.

Nii et, nĂ€iliselt on siin lĂ€htekoodid, siin on Emscripten — vĂ”ta ja kompileeri. Kuid on olemas ka teegid, millest Qemu sĂ”ltub, ning teegid, millest need teegid sĂ”ltuvad jne, kusjuures ĂŒheks neist on libffi, millest sĂ”ltub glib. Internetis levisid kuulujutud, et Emscripteni laiendite suures raamatukogus on see olemas, kuid uskusin pigem vaevalt: esiteks, uut kompilaatorit selle jaoks ei eksisteerinud, teiseks, see on liiga madala taseme raamatukogu, et lihtsalt vĂ”tta ja JS-ks kompileerida. Asi ei seisne ainult assembleri sisselĂŒlitustes — tĂ”enĂ€oliselt, kui pingutada, siis mĂ”nele n-ö calling convention'ile on vĂ”imalik ka ilma nendeta vajalikud argumendid stack'i tekitada ja funktsiooni kutsuda. Ainult Emscripten on keeruline asi: selleks, et genereeritud kood nĂ€eks vĂ€lja harjunud JS-mootori optimeerijatele, kasutatakse mitmeid trikke. Eriti nn relooping — koodigeneraator pĂŒĂŒab saadud LLVM IR-i pĂ”hjal, kasutades teatud abstraktseid hĂŒppeinstruktsioone, rekonstrueerida usutavaid if-e, tsĂŒklit jne. Ja kuidas edastatakse argumente funktsiooni? Loomulikult, nagu JS-funktsioonide argumendid, see tĂ€hendab, et vĂ”imaluse korral mitte lĂ€bi stack'i.

Alguses oli mĂ”te lihtsalt kirjutada libffi asendus JS-is ja lasta lĂ€bi tavalised testid, kuid lĂ”ppkokkuvĂ”ttes segasin end segadusse, kuidas muuta oma header-failid, et need töötaks olemasoleva koodiga — mis sa teed, nagu öeldakse, "Kas probleemid on nii keerulised vĂ”i oleme meie nii rumalad". Olin sunnitud portima libffi veel ĂŒhele arhitektuurile, kui nii vĂ”ib öelda — Ă”nneks on Emscriptenis olemas nii makrosid inline assembleri jaoks (JavaScriptis, jah — mis arhitektuur, sellel ka assembler) kui ka vĂ”imalus kĂ€ivitada jooksvalt genereeritud koodi. LĂŒhidalt, pĂ€rast mĂ”ningaid heitlemisi platformi sĂ”ltuvate libffi fragmentidega, sain mingit kompileeritavat koodi ja kĂ€ivitasin selle esimesel ettejuhtuval testil. Minu ĂŒllatuseks lĂ€bis test edukalt. Olles oma geeniusest ĂŒllatunud — ei, polnud nalja, töötas esimesel katsel — vaatasin ikka veel, mitte uskudes oma silmi, uuesti saadud koodi, et hinnata, kuhu edasi kaevuda. Siin sain uuesti ĂŒllatuse — ainus, mida mu funktsioon tegi ffi_call — oli edastada eduka ĂŒleskutsumise teade. Üleskutset endaga ei toimunud. Nii saatsin oma esimese pull request'i, mis parandas arusaadavat iga olĂŒmpia osalejale viga testis — reaalarve ei tohiks vĂ”rrelda nagu a == b ja isegi nagu a - b < EPS — tuleb veel moodul unustada, muidu 0 osutub vĂ€ga isegi 1/3
 ÜhesĂ”naga, ma sain mingisuguse libffi teegi, mis lĂ€bib kĂ”ige lihtsamad testid ja millega compileeritakse glib — otsustasin, et hiljem kirjutan juurde. Kui ette vaadata, siis selgus, et kompilaator ei lisanud isegi lĂ”plikku koodi libffi funktsioonile.

Aga nagu ma juba ĂŒtlesin, on mĂ”ned piirangud ja mitmekesisest mÀÀramatust kĂ€itumisest peitis end ĂŒhtlaselt ebameeldiv joon — JavaScript ei toeta disaini kohaselt samaaegset mĂ€lukasutust. PĂ”himĂ”tteliselt vĂ”ib seda isegi nimetada mĂ”istlikuks ideeks, kuid mitte koodi portimise jaoks, mille arhitektuur tugineb C keele lĂ”imedele. Üldiselt katsetatakse Firefoxis jagatud töötajate toetamisega ning pthreadi rakendus on Emscriptenis olemas, kuid ma ei tahtnud sellest sĂ”ltuda. Pidi vaikselt vĂ€lja kaevama samaaegsuse Qemu koodist — see tĂ€hendab otsima, kus lĂ”ime kĂ€ivitatakse, liigutama selle lĂ”ime töö keera eraldi funktsiooni ning jĂ€rjestikku kutsuma sellised funktsioonid pĂ”hiloopist.

Teine katse

Mingil hetkel sai selgeks, et asi ei edene, ja et sĂŒsteemitunne on vajalik, et koodis ĂŒleliigseid lahendusi mitte rakendada. Tulemus: tuleb kuidagi sĂŒstemaatiliselt korraldada Ă€mbrite lisamise protsess. SeetĂ”ttu vĂ”eti toona uusim versioon 2.4.1 (mitte 2.5.0, sest kes teab, vĂ”ib-olla seal on veel mĂ”ned uue versiooni defektid, ning mul on enda defektidest tĂ€iesti piisavalt), ja esimese asjana kirjutati ohutult ĂŒmber thread-posix.c. Noh, see tĂ€hendab, et ohutult: kui keegi ĂŒritas teostada tegevust, mis viib lukustumiseni, kutsuti kohe vĂ€lja funktsioon abort() — muidugi ei lahendanud see kohe kĂ”iki probleeme, kuid vĂ€hemalt on see kuidagi meeldivam, kui andmete jĂ€rjepidevust vaikides rikkuda.

Üldiselt aitab Emscripteni koodi portimisel vĂ€ga palju valikud -s ASSERTIONS=1 -s SAFE_HEAP=1 — need tuvastavad teatud tĂŒĂŒpi mÀÀramatut kĂ€itumist, nĂ€iteks mittejoondatud aadresside juurde pÀÀsemise (mis ei ole absoluutselt kooskĂ”las kirjutatud massiivide koodiga nagu HEAP32[addr >> 2] = 1) vĂ”i vale arvu argumentidega funktsiooni kutsumise.

Üks asi, millest tasub rÀÀkida, on joondamisvigade probleem. Nagu ma juba ĂŒtlesin, Qemu-l on "degeneratiivne" tĂ”lgendav TCI (tiny code interpreter) koodigeneratsiooni taga ning et Qemu uue arhitektuuri jaoks ĂŒles ehitada ja kĂ€ivitada, piisab, kui on olemas C-kood kompilaator. MĂ€rksĂ”nad "kui on vedanud". Minul ei olnud vedamist ja selgus, et TCI kasutab oma bytecode'i tĂ”lgendamisel mittejoondatud juurdejuurde. See tĂ€hendab, et igasuguste ARM'i ja teiste arhitektuuride puhul, kus on vaja joondatud juurdejuurde, Qemu kompileeritakse, kuna neile on olemas normaalne TCG tagakood, mis genereerib natiivset koodi, kuid kas TCI nendel töötab — see on veel kĂŒsimus. Siiski, nagu selgus, oli TCI dokumentatsioonis midagi sellist selgelt mainitud. LĂ”ppkokkuvĂ”ttes lisati koodi mittejoondatud lugemise funktsioonid, mis avastati Qemu's muust osast.

MĂ€lu purunemine

LĂ”ppkokkuvĂ”ttes parandati TCI mittejoondatud juurdejuur ja tehti peamine tsĂŒkkel, mis jĂ€rjestikuste kutsete kaudu töötas CPU, RCU ja paar vĂ€ikest asja. Ja nĂŒĂŒd, kĂ€ivitades Qemu valikuga -d exec,in_asm,out_asm, mis tĂ€histab, et tuleb öelda, milliseid koodiblokke tĂ€idetakse, ja samuti, tĂ”lkehetkel kirjutada, mis oli kĂŒlalistarkvara kood ja mis hostitarkvara kood on (antud juhul, bytecode). See kĂ€ivitub, teeb paar tĂ”lkeplokki, kirjutab minu jĂ€tatud silumis teadete, et RCU hakkab tööle ja
 kukub abort() funktsiooni sisse. free()Funktsiooni "kaevamise" teel free() Ă”nnestus vĂ€lja selgitada, et mĂ€lu bloki pĂ€ises, mis asub kaheksa bait'i ulatuses eraldatud mĂ€lu ees, leidus block'i suuruse vĂ”i nende laadse info asemel prĂŒgi.

Kuhja hĂ€vitamine — kui armas... Sellises olukorras on kasulik vahend — (kui vĂ”imalik) koguda samadest lĂ€htefailidest natiivne binaarfail ja lasta see Valgrindis lĂ€bi. MĂ”ne aja pĂ€rast oli binaarfail valmis. KĂ€ivitan samade parameetritega — kukub jĂ€lle initsialiseerimisel, olles jĂ”udnud tegelikult teostuseni. Loomulikult on see ebameeldiv — ilmselt ei olnud lĂ€htefailid siiski tĂ€pselt samad, mis pole ĂŒllatav, kuna configure leidis mĂ”ned teised parameetrid, kuid mul on ju Valgrind — kĂ”igepealt fikseerin selle vea ja seejĂ€rel, kui mul vedas, ilmub ka algne. KĂ€ivitan kĂ”ik sama Valgrindis... Kuu-uu, ahh, see kĂ€ivitub, möödub initsialiseerimisest normaalselt ja liigub edasi, mööda algsest veast ilma ainsa mĂ€luaadressi vigade hoiatuseta, rÀÀkimata kukkumistest. Selliseks eluks ei olnud mind ette valmistatud — kukkuv programm lĂ”petab kukkumise Valgrindis kĂ€ivitades. Mis see oli — mĂ”istatus. Minu hĂŒpotees on, et kuna praeguse kĂ€su ĂŒmbruses pĂ€rast kukkumist initsialiseerimisel nĂ€itas gdb tööd. memset-ga kehtiva nĂ€itajaga, kasutades kas mmx, vĂ”i xmm registreid, siis vĂ”is see olla mingi joondusviga, kuigi siiski on raske uskuda.

Okei, Valgrind siin ei nĂ€i abiks olevat. Ja siin hakkas see kĂ”ige ebameeldivam osa — kĂ”ik nagu kĂ€ivitub, aga kukub kokku tĂ€iesti arusaamatutel pĂ”hjustel sĂŒndmuse tĂ”ttu, mis vĂ”is juhtuda miljoneid kĂ€ske tagasi. Pikalt ei olnud isegi aru, kuidas lĂ€heneda. LĂ”puks tuli ikkagi istuda ja debugeerida. Pealkirja sedel, millega see oli ĂŒmber kirjutatud, nĂ€itas, et see ei nĂ€e vĂ€lja nagu arv, vaid pigem mingid binaarsed andmed. Ja, oh ime, see binaarstring leiti BIOSi failist — seega sai nĂŒĂŒd kĂŒllalt kindlalt öelda, et see oli ĂŒlekoormus, ja isegi aru saada, mis sinna puhvri kirjutati. Edasi lĂ€ks nii — Emscriptenis, Ă”nneks, aadressiruumi randomiseerimist pole, ka auke ei ole, seega on vĂ”imalik kirjutada kuskil koodi keskel andmete vĂ€ljundit nĂ€idates eelmisest kĂ€ivitamisest, vaadata andmeid, vaadata nĂ€idikut ja, kui see pole muutunud, saada mĂ”tteainet. TĂ”si, iga muudatuse jĂ€rel kulub linkimisele paar minutit, aga mis sa teed. Tulemuseks leiti konkreetne rida, mis kopeeris BIOSi ajutisest puhvrist kĂŒlastaja mĂ€llu — ja tĂ”epoolest, puhvris ei olnud piisavalt ruumi. Selle kummalise puhveraadressi allika otsimine viis funktsiooni qemu_anon_ram_alloc failis oslib-posix.c — loogika oli selline: mĂ”nikord vĂ”ib olla kasulik joondada aadress huge page'iga, mille suurus on 2 Mb, selleks palume mmap esialgu veidi rohkem ja seejĂ€rel anname ĂŒleliigse tagasi kasutades munmap. Ja kui sellist joondamist ei ole vaja, siis mÀÀrame 2 Mb asemel tulemuse getpagesize() — mmap siiski annab joondatud aadressi... Nii et Emscriptenis mmap lihtsalt kutsub vĂ€lja malloc, ja see ei jaga loomulikult lehekĂŒlge. ÜhesĂ”naga, viga, mis mind paar kuud hĂ€iris, likvideeriti muutusega kaks ridadel.

Funktsioonide kutsumise eripÀra

Ja nĂŒĂŒd juba protsessor midagi arvutab, Qemu ei kaku, aga ekraan ei lĂŒlitu sisse ja protsessor lĂ€heb kiiresti silmusesse, nagu nĂ€htub vĂ€ljundist -d exec,in_asm,out_asm. Ilmnes teooria: taimeri katkestusi ei tule (vĂ”i tĂ”epoolest kĂ”iki katkestusi). Ja tĂ”epoolest, kui eemaldada katkestused natiivse kompileerimise variandist, mis kummalisel kombel töötas, saab sarnase pildi. Kuid saladus ei olnud ĂŒldse selles: vĂ”rdlemine jĂ€lgimistest, mis saadeti ĂŒlaltoodud valikuga, nĂ€itas, et tĂ€itmise trajektoorid lahknevad vĂ€ga varakult. Siinkohal tuleb öelda, et salvestamise vĂ”rdlemine kĂ€ivitusprogrammiga emrun debugimisvĂ€ljastusega natiivse kompileerimise vĂ€ljastusega ei ole sugugi mehaaniline protsess. Ma ei tea tĂ€pselt, kuidas veebibrauseris kĂ€ivitatud programm on ĂŒhendatud emrun, kuid mĂ”ned read vĂ€ljundis on paigutatud teineteisega segamini, seega erinevus diffis ei ole veel pĂ”hjus arvata, et trajektoorid on lahku lĂ€inud. Üldiselt sai selgeks, et juhiste jĂ€rgi ljmpl toimub ĂŒleminek erinevatele aadressidele ja ka bytecode genereeritakse pĂ”himĂ”tteliselt erinevalt: ĂŒhes on juhis C funktsiooni abifunktsiooni kutsumiseks, teises - ei ole. PĂ€rast juhist otsimist ja koodi uurimist, mis neid juhiseid tĂ”lgib, sai selgeks, et esiteks viidi sellele registreerimisega cr0 seotud abifunktsiooni kaudu - mis tĂ”i protsessori kaitstud reĆŸiimi, ja teiseks, et js-versioon ei jĂ”udnud kaitstud reĆŸiimi. Asi on selles, et Emscripteni veel ĂŒks omadus on vastumeelsus taluda koodi nagu kĂ€skluse implementatsioon call TCIs, mis viib iga funktsiooni nĂ€itaja tĂŒĂŒpi long long f(int arg0, .. int arg9) — funktsioone tuleb kutsuda Ă”ige argumendi arvuga. Kui seda reeglit rikutakse, kukub programm sĂ”ltuvalt silumiseseadetest kas alla (mis on hea) vĂ”i kutsub ĂŒles tĂ€iesti vale funktsiooni (mida on kurvalt keeruline siluda). On veel kolmas vĂ”imalus - lubada ĂŒmbriste genereerimist, mis lisavad/eemaldavad argumente, kuid kokkuvĂ”ttes vĂ”tavad need ĂŒmbrised vĂ€ga palju ruumi, arvestades, et mulle on tegelikult vaja vaid natuke ĂŒle saja ĂŒmbrise. Juba see on ĂŒsna kurb, kuid osutus tĂ”sisem probleem: genereeritud koodi funktsioonide ĂŒmbristes muudetud argumendid konverteeriti, kuid genereeritud argumentidega funktsiooni kohati ei kutsutud — tĂ€pselt nagu minu libffi teostuses. See tĂ€hendab, et mĂ”ned abifunktsioonid lihtsalt ei töötanud.

Õnneks on Qemu-l masina loetavaid abimeeste loendeid pealkirjafaili kujul, nagu

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

Neid kasutatakse ĂŒsna lĂ”busalt: kĂ”igepealt ĂŒledefineeritakse makrod kĂ”ige kaootilisemal viisil DEF_HELPER_n, siis lisatakse helper.h. Kuni selle hetkeni, et makro avaldub struktuuri initsialiseerijas ja koma, seejĂ€rel mÀÀratletakse massiiv, mille asemel on #include <helper.h> LĂ”puks tuli vĂ”imalus proovida raamatukogu pyparsing, ja kirjutati skript, mis genereerib tĂ€pselt need mĂ€hised ja ainult nende funktsioonide jaoks, mis on vajalikud.

Ja nii, pĂ€rast seda nĂ€ib protsessor töötavat. NĂ€ib, et ekraan ei initsialiseerunud, kuigi natiivses kogus suudeti kĂ€ivitada memtest86+. Siin tuleks tĂ€psustada, et Qemu blokeerimise seadmete kood on kirjutatud korutine; Emscriptenil on oma ĂŒsna keeruline rakendus, kuid seda tuli veel toetada Qemu koodis, andes vĂ”imaluse protsessorit juba praegu testida: Qemu toetab valikuid -kernel, -initrd, -append, millega saab laadida Linuxi vĂ”i nĂ€iteks memtest86+, kasutamata tegelikult blokeerimise seadmeid. Aga siin oli probleem: natiivses kogus oli vĂ”imalik nĂ€ha Linuxi kernelit konsoolil valiku -nographic, aga brauserist ei tulnud mingit vĂ€ljundit terminali, kust see kĂ€ivitati emrun, ei tulnud. Tundub, et protsessor ei tööta vĂ”i on probleem graafika vĂ€ljundis. Ja siis tuli mul meelde natuke oodata. Selgus, et "protsessor ei maga, vaid lihtsalt vilgub aeglaselt", ja umbes viie minuti pĂ€rast viskas tuum konsoolile rida teateid ja jĂ€tkas edasist hangumist. Selgeks sai, et protsessor toimib ĂŒldiselt, ja tuleb otsida SDL2 koodi. Kahjuks ei oska ma seda raamatukogu kasutada, nii et kohati tuli tegutseda Ă”nne kĂ”ige pĂ”hjalikumal viisil. Ühes hetkes vilkus ekraanil rida parallel0 sinisel taustal, mis mĂ”jus teatud mĂ”tetena. LĂ”puks osutus probleemiks, et Qemu avab mitmeid virtuaalseid aknaid ĂŒhe fĂŒĂŒsilise akna sees, mille vahel saab vahetada Ctrl-Alt-n: natiivses koguses töötab see, Emscriptenis – mitte. PĂ€rast mittevajalike akende eemaldamist valikute abil -monitor none -parallel none -serial none ja sundides kogu ekraani iga kaadriga ĂŒmber joonistama, toimus kĂ”ik jĂ€rsku.

Korutine

Nii, brauseri emulatsioon töötab, kuid midagi huvitavat ĂŒhe ketta pealt ei saa kĂ€ivitada, kuna ei ole plokk sisend-vĂ€ljundit — vajalik on toetada koruutine. Qemu-s on juba mĂ”ned koruutine tagaplaanid olemas, kuid JavaScripti ja Emscripteni koodigeneraatori eripĂ€ra tĂ”ttu ei saa lihtsalt vĂ”tta ja hakata kuhjama stekke. Tundub, et "kĂ”ik on kadunud, kips vĂ”etakse maha", kuid Emscripteni arendajad on juba kĂ”igest hoolitsenud. Selle rakendamine on ĂŒsna mĂ€rkamatult: mis oleks, kui nimetaksime kahtlaseks funktsiooni vĂ€ljakutset nagu emscripten_sleep ja mĂ”ned teised, mis kasutavad Asyncify mehhanismi, samuti vĂ€ljakutsed.Pointeri kaudu ja vĂ€ljakutsed igasuguste funktsioonide puhul, kus allpool stekis vĂ”ib esineda ĂŒks kahest eelnevast olukorrast. Ja nĂŒĂŒd eraldame iga kahtlase vĂ€ljakutse korral asĂŒnkroonse konteksti, ning kohe pĂ€rast vĂ€ljakutset kontrollime, kas toimus asĂŒnkroonne vĂ€lja kutse, ja kui toimus, salvestame kĂ”ik lokaalsed muutujad sellesse asĂŒnkroonsesse konteksti, mÀÀrame, millise funktsiooni juurde tuleb edastada juhtimine, kui tuleb jĂ€tkata tĂ€itmist, ning lahkume praegusest funktsioonist. Siin on ruumi uurida efekti tĂŒkeldamist — vajaduseks koodi jĂ€tkamiseks pĂ€rast asĂŒnkroonsest vĂ€ljakutsest tagasiminekut genereerib kompilaator "lĂ”ike" funktsiooni, mis algavad pĂ€rast kahtlast vĂ€ljakutset — nii: kui on n kahtlast vĂ€ljakutset, siis funktsioon lĂ”hustatakse kuskil n/2 vĂ”rra — ja see on veel, kui mitte arvestada, et algsele funktsioonile tuleb pĂ€rast iga potentsiaalselt asĂŒnkroonset vĂ€ljakutset lisada osa lokaalsest muutujast. Hiljem tuli isegi kirjutada lihtne skript Pythoni keeles, mis antud kogumi eriti tĂŒkeldatud funktsioonide pĂ”hjal, mis eeldatavalt "ei luba asĂŒnkroonsust enda kaudu" (s.o. nendes ei toimu steki kerimist ja kĂ”ik see, mida ma just kirjeldasin), osutab, milliseid vĂ€ljakutseid lĂ€bi indikaatorite mĂ”nedes funktsioonides tuleb kompilaatoril ignoreerida, et neid funktsioone ei kĂ€sitletaks nagu asĂŒnkroonseid. Sest 60 MB JS-failid on juba ilmselgelt ĂŒle mĂ”istuse — olgu vĂ€hemalt 30. Kuigi, kord seadsin ĂŒles kogumisk skripti ja juhuslikult viskasin vĂ€lja linkimise valikud, mille seas oli ka -O3. KĂ€ivitan genereeritud koodi ja Chromium tarbib mĂ€lu ning kukub kokku. Hiljem juhuslikult vaatasin, mida ta proovis laadida... Noh, mida ma oskan öelda, ma jÀÀks ka seisma, kui mind palutaks pĂ”hjalikult uurida ja optimeerida JavaScripti 500+ MB ulatuses.

Kahjuks ei olnud Asyncify toe teegi koodis kontrollid eriti ĂŒhilduvad longjmp-dega, mida kasutatakse virtuaalse protsessori koodis, kuid pĂ€rast vĂ€ikest puhasti, mis need kontrollid vĂ€lja lĂŒlitas ja sunniviisiliselt taastada kontekstid nagu kĂ”ik oleks hĂ€sti, kood töötas. Siis hakkas midagi kummalist juhtuma: mĂ”nikord aktiveerusid sĂŒnkroniseerimise koodi kontrollid - need, mis lĂ”petavad koodi, kui selle tĂ€itmise loogika jĂ€rgi peaks see blokeeruma - keegi ĂŒritas haarata juba haaratud mutexi. Õnneks ei olnud see loogiline probleem seeriakoodis - lihtsalt kasutasin Emscripteni pakkumist, milleks on peamine tsĂŒkkel, kuid mĂ”nikord asĂŒnkroonne kutsumine voldis mĂ€lu tĂ€ielikult lahti ja sel hetkel aktiveerus setTimeout peamisest tsĂŒklist - seega sattus kood peamise tsĂŒkli iteratsioonile, ilma et oleks eelmisest iteratsioonist vĂ€ljunud. Kirjutasin ĂŒmber lĂ”pmisse tsĂŒklisse ja emscripten_sleep, ja probleemid mutexidega lĂ”ppesid. Kood muutus isegi loogilisemaks - sest pĂ”himĂ”tteliselt ei ole mul mingit koodi, mis valmistab ette uue animatsiooni kaadri - lihtsalt protsessor arvutab midagi ja ekraan vĂ€rskendatakse perioodiliselt. Kuid probleemid sellega ei lĂ”ppenud: mĂ”nikord lĂ”petas Qemu tĂ€itmise lihtsalt vaikselt, ilma igasuguste erandite ja vigadeta. Sel hetkel ei viitsinud ma sellega tegeleda, kuid edasi vaadates ĂŒtlen, et probleem oli jĂ€rgmine: koroutine kood ei kasutanud tegelikult setTimeout (noh vĂ”i vĂ€hemalt ei kasutanud nii sageli, kui vĂ”iks arvata): funktsioon emscripten_yield lihtsalt seab asĂŒnkroonse kutsumise lippu. Kogu asi on selles, et emscripten_coroutine_next ei ole asĂŒnkroonne funktsioon: sees ta kontrollib lippu, lĂ€htestab selle ja edastab juhtimise sinna, kuhu see vajalik. See tĂ€hendab, et tema juures lĂ”ppeb mĂ€lu pööramine. Probleem oli selles, et „use-after-free“ tĂ”ttu, mis ilmnes koroutine pooled vĂ€lja lĂŒlitamisel, kuna ma ei kopeerinud vajalikku koodireadet olemasolevast coroutine backendist, funktsioon qemu_in_coroutine tagastas true, kui tegelikult oleks pidanud tagastama false. See viis kutsumiseni emscripten_yield, mille ĂŒlemineku kohal, ei olnud emscripten_coroutine_next, virn ulatus kĂ”ige kĂ”rgemale, kuid mingeid setTimeout, nagu ma juba ĂŒtlesin, ei olnud seadistatud.

JavaScripti koodigeneerimine

Ja siin on, lubatud "liha tagasi pööramine". Tegelikult ei ole. Loomulikult, kui kÀitada brauseris Qemu ja seal - Node.js, siis pÀrast koodigeneerimist Qemus saame kindlasti hoopis teistsuguse JavaScripti. Aga siiski, mingisugune tagasipööramine on olemas.

Esmalt natuke sellest, kuidas Qemu töötab. Palun vabandage mind kohe: ma ei ole Qemu professionaalne arendaja ja minu jĂ€reldused vĂ”ivad olla kohati ekslikud. Nagu öeldakse, "ĂŒliĂ”pilase arvamus ei pea kokku langema Ă”petaja, Peano aksioomide ja ĂŒhiskondliku mĂ”istusega". Qemu-l on teatud hulk toetatud kĂŒlgarhitektuure ja igaĂŒhe jaoks on kataloog, nagu target-i386. Koostamisel saab mÀÀrata mitme kĂŒlgarhitektuuri toetuse, kuid tulemuseks on lihtsalt mitu binaarfaili. Kood kĂŒlgarhitektuuri toetamiseks genereerib omakorda mĂ”ned Qemu sisemised toimingud, mille TCG (Tiny Code Generator) juba muundab hostiarhitektuuri masinkoodiks. Nagu vĂ€idetakse tcg kataloogis asuvas readme-failis, oli see algselt osa tavalise C-kompilaatori tööst, mida hiljem kohandati JIT-i jaoks. Seega, nĂ€iteks, sihtharhitektuur selle dokumendi terminites - ei ole enam kĂŒlgharhitektuur, vaid hostiarhitektuur. Ühel hetkel ilmus veel ĂŒks komponent - Tiny Code Interpreter (TCI), mis peaks tĂ€itma koodi (peaaegu samu sisemisi toiminguid) ilma konkreetse hostiarhitektuuri koodigeneerijata. Tegelikult, nagu on öeldud tema dokumentatsioonis, ei pruugi see tĂ”lgendaja alati töötada sama hĂ€sti kui JIT-koodigeneerija mitte ainult kvantitatiivselt kiiruselt, vaid ka kvalitatiivselt. Kuigi ma ei ole kindel, et tema kirjeldus on tĂ€ielikult ajakohane.

Alguses ĂŒritasin luua tĂ€isvÀÀrtuslikku TCG tagavara, kuid segadusse sattusin kiiresti lĂ€htekoodides ja mitte eriti selges baytkoodi kĂ€skude kirjelduses, seetĂ”ttu otsustasin TCI tĂ”lgendajat ĂŒmber kirjutada. See andis koheselt mitmeid eeliseid:

  • koodigeneerija rakendamisel sai vaadata mitte kĂ€skude kirjeldust, vaid tĂ”lgendaja koodi
  • Saab genereerida funktsioone mitte iga kohatud tĂ”lkeploki jaoks, vaid nĂ€iteks alles pĂ€rast utsesandat 100. tĂ€itmist.
  • Kui genereeritud koodis midagi muutub (mis nĂ€ib olevat vĂ”imalik, arvestades funktsioone, mille nimedes on sĂ”na patch), pean invalideerima genereeritud JS-koodi, kuid vĂ€hemalt on mul vĂ”imalus seda uuesti genereerida.

Kolmanda punkti osas ei ole ma kindel, et patchimine on vÔimalik pÀrast koodi esmakordset tÀitmist, kuid esimesed kaks punkti on piisavad.

Alguses genereeriti kood suurena switch-lausesse, mis viitas lĂ€htestatud kĂ€su byte'ile, kuid hiljem, meenutades artiklit Emscripteni kohta, genereeritava JS-i optimeerimist ja reloopingut, otsustasin genereerida inimeste jaoks arusaadavam kood, eriti kuna praktiliselt selgus, et ainus sissepÀÀs tĂ”lkeplokki on selle algus. Nii ka tehti, mĂ”ne aja pĂ€rast sai valmis koodigeneraator, mis genereeris koodi if-idega (kuigi ilma tsĂŒkliteta). Kuid hĂ€da on selles, et see kukkus, andes teada, et kĂ€sk osutus mingi vale pikkusega. Samal ajal oli see viimane kĂ€sk selle rekurssiooni tasemel. brcond. HĂ€sti, lisan identse kontrolli selle kĂ€su genereerimisse enne rekurssiooni kutsumist ja pĂ€rast seda ja... mitte ĂŒkski neist ei toiminud, kuid pĂ€rast assert'i switch'i jĂ€rel kukkusid siiski. LĂ”puks, analĂŒĂŒsides genereeritud koodi, sain aru, et pĂ€rast switch'i laaditakse viit praegusele kĂ€sule uuesti virnast ja seda tĂ”enĂ€oliselt katab genereeritud JavaScript-kood. Nii see oligi. Buffri suurendamine ĂŒhest megabaitist kĂŒmne peale ei aidanud midagi ja sai selgeks, et koodigeneraator jookseb ringi. Pidin kontrollima, et me ei ĂŒletaks hetke TB piire ja kui me ĂŒletasime, siis andma jĂ€rgmise TB aadressi miinuse mĂ€rgiga, et saaksime tĂ€itmist jĂ€tkata. Pealegi lahendab see probleemi, "milliseid genereeritud funktsioone invalidida, kui see tĂŒkk byte'ist muutus?" - tuleb invalidida ainult see funktsioon, mis vastab sellele tĂ”lkeplokile. Muide, kuigi ma debugisin kĂ”ike Chromes (kuna kasutan Firefoxi ja mulle on mugavam eksperimenteerida eraldi brauseris), aitas Firefox mul parandada vastuolus asm.js standardiga, pĂ€rast mida töötas kood Chromes sujuvamalt.

NĂ€idis 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 veel lĂ”pule viidud, kuid selle pikaleveninud projekti viimistlemine on mind salaja vĂ€sitama hakanud. SeetĂ”ttu otsustasin praegu avaldada selle, mis on. Kood on kohati hirmutav, kuna tegemist on katsetusega ega ole ette teada, mida tĂ€pselt teha. TĂ”enĂ€oliselt tuleks hiljem luua normaalsed aatomÀÀrangud ĂŒhelt moodsamalt Qemu versioonilt. Praegu on olemas git-i haru, mis on formaadis blogi: iga „taseme“ kohta, mis on kuidagi lĂ€bitud, on lisatud ulatuslik kommentaar eesti keeles. VÀÀrib mĂ€rkimist, et see artikkel on suuresti tagasiviidud vĂ€ljavĂ”tte kirjeldus. git log.

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

Mis juba praegu töötab:

  • Töötab virtuaalne x86 protsessor
  • On töötav prototĂŒĂŒp JIT-koodigeneraatorist masinkoodist JavaScripti
  • On ettevalmistus teiste 32-bitiste kĂŒlalisarhitektuuride kogumiseks: saate praegu imetleda, kuidas Linux MIPS arhitektuuril, laadimise etapis, brauseris hangub.

Mis veel teha vÔiks

  • Emulatsiooni kiiremaks muuta. Isegi JIT-reĆŸiimis töötab see nĂ€iliselt aeglasemalt kui Virtual x86 (aga potentsiaalselt on peal Qemu, millel on suur hulk emuleeritud riistvara ja arhitektuure).
  • Luua normaalne liides — veebiarendaja olen ma, otsekui, nii-ehk-na, seetĂ”ttu muutsin seni vaid Emscripteni standardset liidest, nii nagu oskasin.
  • Proovida kĂ€ivitada keerukamaid Qemu funktsioone — vĂ”rk, VM migratsioon jne.
  • UPD: Pean saatma Emscripteni oma ettevĂ”e töötulemused ja vead ĂŒlesanne, nagu seda tegid varasemad Qemu ja teiste projektide porteerijad. TĂ€nan neid vĂ”imaluse eest kasutada nende panust Emscriptenisse oma ĂŒlesande raames.

Allikas: habr.com

Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid | ProHoster