Qemu.js ar JIT atbalstu: jūs joprojām varat pagriezt malto gaļu atpakaļ

Pirms dažiem gadiem Fabriss Belards raksta jslinux ir datora emulators, kas rakstÄ«ts JavaScript. Pēc tam bija vismaz vairāk Virtuālais x86. Bet visi, cik es zinu, bija tulki, savukārt Qemu, ko daudz agrāk sarakstÄ«jis tas pats Fabriss Belards, un, iespējams, jebkurÅ” sevi cienoÅ”s mÅ«sdienu emulators izmanto JIT viesa koda kompilāciju resursdatora sistēmas kodā. Man Ŕķita, ka ir pienācis laiks Ä«stenot pretēju uzdevumu saistÄ«bā ar to, ko risina pārlÅ«kprogrammas: JIT maŔīnkoda kompilācija JavaScript, kam visloÄ£iskāk Ŕķita portēt Qemu. Å Ä·iet, kāpēc Qemu, ir vienkārŔāki un lietotājam draudzÄ«gāki emulatori - piemēram, tas pats VirtualBox - ir instalēti un darbojas. Bet Qemu ir vairākas interesantas funkcijas

  • atvērtais avots
  • spēja strādāt bez kodola draivera
  • spēja strādāt tulka režīmā
  • atbalsts lielam skaitam resursdatora un viesu arhitektÅ«ru

AttiecÄ«bā uz treÅ”o punktu tagad varu paskaidrot, ka patiesÄ«bā TCI režīmā tiek interpretētas nevis paÅ”as viesu maŔīnas instrukcijas, bet gan no tām iegÅ«tais baitkods, taču tas nemaina bÅ«tÄ«bu - lai izveidotu un palaistu. Qemu uz jaunas arhitektÅ«ras, ja paveicas, pietiek ar C kompilatoru - kodu Ä£eneratora rakstÄ«Å”anu var atlikt.

Un tagad pēc divu gadu nesteidzÄ«gas čaloÅ”anās ar Qemu pirmkodu brÄ«vajā laikā parādÄ«jās strādājoÅ”s prototips, kurā jau var palaist, piemēram, Kolibri OS.

Kas ir Emscripten

MÅ«sdienās ir parādÄ«juÅ”ies daudzi kompilatori, kuru gala rezultāts ir JavaScript. Daži, piemēram, Type Script, sākotnēji bija paredzēti kā labākais veids, kā rakstÄ«t tÄ«meklÄ«. Tajā paŔā laikā Emscripten ir veids, kā izmantot esoÅ”o C vai C++ kodu un apkopot to pārlÅ«kprogrammā lasāmā formā. Ieslēgts Ŕī lapa Mēs esam apkopojuÅ”i daudzas plaÅ”i pazÄ«stamu programmu ostas: Å”eitPiemēram, varat apskatÄ«t PyPy - starp citu, viņi apgalvo, ka viņiem jau ir JIT. Faktiski ne katru programmu var vienkārÅ”i apkopot un palaist pārlÅ«kprogrammā - ir vairāki Iespējas, ar ko gan nākas samierināties, jo tajā paŔā lappusē uzrakstÄ«ts ā€œEmscripten var sastādÄ«t gandrÄ«z jebkuru portatÄ«vs C/C++ kods uz JavaScript". Tas nozÄ«mē, ka ir vairākas darbÄ«bas, kas saskaņā ar standartu ir nedefinētas, bet parasti darbojas x86 ā€” piemēram, nesaskaņota piekļuve mainÄ«gajiem, kas dažās arhitektÅ«rās parasti ir aizliegta. Kopumā , Qemu ir starpplatformu programma un , es gribēju ticēt, un tajā jau nav daudz nenoteiktas uzvedÄ«bas ā€” ņemiet to un kompilējiet, tad nedaudz padarbojieties ar JIT ā€” un esat pabeidzis! Bet tas nav gadÄ«jums...

Pirmais mēģinājums

VispārÄ«gi runājot, es neesmu pirmā persona, kas nākusi klajā ar ideju par Qemu pārneÅ”anu uz JavaScript. ReactOS forumā tika uzdots jautājums, vai tas ir iespējams, izmantojot Emscripten. Jau agrāk klÄ«da baumas, ka Fabriss Belards to izdarÄ«jis personÄ«gi, bet mēs runājām par jslinux, kas, cik man zināms, ir tikai mēģinājums manuāli panākt pietiekamu veiktspēju JS, un tika rakstÄ«ts no nulles. Vēlāk tika uzrakstÄ«ts Virtual x86 - tam tika ievietoti neapslēpti avoti, un, kā teikts, lielākais emulācijas ā€œreālismsā€ ļāva izmantot SeaBIOS kā programmaparatÅ«ru. Turklāt bija vismaz viens mēģinājums portēt Qemu, izmantojot Emscripten - es mēģināju to izdarÄ«t ligzdu pāris, bet attÄ«stÄ«ba, cik saprotu, bija iesaldēta.

Tātad, Ŕķiet, Å”eit ir avoti, Å”eit ir Emscripten - ņemiet to un apkopojiet. Bet ir arÄ« bibliotēkas, no kurām ir atkarÄ«gs Qemu, un bibliotēkas, no kurām ir atkarÄ«gas Ŕīs bibliotēkas utt., un viena no tām ir libffi, no kura glib ir atkarÄ«gs. Internetā klÄ«da baumas, ka lielajā Emscripten bibliotēku portu kolekcijā tāds ir, taču tam kaut kā bija grÅ«ti noticēt: pirmkārt, tas nebija paredzēts kā jauns kompilators, otrkārt, pārāk zema lÄ«meņa bibliotēku, lai vienkārÅ”i paņemtu un apkopotu JS. Un tas nav tikai jautājums par montāžas ieliktņiem - iespējams, ja to pagriežat, dažām izsaukÅ”anas konvencijām varat Ä£enerēt nepiecieÅ”amos argumentus stekā un izsaukt funkciju bez tiem. Bet Emscripten ir viltÄ«ga lieta: lai Ä£enerētais kods izskatÄ«tos pazÄ«stams pārlÅ«kprogrammas JS dzinēja optimizētājam, tiek izmantoti daži triki. Jo Ä«paÅ”i tā sauktais relooping - kodu Ä£enerators, izmantojot saņemto LLVM IR ar dažām abstraktām pārejas instrukcijām, mēģina atjaunot ticamus ifs, cilpas utt. Nu, kā argumenti tiek nodoti funkcijai? Protams, kā argumentus JS funkcijām, tas ir, ja iespējams, ne caur steku.

Sākumā bija doma vienkārÅ”i uzrakstÄ«t libffi nomaiņu ar JS un palaist standarta testus, bet beigās samulsu, kā savus header failus uztaisÄ«t tā, lai tie strādātu ar esoÅ”o kodu - ko darÄ«t? kā saka: "Vai uzdevumi ir tik sarežģīti "Vai mēs esam tik stulbi?" Man bija jāportē libffi uz citu arhitektÅ«ru, tā teikt - par laimi Emscripten ir gan makro inline montāžai (Javascript, jā - nu, lai kāda arhitektÅ«ra, tātad montētājs), gan iespēja palaist lidojumā Ä£enerētu kodu. Kopumā, kādu laiku čakarējot ar platformas atkarÄ«giem libffi fragmentiem, es ieguvu kompilējamu kodu un palaistÄ«ju to pirmajā testā, ar kuru saskāros. Man par pārsteigumu tests bija veiksmÄ«gs. Apdullināts no sava Ä£enialitātes - ne pa jokam, tas nostrādāja jau no pirmās palaiÅ”anas - es, joprojām neticot savām acÄ«m, devos vēlreiz apskatÄ«t iegÅ«to kodu, lai izvērtētu, kur tālāk rakt. Å eit es sajuku prātā otro reizi - vienÄ«gais, ko darÄ«ja mana funkcija, bija ffi_call - tas ziņoja par veiksmÄ«gu zvanu. PaÅ”a zvana nebija. Tāpēc es nosÅ«tÄ«ju savu pirmo vilkÅ”anas pieprasÄ«jumu, kurā tika izlabota kļūda testā, kas ir skaidra jebkuram olimpiādes skolēnam - reālos skaitļus nevajadzētu salÄ«dzināt kā a == b un pat kā a - b < EPS - jāatceras arÄ« modulis, pretējā gadÄ«jumā 0 izrādÄ«sies ļoti vienāds ar 1/3... Vispār es izdomāju noteiktu libffi portu, kas iztur visvienkārŔākos testus un ar kuru ir glibs sastādÄ«ts - nolēmu, ka vajadzēs, pievienoÅ”u vēlāk. Raugoties uz priekÅ”u, teikÅ”u, ka, kā izrādÄ«jās, kompilators pat neiekļāva libffi funkciju gala kodā.

Bet, kā jau teicu, ir daži ierobežojumi, un starp dažādu nedefinētu uzvedÄ«bu brÄ«vu izmantoÅ”anu ir paslēpta vēl nepatÄ«kamāka iezÄ«me - JavaScript pēc dizaina neatbalsta daudzpavedienu izmantoÅ”anu ar koplietojamo atmiņu. Principā to parasti var pat saukt par labu ideju, bet ne koda pārneÅ”anai, kura arhitektÅ«ra ir saistÄ«ta ar C pavedieniem. VispārÄ«gi runājot, Firefox eksperimentē ar koplietojamo darbinieku atbalstu, un Emscripten viņiem ir pthread ievieÅ”ana, taču es negribēju no tā paļauties. Man vajadzēja lēnām izskaust no Qemu koda multithreading - tas ir, noskaidrot, kur pavedieni darbojas, pārvietot Å”ajā pavedienā esoŔās cilpas pamattekstu atseviŔķā funkcijā un izsaukt Ŕādas funkcijas pa vienai no galvenās cilpas.

Otrais mēģinājums

Kādā brÄ«dÄ« kļuva skaidrs, ka problēma joprojām pastāv un ka nejauÅ”a kruÄ·u grÅ«stÄ«Å”ana ap kodu neko labu nenovedÄ«s. Secinājums: mums kaut kā jāsistematizē kruÄ·u pievienoÅ”anas process. Tāpēc tika paņemta 2.4.1 versija, kas tobrÄ«d bija svaiga (nevis 2.5.0, jo, kas zina, jaunajā versijā bÅ«s bugs, kas vēl nav noÄ·erts, un man pietiek ar savām kļūdām ), un pirmā lieta bija to droÅ”i pārrakstÄ«t thread-posix.c. Nu, tas ir, kā droÅ”i: ja kāds mēģināja veikt darbÄ«bu, kas noveda pie bloÄ·Ä“Å”anas, funkcija nekavējoties tika izsaukta abort() - tas, protams, neatrisināja visas problēmas uzreiz, bet tas vismaz bija kaut kā patÄ«kamāk nekā klusi saņemt pretrunÄ«gus datus.

Kopumā Emscripten opcijas ir ļoti noderÄ«gas koda pārneÅ”anai uz JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - tie uztver dažus nenoteiktas darbÄ«bas veidus, piemēram, izsaukumus uz nesaskaņotu adresi (kas nepavisam neatbilst drukāto masÄ«vu kodam, piemēram, HEAP32[addr >> 2] = 1) vai funkcijas izsaukÅ”ana ar nepareizu argumentu skaitu.

Starp citu, izlÄ«dzināŔanas kļūdas ir atseviŔķa problēma. Kā jau teicu, Qemu ir ā€œdeÄ£enerētaā€ interpretācijas aizmugure koda Ä£enerÄ“Å”anai TCI (tiny code interpreter), un, lai izveidotu un palaistu Qemu uz jaunas arhitektÅ«ras, ja jums paveicas, pietiek ar C kompilatoru. Atslēgvārdi "ja tev paveicas". Man nepaveicās, un izrādÄ«jās, ka TCI izmanto nesaskaņotu piekļuvi, parsējot savu baitkodu. Tas ir, uz visu veidu ARM un citām arhitektÅ«rām ar obligāti izlÄ«dzinātu piekļuvi, Qemu kompilē, jo tiem ir parasta TCG aizmugure, kas Ä£enerē vietējo kodu, bet vai TCI darbosies uz tiem, tas ir cits jautājums. Tomēr, kā izrādÄ«jās, TCI dokumentācija skaidri norādÄ«ja uz kaut ko lÄ«dzÄ«gu. Rezultātā kodam tika pievienoti funkciju izsaukumi nesaskaņotai lasÄ«Å”anai, kas tika atrasti citā Qemu daļā.

Kaudzes iznīcināŔana

Rezultātā tika izlabota nesaskaņota piekļuve TCI, tika izveidota galvenā cilpa, kas savukārt sauca procesoru, RCU un dažas citas mazas lietas. Un tāpēc es palaidu Qemu ar opciju -d exec,in_asm,out_asm, kas nozÄ«mē, ka jums ir jāpasaka, kuri koda bloki tiek izpildÄ«ti, kā arÄ« apraides laikā jāuzraksta, kāds bija viesa kods, par ko kļuva resursdatora kods (Å”ajā gadÄ«jumā baitkods). Tas sākas, izpilda vairākus tulkoÅ”anas blokus, raksta atkļūdoÅ”anas ziņojumu, ko atstāju, ka RCU tagad sāksies un... avarē abort() funkcijas iekÅ”pusē free(). Pielāgojot funkciju free() Mums izdevās noskaidrot, ka kaudzes bloka galvenē, kas atrodas astoņos baitos pirms pieŔķirtās atmiņas, bloka lieluma vai tamlÄ«dzÄ«ga vietā atradās atkritumi.

KaudzÄ«tes iznÄ«cināŔana - cik mīļi... Tādā gadÄ«jumā ir noderÄ«gs lÄ«dzeklis - no (ja iespējams) no tiem paÅ”iem avotiem samontē native bināru un palaist zem Valgrind. Pēc kāda laika binārais fails bija gatavs. Es to palaižu ar tādām paŔām opcijām - tas avarē pat inicializācijas laikā, pirms faktiski sasniedz izpildi. Tas, protams, ir nepatÄ«kami - acÄ«mredzot avoti nebija gluži tie paÅ”i, kas nav pārsteidzoÅ”i, jo konfigurÄ“Å”ana izpētÄ«ja nedaudz atŔķirÄ«gas iespējas, bet man ir Valgrind - vispirms izlaboÅ”u Å”o kļūdu un pēc tam, ja paveiksies , parādÄ«sies oriÄ£inālais. Es palaižu to paÅ”u zem Valgrind... Y-y-y, y-y-y, uh-uh, tas sākās, normāli tika inicializēts un tika novērsta sākotnējā kļūda, bez brÄ«dinājuma par nepareizu piekļuvi atmiņai, nemaz nerunājot par kritieniem. DzÄ«ve, kā saka, mani tam nesagatavoja - avaroÅ”a programma pārstāj avarēt, kad tā tiek palaista zem Walgrind. Kas tas bija, ir noslēpums. Mana hipotēze ir tāda, ka vienu reizi paÅ”reizējās instrukcijas tuvumā pēc avārijas inicializācijas laikā gdb parādÄ«ja darbu memset-a ar derÄ«gu rādÄ«tāju, izmantojot vai nu mmx, vai xmm reÄ£istros, tad, iespējams, tā bija sava veida izlÄ«dzināŔanas kļūda, lai gan joprojām ir grÅ«ti noticēt.

Labi, Valgrinds Å”eit nepalÄ«dz. Un te sākās pats pretÄ«gākais - Ŕķiet, ka viss pat sākas, bet pilnÄ«gi nezināmu iemeslu dēļ avarē notikums, kas varēja notikt pirms miljoniem instrukciju. Ilgu laiku pat nebija skaidrs, kā pieiet. Beigās tomēr nācās apsēsties un atkļūdot. Izdrukājot to, ar ko virsraksts tika pārrakstÄ«ts, izrādÄ«jās, ka tas neizskatÄ«jās pēc skaitļa, bet gan pēc kaut kādiem bināriem datiem. Un, lÅ«k, Ŕī binārā virkne tika atrasta BIOS failā - tas ir, tagad varēja ar pamatotu pārliecÄ«bu teikt, ka tā ir bufera pārpilde, un ir pat skaidrs, ka tā tika ierakstÄ«ta Å”ajā buferÄ«. Nu, tad kaut kas lÄ«dzÄ«gs Å”im - Emscripten, par laimi, nav adreÅ”u telpas randomizācijas, tajā arÄ« nav nekādu caurumu, tāpēc var ierakstÄ«t kaut kur koda vidÅ«, lai izvadÄ«tu datus pēc rādÄ«tāja no pēdējā palaiÅ”anas, apskatiet datus, skatiet rādÄ«tāju un, ja tas nav mainÄ«jies, iegÅ«stiet vielu pārdomām. Tiesa, saiÅ”u izveide pēc jebkādām izmaiņām aizņem pāris minÅ«tes, bet ko jÅ«s varat darÄ«t? Rezultātā tika atrasta Ä«paÅ”a rinda, kas kopēja BIOS no pagaidu bufera uz viesa atmiņu - un, patieŔām, buferÄ« nebija pietiekami daudz vietas. Atrodot Ŕīs dÄ«vainās bufera adreses avotu, tika izveidota funkcija qemu_anon_ram_alloc failā oslib-posix.c - loÄ£ika bija Ŕāda: dažreiz var bÅ«t noderÄ«gi pielāgot adresi milzÄ«gai 2 MB lielai lapai, tāpēc mēs jautāsim mmap vispirms nedaudz vairāk, un tad mēs ar palÄ«dzÄ«bu atgriezÄ«sim pārpalikumu munmap. Un, ja Ŕāda izlÄ«dzināŔana nav nepiecieÅ”ama, mēs norādÄ«sim rezultātu, nevis 2 MB getpagesize() Sākot no mmap tas joprojām izdos saskaņotu adresi... Tātad Emscripten mmap tikai zvani malloc, bet, protams, tas nesakrÄ«t lapā. Kopumā kļūda, kas mani sarÅ«gtināja pāris mēneÅ”us, tika izlabota, veicot izmaiņas divi lÄ«nijas.

ZvanīŔanas funkciju iezīmes

Un tagad procesors kaut ko skaita, Qemu neavar, bet ekrāns neieslēdzas, un procesors ātri nonāk cilpās, spriežot pēc izejas -d exec,in_asm,out_asm. Ir parādÄ«jusies hipotēze: taimera pārtraukumi (vai vispār visi pārtraukumi) neienāk. Un patieŔām, ja atskrÅ«vējat pārtraukumus no vietējās montāžas, kas kaut kādu iemeslu dēļ darbojās, jÅ«s iegÅ«stat lÄ«dzÄ«gu attēlu. Bet tā nepavisam nebija atbilde: salÄ«dzinot izsekoÅ”anas pēdas ar iepriekÅ” minēto iespēju, tika parādÄ«ts, ka izpildes trajektorijas atŔķīrās ļoti agri. Å eit jāsaka, ka salÄ«dzinājums ar to, kas tika ierakstÄ«ts, izmantojot palaiÅ”anas programmu emrun izvades atkļūdoÅ”ana ar sākotnējās montāžas izvadi nav pilnÄ«bā mehānisks process. Es precÄ«zi nezinu, kā programma, kas darbojas pārlÅ«kprogrammā, izveido savienojumu ar emrun, bet dažas lÄ«nijas izvadā izrādās pārkārtotas, tāpēc atŔķirÄ«bas atŔķirÄ«ba vēl nav iemesls uzskatÄ«t, ka trajektorijas ir novirzÄ«juŔās. Kopumā kļuva skaidrs, ka saskaņā ar instrukcijām ljmpl notiek pāreja uz dažādām adresēm, un Ä£enerētais baitkods bÅ«tiski atŔķiras: vienā ir instrukcija izsaukt palÄ«gfunkciju, otrā nav. Pēc instrukciju googlÄ“Å”anas un koda izpētÄ«Å”anas, kas tulko Ŕīs instrukcijas, kļuva skaidrs, ka, pirmkārt, tieÅ”i pirms tās reÄ£istrā cr0 tika veikts ieraksts - arÄ« izmantojot palÄ«gu - kas pārslēdza procesoru uz aizsargāto režīmu, otrkārt, ka js versija nekad nepārslēdzās uz aizsargāto režīmu. Bet fakts ir tāds, ka vēl viena Emscripten iezÄ«me ir tā nevēlÄ“Å”anās paciest kodu, piemēram, instrukciju ievieÅ”anu. call TCI, kurā jebkuras funkcijas rādÄ«tājs rada veidu long long f(int arg0, .. int arg9) - funkcijas ir jāizsauc ar pareizo argumentu skaitu. Ja Å”is noteikums tiek pārkāpts, atkarÄ«bā no atkļūdoÅ”anas iestatÄ«jumiem programma vai nu avarēs (kas ir labi), vai vispār izsauks nepareizo funkciju (kuru bÅ«s bēdÄ«gi atkļūdot). Ir arÄ« treŔā iespēja - iespējot iesaiņotāju Ä£enerÄ“Å”anu, kas pievieno/noņem argumentus, bet kopumā Å”ie iesaiņotāji aizņem daudz vietas, neskatoties uz to, ka patiesÄ«bā man vajag tikai nedaudz vairāk par simts iesaiņojumiem. Tas vien ir ļoti skumji, bet izrādÄ«jās nopietnāka problēma: Ä£enerētajā iesaiņojuma funkciju kodā tika konvertēti un pārveidoti argumenti, bet dažreiz funkcija ar Ä£enerētajiem argumentiem netika izsaukta - nu, tāpat kā mana libffi ievieÅ”ana. Tas ir, daži palÄ«gi vienkārÅ”i netika izpildÄ«ti.

Par laimi, Qemu ir maŔīnlasāmi palÄ«gu saraksti galvenes faila veidā, piemēram

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

Tie tiek izmantoti diezgan smieklÄ«gi: pirmkārt, makro tiek definēti no jauna visdÄ«vainākajā veidā DEF_HELPER_n, un pēc tam ieslēdzas helper.h. Ciktāl makro tiek izvērsts struktÅ«ras inicializatorā un komatā, un pēc tam tiek definēts masÄ«vs un elementu vietā - #include <helper.h> Rezultātā man beidzot bija iespēja darbā izmēģināt bibliotēku pÄ«parsÄ“Å”ana, un tika uzrakstÄ«ts skripts, kas Ä£enerē tieÅ”i tos iesaiņojumus tieÅ”i tām funkcijām, kurām tie ir nepiecieÅ”ami.

Un tā, pēc tam Ŕķita, ka procesors strādāja. Å Ä·iet, ka tas ir tāpēc, ka ekrāns nekad netika inicializēts, lai gan memtest86+ varēja darboties sākotnējā komplektācijā. Å eit jāprecizē, ka Qemu bloka I/O kods ir ierakstÄ«ts korutÄ«nās. Emscripten ir sava ļoti sarežģīta ievieÅ”ana, taču tā joprojām bija jāatbalsta Qemu kodā, un tagad varat atkļūdot procesoru: Qemu atbalsta opcijas -kernel, -initrd, -append, ar kuru var palaist Linux vai, piemēram, memtest86+, vispār neizmantojot blokierÄ«ces. Bet Å”eit ir problēma: vietējā komplektācijā var redzēt Linux kodola izvadi konsolei ar opciju -nographic, un nav izvades no pārlÅ«kprogrammas uz termināli, no kurienes tas tika palaists emrun, nenāca. Tas ir, nav skaidrs: procesors nedarbojas vai grafikas izvade nedarbojas. Un tad man ienāca prātā nedaudz pagaidÄ«t. IzrādÄ«jās, ka ā€œprocesors neguļ, bet vienkārÅ”i lēni mirgoā€, un pēc apmēram piecām minÅ«tēm kodols iemeta konsolei virkni ziņojumu un turpināja karāties. Kļuva skaidrs, ka procesors kopumā darbojas, un mums ir jāiedziļinās kodā darbam ar SDL2. Diemžēl es nezinu, kā izmantot Å”o bibliotēku, tāpēc dažās vietās man bija jārÄ«kojas nejauÅ”i. Kādā brÄ«dÄ« ekrānā uz zila fona mirgoja paralēla 0 lÄ«nija, kas lika domāt par dažām pārdomām. Galu galā izrādÄ«jās, ka problēma bija tā, ka Qemu vienā fiziskajā logā atver vairākus virtuālos logus, starp kuriem var pārslēgties, izmantojot Ctrl-Alt-n: tas darbojas vietējā bÅ«vniecÄ«bā, bet ne Emscripten. Pēc atbrÄ«voÅ”anās no nevajadzÄ«giem logiem, izmantojot opcijas -monitor none -parallel none -serial none un norādÄ«jumi, lai piespiedu kārtā pārzÄ«mētu visu ekrānu katrā kadrā, viss pēkŔņi nostrādāja.

Korutīnas

Tātad emulācija pārlÅ«kprogrammā darbojas, taču tajā nevar palaist neko interesantu vienā disketē, jo nav bloka I/O - jums ir jāievieÅ” korutÄ«nu atbalsts. Qemu jau ir vairākas korutÄ«nas aizmugursistēmas, taču JavaScript un Emscripten koda Ä£eneratora bÅ«tÄ«bas dēļ jÅ«s nevarat vienkārÅ”i sākt žonglēt ar kaudzēm. Å Ä·iet, ka "viss ir pagājis, apmetums tiek noņemts", bet Emscripten izstrādātāji jau ir visu parÅ«pējuÅ”ies. Tas ir ieviests diezgan smieklÄ«gi: sauksim Ŕādu funkcijas izsaukumu par aizdomÄ«gu emscripten_sleep un vairākas citas, kas izmanto Asyncify mehānismu, kā arÄ« rādÄ«tāja izsaukumus un izsaukumus uz jebkuru funkciju, ja viens no diviem iepriekŔējiem gadÄ«jumiem var notikt tālāk steka. Un tagad pirms katra aizdomÄ«ga zvana atlasÄ«sim asinhrono kontekstu un uzreiz pēc zvana pārbaudÄ«sim, vai nav noticis asinhronais zvans, un ja ir, tad Å”ajā asinhronajā kontekstā saglabāsim visus lokālos mainÄ«gos, norādÄ«sim, kura funkcija lai pārsÅ«tÄ«tu kontroli uz laiku, kad jāturpina izpilde, un izietu no paÅ”reizējās funkcijas. Å eit ir iespēja izpētÄ«t efektu izŔķērdÄ“Å”ana ā€” koda izpildes turpināŔanas vajadzÄ«bām pēc atgrieÅ”anās no asinhronā izsaukuma kompilators Ä£enerē funkcijas ā€œstubsā€, kas sākas pēc aizdomÄ«ga izsaukuma ā€“ Ŕādi: ja ir n aizdomÄ«gi izsaukumi, tad funkcija tiks paplaÅ”ināta kaut kur n/2 reizes ā€” tas joprojām ir, ja ne Ņemiet vērā, ka pēc katra potenciāli asinhronā izsaukuma sākotnējā funkcijai ir jāpievieno daži lokālie mainÄ«gie. Pēc tam man pat bija jāraksta vienkārÅ”s Python skripts, kas, pamatojoties uz noteiktu Ä«paÅ”i pārmērÄ«gi izmantotu funkciju kopu, kas it kā ā€œneļauj asinhronijai iziet cauri sevā€ (tas ir, steka veicināŔana un viss, ko tikko aprakstÄ«ju, nav darbs tajos), norāda izsaukumus, izmantojot norādes, kurās funkcijas kompilatoram ir jāignorē, lai Ŕīs funkcijas netiktu uzskatÄ«tas par asinhronām. Un tad JS faili, kuru lielums ir mazāks par 60 MB, ir acÄ«mredzami par daudz ā€” teiksim, vismaz 30. Lai gan reiz es iestatÄ«ju montāžas skriptu un nejauÅ”i izmetu linkera opcijas, starp kurām bija -O3. Es palaižu Ä£enerēto kodu, un Chromium patērē atmiņu un avarē. Es tad nejauÅ”i paskatÄ«jos, ko viņŔ mēģina lejupielādēt... Nu, ko lai saka, es arÄ« bÅ«tu sastingusi, ja man bÅ«tu palÅ«gts pārdomāti izpētÄ«t un optimizēt 500+ MB Javascript.

Diemžēl pārbaudes Asyncify atbalsta bibliotēkas kodā nebija gluži draudzÄ«gas longjmp-s, kas tiek izmantoti virtuālā procesora kodā, bet pēc neliela ielāpa, kas atspējo Ŕīs pārbaudes un piespiedu kārtā atjauno kontekstus, it kā viss bÅ«tu kārtÄ«bā, kods darbojās. Un tad sākās dÄ«vaina lieta: dažkārt tika iedarbinātas sinhronizācijas koda pārbaudes - tās paÅ”as, kas kodu sabojā, ja pēc izpildes loÄ£ikas tas bÅ«tu jābloķē - kāds mēģināja sagrābt jau notvertu mutex. Par laimi, izrādÄ«jās, ka seriālā kodā tā nav loÄ£iska problēma - es vienkārÅ”i izmantoju Emscripten nodroÅ”ināto standarta galvenās cilpas funkcionalitāti, bet dažreiz asinhronais izsaukums pilnÄ«bā atŔķetināja steku, un tajā brÄ«dÄ« tas neizdodas. setTimeout no galvenās cilpas - tādējādi kods ievadÄ«ja galvenās cilpas iterāciju, neatstājot iepriekŔējo iterāciju. PārrakstÄ«ja uz bezgalÄ«gas cilpas un emscripten_sleep, un problēmas ar muteksiem apstājās. Kods ir kļuvis pat loÄ£iskāks - galu galā man nav koda, kas sagatavotu nākamo animācijas kadru - procesors vienkārÅ”i kaut ko aprēķina, un ekrāns tiek periodiski atjaunināts. Tomēr problēmas ar to neapstājās: dažreiz Qemu izpilde vienkārÅ”i klusi tika pārtraukta bez izņēmumiem vai kļūdām. Tajā brÄ«dÄ« es no tā atteicos, bet, skatoties uz priekÅ”u, teikÅ”u, ka problēma bija Ŕāda: korutÄ«nas kods faktiski neizmanto setTimeout (vai vismaz ne tik bieži, kā jÅ«s varētu domāt): funkcija emscripten_yield vienkārÅ”i iestata asinhronā zvana karogu. Visa bÅ«tÄ«ba ir tāda emscripten_coroutine_next nav asinhrona funkcija: iekŔēji tā pārbauda karogu, atiestata to un nodod kontroli tur, kur tas ir nepiecieÅ”ams. Tas ir, steka veicināŔana ar to beidzas. Problēma bija tāda, ka sakarā ar lietoÅ”anas pēc-free, kas parādÄ«jās, kad korutÄ«nas pÅ«ls tika atspējots, jo es nenokopēju svarÄ«gu koda rindiņu no esoŔās korutÄ«nas aizmugursistēmas, funkcija qemu_in_coroutine atgriezta patiesa, lai gan patiesÄ«bā tai vajadzēja atgriezt nepatiesu. Tas noveda pie zvana emscripten_yield, virs kura uz kaudzes neviena nebija emscripten_coroutine_next, kaudze atlocÄ«jās lÄ«dz paÅ”ai augÅ”ai, bet nē setTimeout, kā jau teicu, netika izstādÄ«ts.

JavaScript koda ģenerēŔana

Un patiesÄ«bā Å”eit ir apsolÄ«tais ā€œmaltās gaļas pagrieÅ”ana atpakaļā€. Ne Ä«sti. Protams, ja pārlÅ«kprogrammā palaižam Qemu un tajā Node.js, tad, protams, pēc koda Ä£enerÄ“Å”anas Qemu mēs saņemsim pilnÄ«gi nepareizu JavaScript. Bet tomēr kaut kāda reversa transformācija.

Pirmkārt, nedaudz par to, kā darbojas Qemu. LÅ«dzu, piedodiet man uzreiz: es neesmu profesionāls Qemu izstrādātājs, un mani secinājumi dažviet var bÅ«t kļūdaini. Kā viņi saka, "skolēna viedoklim nav jāsakrÄ«t ar skolotāja viedokli, Peano aksiomātiku un veselo saprātu." Qemu ir noteikts skaits atbalstÄ«to viesu arhitektÅ«ru, un katrai ir savs direktorijs target-i386. Veidojot, varat norādÄ«t atbalstu vairākām viesu arhitektÅ«rām, taču rezultāts bÅ«s tikai vairāki binārie faili. Viesu arhitektÅ«ras atbalsta kods savukārt Ä£enerē dažas iekŔējās Qemu darbÄ«bas, kuras TCG (Tiny Code Generator) jau pārvērÅ” maŔīnkodā resursdatora arhitektÅ«rai. Kā norādÄ«ts failā readme, kas atrodas tcg direktorijā, tas sākotnēji bija daļa no parastā C kompilatora, kas vēlāk tika pielāgots JIT. Tāpēc, piemēram, mērÄ·a arhitektÅ«ra Ŕī dokumenta izteiksmē vairs nav viesu arhitektÅ«ra, bet gan resursdatora arhitektÅ«ra. Kādā brÄ«dÄ« parādÄ«jās vēl viens komponents - Tiny Code Interpreter (TCI), kuram vajadzētu izpildÄ«t kodu (gandrÄ«z tās paÅ”as iekŔējās darbÄ«bas), ja nav koda Ä£eneratora konkrētai resursdatora arhitektÅ«rai. Faktiski, kā norādÄ«ts dokumentācijā, Å”is tulks ne vienmēr var darboties tik labi kā JIT koda Ä£enerators ne tikai kvantitatÄ«vi ātruma, bet arÄ« kvalitatÄ«vi. Lai gan es neesmu pārliecināts, ka viņa apraksts ir pilnÄ«bā atbilstoÅ”s.

Sākumā mēģināju izveidot pilnvērtÄ«gu TCG aizmuguri, taču ātri apmulsu avota kodā un ne visai skaidrā baitkoda instrukciju aprakstā, tāpēc nolēmu ietÄ«t TCI tulku. Tas deva vairākas priekÅ”rocÄ«bas:

  • ievieÅ”ot koda Ä£eneratoru, varēja skatÄ«ties nevis instrukciju aprakstu, bet gan tulka kodu
  • funkcijas var Ä£enerēt ne katram sastaptajam tulkoÅ”anas blokam, bet, piemēram, tikai pēc simtās izpildes
  • ja Ä£enerētais kods mainÄ«sies (un tas Ŕķiet iespējams, spriežot pēc funkcijām ar nosaukumiem, kas satur vārdu patch), man vajadzēs anulēt Ä£enerēto JS kodu, bet vismaz man bÅ«s no kā to atjaunot

AttiecÄ«bā uz treÅ”o punktu es neesmu pārliecināts, ka ielāpÄ“Å”ana ir iespējama pēc koda pirmās izpildes, taču pietiek ar pirmajiem diviem punktiem.

Sākotnēji kods tika Ä£enerēts liela slēdža veidā oriÄ£inālās baitkoda instrukcijas adresē, bet pēc tam, atceroties rakstu par Emscripten, Ä£enerētā JS optimizāciju un relooping, es nolēmu Ä£enerēt vairāk cilvēka koda, jo Ä«paÅ”i tāpēc, ka empÄ«riski tā izrādÄ«jās, ka vienÄ«gais ieejas punkts tulkoÅ”anas blokā ir tā sākums. Pēc kāda laika mums bija kodu Ä£enerators, kas Ä£enerēja kodu ar ifs (kaut arÄ« bez cilpām). Bet nepaveicās, tas avarēja, dodot ziņu, ka instrukcijas bija nepareiza garuma. Turklāt pēdējā instrukcija Å”ajā rekursijas lÄ«menÄ« bija brcond. Labi, es pievienoÅ”u identisku pārbaudi Ŕīs instrukcijas Ä£enerÄ“Å”anai pirms un pēc rekursÄ«vā izsaukuma, un... neviens no tiem netika izpildÄ«ts, taču pēc pārslēgÅ”anas Assert tie joprojām neizdevās. Beigās, izpētot Ä£enerēto kodu, sapratu, ka pēc pārslēgÅ”anas rādÄ«tājs uz paÅ”reizējo instrukciju tiek pārlādēts no steka un, iespējams, tiek pārrakstÄ«ts ar Ä£enerēto JavaScript kodu. Un tā arÄ« izrādÄ«jās. Bufera palielināŔana no viena megabaita lÄ«dz desmit ne pie kā neizraisÄ«ja, un kļuva skaidrs, ka kodu Ä£enerators darbojas riņķī. Nācās pārbaudÄ«t, vai neesam izgājuÅ”i ārpus esoŔās TB robežām, un ja tikām, tad izdot nākamās TB adresi ar mÄ«nusa zÄ«mi, lai varētu turpināt izpildi. Turklāt tas atrisina problēmu "kuras Ä£enerētās funkcijas bÅ«tu jāanulē, ja Å”is baitkoda gabals ir mainÄ«jies?" ā€” ir jāanulē tikai funkcija, kas atbilst Å”im tulkoÅ”anas blokam. Starp citu, lai gan es visu atkļūdoju pārlÅ«kā Chromium (tā kā es izmantoju Firefox un man ir vieglāk eksperimentiem izmantot atseviŔķu pārlÅ«kprogrammu), Firefox man palÄ«dzēja izlabot nesaderÄ«bu ar standartu asm.js, pēc tam kods sāka darboties ātrāk Chromium.

Ģenerētā koda piemērs

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

Secinājums

Tātad, darbs joprojām nav pabeigts, bet man ir apnicis slepeni pilnveidot Å”o ilgtermiņa bÅ«vniecÄ«bu. Tāpēc nolēmu publicēt to, kas man Å”obrÄ«d ir. Kods vietām ir nedaudz biedējoÅ”s, jo tas ir eksperiments, un iepriekÅ” nav skaidrs, kas jādara. Iespējams, tad ir vērts izdot parastās atomu saistÄ«bas papildus kādai modernākai Qemu versijai. Tikmēr Gitā ir pavediens bloga formātā: katram vismaz kaut kā nokārtotam ā€œlÄ«menimā€ pievienots detalizēts komentārs krievu valodā. PatiesÄ«bā Å”is raksts lielā mērā ir secinājuma pārstāsts git log.

JÅ«s varat to visu izmēģināt Å”eit (uzmanieties no satiksmes).

Kas jau darbojas:

  • darbojas x86 virtuālais procesors
  • Ir pieejams JIT koda Ä£eneratora prototips no maŔīnas koda uz JavaScript
  • Ir veidne citu 32 bitu viesu arhitektÅ«ru montāžai: Å”obrÄ«d varat apbrÄ«not Linux, jo MIPS arhitektÅ«ra iesaldē pārlÅ«kprogrammā ielādes stadijā.

Ko vēl var darīt

  • Paātrināt emulāciju. Pat JIT režīmā Ŕķiet, ka tas darbojas lēnāk nekā Virtual x86 (bet, iespējams, ir vesels Qemu ar daudz emulētas aparatÅ«ras un arhitektÅ«ras)
  • Lai izveidotu normālu interfeisu - atklāti sakot, es neesmu labs tÄ«mekļa izstrādātājs, tāpēc Å”obrÄ«d esmu pārtaisÄ«jis standarta Emscripten apvalku, cik vien labi varu.
  • Mēģiniet palaist sarežģītākas Qemu funkcijas - tÄ«klu veidoÅ”anu, VM migrāciju utt.
  • UPD: jums bÅ«s jāiesniedz savi daži uzlabojumi un kļūdu ziņojumi Emscripten augÅ”pus, kā to darÄ«ja iepriekŔējie Qemu un citu projektu nesēji. Paldies viņiem par iespēju netieÅ”i izmantot savu ieguldÄ«jumu Emscripten kā daļu no mana uzdevuma.

Avots: www.habr.com

Pievieno komentāru