Qemu.js JIT-toega: hakkliha saab ikka tagurpidi keerata

Paar aastat tagasi Fabrice Bellard kirjutas jslinux on JavaScriptis kirjutatud arvutiemulaator. Pärast seda oli vähemalt rohkem Virtuaalne x86. Kuid minu teada olid nad kõik tõlgid, samas kui sama Fabrice Bellardi palju varem kirjutatud Qemu ja ilmselt iga endast lugupidav kaasaegne emulaator kasutab külaliskoodi JIT-kompileerimist hostisüsteemi koodiks. Mulle tundus, et on aeg rakendada vastupidist ülesannet seoses sellega, mida brauserid lahendavad: masinkoodi JIT kompileerimine JavaScripti, mille jaoks tundus kõige loogilisem Qemu portimine. Tundub, et miks Qemu on lihtsamad ja kasutajasõbralikumad emulaatorid - näiteks sama VirtualBox - installitud ja töötavad. Kuid Qemul on mitmeid huvitavaid funktsioone

  • avatud lähtekoodiga
  • võime töötada ilma kerneli draiverita
  • võime töötada tõlgi režiimis
  • toetus suurele hulgale nii host- kui ka külalisarhitektuuridele

Kolmanda punkti kohta võin nüüd seletada, et tegelikult ei tõlgendata TCI režiimis külalismasina käske endid, vaid nendest saadud baitkoodi, kuid see ei muuda olemust – et ehitada ja käivitada. Qemu uuel arhitektuuril, kui veab, piisab C-kompilaatorist – koodigeneraatori kirjutamine võib edasi lükata.

Ja nüüd, pärast kaheaastast rahulikku vabal ajal Qemu lähtekoodi kallal nokitsemist, ilmus töötav prototüüp, milles saab juba näiteks Kolibri OS-i käivitada.

Mis on Emscripten

Tänapäeval on ilmunud palju kompilaatoreid, mille lõpptulemuseks on JavaScript. Mõned, nagu Type Script, olid algselt mõeldud parimaks viisiks veebi kirjutamiseks. Samas on Emscripten võimalus võtta olemasolev C või C++ kood ja kompileerida see brauseris loetavasse vormi. Peal see leht Oleme kogunud palju tuntud programme: siinNäiteks võite vaadata PyPyt - muide, nad väidavad, et neil on juba JIT. Tegelikult ei saa iga programmi lihtsalt brauseris kompileerida ja käivitada – neid on mitmeid Funktsioonid, millega tuleb aga leppida, kuna samal leheküljel olev kiri ütleb “Emscripteni abil saab koostada peaaegu iga kaasaskantav C/C++ kood JavaScriptile". See tähendab, et on mitmeid operatsioone, mis on standardi järgi määratlemata käitumisega, kuid töötavad tavaliselt x86 peal – näiteks joondamata juurdepääs muutujatele, mis on mõnel arhitektuuril üldiselt keelatud. Üldiselt , Qemu on platvormideülene programm ja , ma tahtsin uskuda, ja see ei sisalda juba palju määratlemata käitumist - võtke see ja kompileerige, seejärel nokitsege veidi JIT-iga - ja oletegi valmis! Aga see pole juhtum...

Esmalt proovige

Üldiselt ei ole ma esimene inimene, kellel on idee teisaldada Qemu JavaScripti. ReactOS-i foorumis küsiti, kas see on Emscripteni abil võimalik. Juba varem levisid jutud, et Fabrice Bellard tegi seda isiklikult, kuid jutt oli jslinuxist, mis minu teada on lihtsalt katse JS-is käsitsi piisavat jõudlust saavutada ja sai kirjutatud nullist. Hiljem kirjutati Virtual x86 - selle jaoks postitati hägustamata allikad ja nagu öeldud, võimaldas emulatsiooni suurem "realism" kasutada SeaBIOS-i püsivarana. Lisaks tehti vähemalt üks katse Qemu portimiseks Emscripteni abil – proovisin seda teha pistikupesapaar, aga areng oli minu arusaamist mööda külmunud.

Nii et tundub, et siin on allikad, siin on Emscripten - võtke see ja koostage. Kuid on ka raamatukogusid, millest Qemu sõltub, ja raamatukogusid, millest need raamatukogud sõltuvad jne, ja üks neist on libffi, millest libedus sõltub. Internetis levisid kuulujutud, et Emscripteni jaoks oli raamatukogude suures portsus selline olemas, kuid seda oli kuidagi raske uskuda: esiteks polnud see mõeldud uueks kompilaatoriks, teiseks oli see liiga madala tasemega raamatukogu lihtsalt kätte võtta ja JS-i kompileerida. Ja asi pole ainult kooste lisades – kui seda väänata, saate ilmselt mõne kutsumistava jaoks genereerida pinus vajalikud argumendid ja kutsuda funktsiooni ilma nendeta. Emscripten on aga keeruline asi: selleks, et genereeritud kood näeks brauseri JS-i mootori optimeerijale tuttav välja, kasutatakse mõningaid nippe. Eelkõige nn relooping - koodigeneraator, mis kasutab vastuvõetud LLVM IR-i koos mõningate abstraktsete üleminekujuhistega, püüab uuesti luua usutavaid if-e, silmuseid jne. Noh, kuidas argumendid funktsioonile edastatakse? Loomulikult argumentidena JS-funktsioonidele, st võimalusel mitte läbi virna.

Alguses oli mõte kirjutada lihtsalt libffi asendus JS-iga ja teha standardtestid, aga lõpuks jäi segadusse, kuidas oma päisefaile teha nii, et need olemasoleva koodiga töötaksid - mida teha? nagu öeldakse: "Kas ülesanded on nii keerulised "Kas me oleme nii lollid?" Pidin libffi nii-öelda teisele arhitektuurile portima - õnneks on Emscriptenil nii makrod inline assembly jaoks (Javascriptis jah - noh, olgu arhitektuur milline tahes, nii et assembler), kui ka võimalus käivitada käigu pealt genereeritud koodi. Üldiselt, pärast mõnda aega platvormist sõltuvate libffi fragmentidega nokitsemist, sain kompileeritava koodi ja käivitasin selle esimese katsega, mis mulle ette sattusin. Minu üllatuseks oli test edukas. Oma geniaalsusest uimastatuna – ilma naljata, see töötas esimesest käivitamisest peale – läksin ikka veel oma silmi mitte uskudes saadud koodi uuesti vaatama, et hinnata, kuhu edasi kaevata. Siin läksin ma teist korda hulluks – ainus, mida mu funktsioon tegi, oli ffi_call - see teatas edukast kõnest. Ise kõnet ei tulnud. Seega saatsin oma esimese tõmbetaotluse, mis parandas igale olümpiaadiõpilasele arusaadava vea testis – reaalnumbreid ei tohiks võrrelda a == b ja isegi kuidas a - b < EPS - peate ka moodulit meeles pidama, vastasel juhul osutub 0 väga võrdseks 1/3-ga... Üldiselt mõtlesin välja kindla libffi pordi, mis läbib kõige lihtsamad testid ja millega glib on koostatud - otsustasin, et on vaja, lisan hiljem. Tulevikku vaadates ütlen, et nagu selgus, ei lisanud kompilaator lõppkoodi isegi libffi funktsiooni.

Kuid nagu ma juba ütlesin, on mõned piirangud ja erinevate määratlemata käitumiste vaba kasutamise hulka on peidetud ebameeldivam funktsioon - JavaScript by design ei toeta ühismäluga mitme lõimega töötamist. Põhimõtteliselt võib seda tavaliselt isegi heaks ideeks nimetada, kuid mitte koodi teisaldamiseks, mille arhitektuur on seotud C lõimedega. Üldiselt katsetab Firefox jagatud töötajate toetamist ja Emscriptenil on nende jaoks pthread-rakendus, kuid ma ei tahtnud sellest sõltuda. Pidin Qemu koodist aeglaselt välja juurima multithreadingu – ehk uurima, kus lõimed jooksevad, teisaldama selles lõimes jooksva tsükli keha eraldi funktsiooniks ja selliseid funktsioone ükshaaval peatsüklist välja kutsuma.

Teine proovida

Mingil hetkel sai selgeks, et probleem on endiselt alles ja juhuslikult koodi ümber karkudega ajamine ei too kaasa midagi head. Järeldus: peame kuidagi süstematiseerima karkude lisamise protsessi. Seetõttu võeti sel ajal värske versioon 2.4.1 (mitte 2.5.0, sest kes teab, siis uues versioonis on veel vigu, mida pole veel tabatud ja mul on piisavalt oma vigu ) ja esimene asi oli see ohutult ümber kirjutada thread-posix.c. Noh, see tähendab ohutult: kui keegi üritas teha blokeerimiseni viivat toimingut, kutsuti see funktsioon kohe välja abort() - loomulikult ei lahendanud see kõiki probleeme korraga, aga vähemalt oli see kuidagi meeldivam kui vaikselt ebaühtlaste andmete vastuvõtmine.

Üldiselt on Emscripteni valikud koodi JS-i teisaldamisel väga abiks -s ASSERTIONS=1 -s SAFE_HEAP=1 - nad tabavad teatud tüüpi määratlemata käitumist, näiteks kõnesid joondamata aadressil (mis ei ole üldse kooskõlas trükitud massiivide koodiga, näiteks HEAP32[addr >> 2] = 1) või vale argumentide arvuga funktsiooni kutsumine.

Muide, joondusvead on omaette teema. Nagu ma juba ütlesin, on Qemul koodi genereerimise TCI (tiny code interpreter) jaoks "mandunud" interpreteeriv taustaprogramm ning Qemu ehitamiseks ja käitamiseks uuel arhitektuuril, kui teil veab, piisab C-kompilaatorist. Märksõnad "kui sul veab". Mul ei vedanud ja selgus, et TCI kasutab baitkoodi sõelumisel joondamata juurdepääsu. See tähendab, et kõikvõimalike ARM-i ja muude arhitektuuride puhul, millel on tingimata nivelleeritud juurdepääs, kompileerib Qemu, kuna neil on tavaline TCG-taustaprogramm, mis genereerib natiivset koodi, kuid kas TCI ka nende peal töötab, on teine ​​küsimus. Kuid nagu selgus, näitas TCI dokumentatsioon selgelt midagi sarnast. Selle tulemusena lisati koodile funktsioonikutsed joondamata lugemiseks, mis avastati Qemu teises osas.

Kuhja hävitamine

Selle tulemusena parandati joondamata juurdepääs TCI-le, tekkis põhisilmus, mis omakorda kutsus protsessorit, RCU-d ja mõnda muud pisiasja. Ja nii ma käivitan Qemu valikuga -d exec,in_asm,out_asm, mis tähendab, et peate ütlema, milliseid koodiplokke käivitatakse, ja ka edastuse ajal, et kirjutada, mis oli külaliskood, milliseks hostikoodiks sai (antud juhul baitkood). Käivitub, täidab mitu tõlkeplokki, kirjutab minu jäetud silumissõnumi, et RCU käivitub nüüd ja... jookseb kokku abort() funktsiooni sees free(). Funktsiooni kallal nokitsedes free() Meil õnnestus välja selgitada, et kuhjaploki päises, mis asub eraldatud mälule eelnevas kaheksas baidis, oli ploki suuruse või muu sarnase asemel prügi.

Kuhja hävitamine - kui armas... Sellisel juhul on kasulik abinõu - (võimalusel) samadest allikatest, koostage native binary ja käivitage see Valgrindi all. Mõne aja pärast oli kahendfail valmis. Käivitan selle samade valikutega – see jookseb kokku isegi initsialiseerimise ajal, enne kui päriselt täitmiseni jõuab. See on muidugi ebameeldiv - ilmselt ei olnud allikad täpselt samad, mis pole ka üllatav, sest konfigureerimine otsis veidi erinevaid võimalusi, aga mul on Valgrind - kõigepealt parandan selle vea ja siis, kui veab , kuvatakse originaal. Ma jooksen sama asja Valgrindi all... Y-y-y, y-y-y, uh-ah, see algas, läbis algseadistuse normaalselt ja liikus algsest veast mööda, ilma et oleks ühtegi hoiatust vale mälupääsu kohta, rääkimata kukkumisest. Elu, nagu öeldakse, ei valmistanud mind selleks ette – kokkujooksev programm lakkab jooksmast, kui Walgrindi all käivitatakse. Mis see oli, on mõistatus. Minu hüpotees on, et kord praeguse käsu läheduses pärast initsialiseerimise ajal toimunud krahhi näitas gdb tööd memset-a kehtiva kursoriga, kasutades kumbagi mmx, või xmm registreid, siis võib-olla oli see mingi joondusviga, kuigi seda on siiani raske uskuda.

Olgu, Valgrind siin ei aita. Ja siit algaski kõige vastikum – kõik tundub isegi algavat, kuid jookseb täiesti teadmata põhjustel kokku sündmuse tõttu, mis oleks võinud juhtuda miljoneid juhiseid tagasi. Pikka aega polnud isegi selge, kuidas läheneda. Lõpuks pidin ikka maha istuma ja siluma. Trükkides seda, millega päis ümber kirjutati, selgus, et see ei paistnud numbrina, vaid pigem mingi binaarandmetena. Ja ennäe ennäe, see binaarne string leiti BIOS-i failist – ehk siis nüüd sai mõistliku kindlusega väita, et tegemist oli puhvri ülevooluga ja on isegi selge, et see on sellesse puhvrisse kirjutatud. No siis midagi sellist - Emscriptenis pole õnneks aadressiruumi randomiseerimist, seal pole ka auke, nii et saab kirjutada kuskile koodi keskele, et viimasest käivitamisest kursori kaupa andmeid väljastada, vaadake andmeid, vaadake kursorit ja kui see pole muutunud, hankige mõtlemisainet. Tõsi, pärast muudatusi kulub linkimiseks paar minutit, kuid mida saate teha? Selle tulemusena leiti konkreetne rida, mis kopeeris BIOS-i ajutisest puhvrist külalismällu - ja tõepoolest, puhvris ei olnud piisavalt ruumi. Selle kummalise puhvri aadressi allika leidmine andis tulemuseks funktsiooni qemu_anon_ram_alloc failis oslib-posix.c - loogika oli järgmine: mõnikord võib olla kasulik joondada aadress suurele 2 MB suurusele lehele, selleks küsime mmap kõigepealt natuke rohkem ja siis tagastame abiga ülejäägi munmap. Ja kui sellist joondamist pole vaja, näitame 2 MB asemel tulemuse getpagesize() - mmap see annab ikka välja joondatud aadressi... Nii et Emscriptenis mmap lihtsalt helistab malloc, kuid loomulikult ei joondu see lehel. Üldiselt parandati viga, mis mind paar kuud frustreeris, muudatusega kaks read.

Helistamisfunktsioonide omadused

Ja nüüd loendab protsessor midagi, Qemu ei jookse kokku, kuid ekraan ei lülitu sisse ja protsessor läheb väljundi järgi otsustades kiiresti silmustesse -d exec,in_asm,out_asm. On tekkinud hüpotees: taimeri katkestused (või üldiselt kõik katkestused) ei jõua kohale. Ja tõepoolest, kui keerate lahti nn sõlmest katkestused, mis mingil põhjusel töötasid, saate sarnase pildi. Kuid see ei olnud üldse vastus: ülaltoodud variandiga väljastatud jälgede võrdlus näitas, et hukkamisteekonnad lahknesid väga varakult. Siinkohal tuleb öelda, et kanderaketi abil salvestatu võrdlus emrun väljundi silumine algkoostu väljundiga ei ole täiesti mehaaniline protsess. Ma ei tea täpselt, kuidas brauseris töötav programm ühenduse loob emrun, kuid väljundis osutuvad mõned read ümber paigutatuks, seega ei ole erinevuse erinevus veel põhjust eeldada, et trajektoorid on lahknenud. Üldiselt sai selgeks, et vastavalt juhistele ljmpl toimub üleminek erinevatele aadressidele ja genereeritav baitkood on põhimõtteliselt erinev: üks sisaldab käsku kutsuda abifunktsiooni, teine ​​mitte. Pärast juhiste guugeldamist ja neid juhiseid tõlkiva koodi uurimist sai selgeks, et esiteks vahetult enne seda registris cr0 tehti salvestus - ka abistaja abil -, mis lülitas protsessori kaitstud režiimile ja teiseks, et js versioon ei lülitu kunagi kaitstud režiimi. Kuid tõsiasi on see, et Emscripteni teine ​​omadus on vastumeelsus taluda sellist koodi nagu juhiste rakendamine call TCI-s, mille mis tahes funktsiooni osuti tulemuseks on tüüp long long f(int arg0, .. int arg9) - funktsioonid tuleb kutsuda õige arvu argumentidega. Kui seda reeglit rikutakse, siis olenevalt silumisseadetest programm kas jookseb kokku (mis on hea) või kutsub üldse välja vale funktsiooni (mida on kurb siluda). On ka kolmas võimalus - lubage argumente lisavate/eemaldavate ümbriste genereerimine, kuid kokku võtavad need ümbrised palju ruumi, hoolimata sellest, et tegelikult on mul vaja vaid veidi rohkem kui sada ümbrist. Ainuüksi see on väga kurb, kuid osutus tõsisemaks probleemiks: ümbrisfunktsioonide loodud koodis teisendati ja teisendati argumendid, kuid mõnikord ei kutsutud genereeritud argumentidega funktsiooni välja - noh, täpselt nagu minu libffi rakendus. See tähendab, et mõnda abilist lihtsalt ei hukatud.

Õnneks on Qemul masinloetavad abiliste nimekirjad päisefaili kujul nagu

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

Neid kasutatakse üsna naljakalt: esiteks määratletakse makrod ümber kõige veidramal viisil DEF_HELPER_nja seejärel lülitub sisse helper.h. Sel määral, mil makro laiendatakse struktuuri initsialisaatoriks ja komadeks ning seejärel määratakse massiiv ja elementide asemel - #include <helper.h> Selle tulemusena sain lõpuks võimaluse proovida raamatukogu tööl püparseerimine, ja kirjutati skript, mis genereerib täpselt need ümbrised täpselt nende funktsioonide jaoks, mille jaoks neid vaja on.

Ja nii, pärast seda tundus protsessor töötavat. Tundub, et selle põhjuseks on asjaolu, et ekraani ei lähtestatud kunagi, kuigi memtest86+ sai käitada algkoosseisus. Siin on vaja selgitada, et Qemu ploki I/O kood on kirjutatud korutiinides. Emscriptenil on oma väga keeruline teostus, kuid seda oli siiski vaja Qemu koodis toetada ja saate nüüd protsessori siluda: Qemu toetab valikuid -kernel, -initrd, -append, millega saab käivitada Linuxi või näiteks memtest86+, ilma plokkseadmeid kasutamata. Kuid siin on probleem: algkoostis võis näha Linuxi kerneli väljundit konsooli valikuga -nographicja brauserist pole väljundit terminali, kust see käivitati emrun, ei tulnud. See tähendab, et pole selge: protsessor ei tööta või graafikaväljund ei tööta. Ja siis tuli mulle pähe, et ootaks veidi. Selgus, et "protsessor ei maga, vaid lihtsalt vilgub aeglaselt" ja umbes viie minuti pärast viskas kernel konsooli hunniku sõnumeid ja jätkas rippumist. Sai selgeks, et protsessor üldiselt töötab ja peame SDL2-ga töötamiseks koodi süvenema. Kahjuks ma ei tea, kuidas seda raamatukogu kasutada, nii et mõnes kohas pidin tegutsema juhuslikult. Mingil hetkel vilkus ekraanil sinisel taustal paralleel0 joon, mis pakkus mõtteid. Lõpuks selgus, et probleem seisnes selles, et Qemu avab ühes füüsilises aknas mitu virtuaalset akent, mille vahel saab Ctrl-Alt-n abil lülituda: native buildis töötab, aga Emscriptenis mitte. Pärast tarbetutest akendest vabanemist valikute abil -monitor none -parallel none -serial none ja juhiseid kogu ekraani iga kaadri jõuga ümber joonistamiseks, kõik äkki töötas.

Korutiinid

Niisiis, emuleerimine brauseris töötab, kuid te ei saa selles käivitada midagi huvitavat ühes disketis, kuna pole ploki I/O-d - peate juurutama korutiinide toe. Qemul on juba mitu korutiini taustaprogrammi, kuid JavaScripti ja Emscripteni koodigeneraatori olemuse tõttu ei saa te lihtsalt virnadega žongleerima hakata. Näib, et "kõik on kadunud, krohv eemaldatakse", kuid Emscripteni arendajad on juba kõige eest hoolitsenud. See on üsna naljakas: nimetame sellist funktsioonikutset kahtlaseks emscripten_sleep ja mitmed teised, mis kasutavad Asyncify mehhanismi, aga ka kursorikutsed ja kõned mis tahes funktsioonidele, kus üks kahest eelmisest juhtumist võib esineda pinu allpool. Ja nüüd, enne iga kahtlast kõnet, valime asünkroonse konteksti ja kohe pärast kõnet kontrollime, kas asünkroonne kõne on toimunud ja kui on, siis salvestame kõik kohalikud muutujad selles asünkroonses kontekstis, näitame, milline funktsioon et anda juhtimine üle, kui peame täitmist jätkama, ja väljuda praegusest funktsioonist. Siin on ruumi mõju uurimiseks raiskamine — koodi täitmise jätkamiseks pärast asünkroonsest kõnest naasmist genereerib kompilaator pärast kahtlast kõnet algava funktsiooni "stubs" - umbes nii: kui kahtlaseid väljakutseid on n, siis laiendatakse funktsiooni kuskil n/2 korda — see on ikka, kui mitte. Pidage meeles, et pärast iga potentsiaalselt asünkroonset kõnet peate lisama algsele funktsioonile mõne kohaliku muutuja salvestamise. Seejärel pidin Pythonis isegi kirjutama lihtsa skripti, mis antud eriti ülekasutatud funktsioonide komplekti põhjal, mis väidetavalt "ei lase asünkroonsusel endast läbi minna" (st virna edendamine ja kõik, mida ma just kirjeldasin, ei lase töö neis), osutab viidete kaudu kutsudele, mille funktsioone peaks kompilaator ignoreerima, et neid funktsioone ei peetaks asünkroonseteks. Ja siis alla 60 MB JS-faile on selgelt liiga palju – oletame, et vähemalt 30. Kuigi kunagi koostasin koosteskripti ja viskasin kogemata välja linkeri valikud, mille hulgas oli -O3. Käivitan loodud koodi ja Chromium sööb mälu ära ja jookseb kokku. Vaatasin siis kogemata, mida ta üritas alla laadida... No mis ma oskan öelda, ma oleksin ka tardunud, kui mul oleks palutud mõtlikult uurida ja optimeerida 500+ MB Javascripti.

Kahjuks ei olnud Asyncify tugiteegi koodi kontrollid täiesti sõbralikud longjmp-s, mida kasutatakse virtuaalse protsessori koodis, kuid pärast väikest paika, mis keelab need kontrollid ja taastab jõuliselt kontekstid, nagu kõik oleks korras, kood töötas. Ja siis algas kummaline asi: mõnikord käivitati sünkroonimiskoodi kontrollid - samad, mis jooksevad koodi kokku, kui täitmisloogika kohaselt peaks see olema blokeeritud - keegi üritas haarata juba püütud mutexi. Õnneks ei osutunud see serialiseeritud koodis loogiliseks probleemiks – kasutasin lihtsalt Emscripteni pakutavat standardset põhisilmuse funktsionaalsust, kuid mõnikord keeras asünkroonne kõne pinu täielikult lahti ja sel hetkel see ebaõnnestus. setTimeout põhitsüklist - seega sisenes kood põhitsükli iteratsiooni eelmisest iteratsioonist lahkumata. Kirjutas ümber lõpmatul tsüklil ja emscripten_sleepja mutexidega seotud probleemid lõppesid. Kood on muutunud isegi loogilisemaks - mul pole ju tegelikult koodi, mis järgmise animatsioonikaadri ette valmistaks - protsessor lihtsalt arvutab midagi ja ekraani värskendatakse perioodiliselt. Probleemid aga sellega ei piirdunud: mõnikord lõppes Qemu täitmine lihtsalt vaikselt ilma erandite ja vigadeta. Sel hetkel loobusin sellest, kuid tulevikku vaadates ütlen, et probleem oli järgmine: korutiinikood tegelikult ei kasuta setTimeout (või vähemalt mitte nii sageli, kui arvate): funktsioon emscripten_yield seab lihtsalt asünkroonse kõne lipu. Kogu point on selles emscripten_coroutine_next ei ole asünkroonne funktsioon: see kontrollib sisemiselt lippu, lähtestab selle ja edastab juhtimise sinna, kus seda vaja on. See tähendab, et stäki reklaamimine lõpeb sellega. Probleem seisnes selles, et kasutuse pärast vaba kasutamise tõttu, mis ilmnes korutiinikogumi väljalülitamisel, kuna ma ei kopeerinud olemasolevast korutiini taustaprogrammist olulist koodirida, funktsioon qemu_in_coroutine tagastas tõene, kuigi tegelikult oleks pidanud tagastama vale. See viis kõneni emscripten_yield, mille kohal ei olnud virna peal kedagi emscripten_coroutine_next, pakk läks lahti päris ülaossa, aga ei setTimeout, nagu ma juba ütlesin, ei eksponeeritud.

JavaScripti koodi genereerimine

Ja siin on tegelikult lubatud "hakkliha tagasipööramine". Mitte päris. Muidugi, kui käivitame brauseris Qemu ja selles Node.js, siis loomulikult saame pärast Qemus koodi genereerimist täiesti vale JavaScripti. Aga ikkagi, mingi pöördtransformatsioon.

Esiteks natuke sellest, kuidas Qemu töötab. Palun andke mulle kohe andeks: ma ei ole professionaalne Qemu arendaja ja minu järeldused võivad kohati olla ekslikud. Nagu öeldakse, "õpilase arvamus ei pea ühtima õpetaja arvamusega, Peano aksiomaatika ja terve mõistusega." Qemul on teatud arv toetatud külalisarhitektuure ja igaühe jaoks on kataloog nagu target-i386. Ehitamisel saate määrata mitme külalisarhitektuuri toe, kuid tulemuseks on lihtsalt mitu kahendfaili. Külalisarhitektuuri toetav kood genereerib omakorda mõned sisemised Qemu toimingud, mille TCG (Tiny Code Generator) muudab juba hostarhitektuuri masinkoodiks. Nagu kataloogis tcg asuvas readme-failis öeldud, oli see algselt osa tavalisest C-kompilaatorist, mida hiljem kohandati JIT-i jaoks. Seetõttu ei ole näiteks selle dokumendi mõistes sihtarhitektuur enam külalisarhitektuur, vaid hostarhitektuur. Mingil hetkel ilmus veel üks komponent - Tiny Code Interpreter (TCI), mis peaks konkreetse hostarhitektuuri jaoks koodigeneraatori puudumisel käivitama koodi (peaaegu samad sisetoimingud). Tegelikult, nagu selle dokumentatsioon ütleb, ei pruugi see tõlk alati nii hästi toimida kui JIT-koodigeneraator, mitte ainult kvantitatiivselt kiiruse, vaid ka kvalitatiivselt. Kuigi ma pole kindel, et tema kirjeldus on täiesti asjakohane.

Alguses proovisin teha täisväärtuslikku TCG taustaprogrammi, kuid sattusin kiiresti segadusse lähtekoodi ja baitkoodi juhiste ebaselge kirjeldusega, nii et otsustasin TCI tõlgi pakkida. See andis mitmeid eeliseid:

  • koodigeneraatori realiseerimisel võiks vaadata mitte juhiste kirjeldust, vaid interpretaatori koodi
  • Funktsioone saab genereerida mitte iga kohatud tõlkeploki jaoks, vaid näiteks alles pärast sajandat täitmist
  • kui genereeritud kood muutub (ja see tundub olevat võimalik, otsustades funktsioonide järgi, mille nimed sisaldavad sõna patch), pean genereeritud JS-koodi kehtetuks tunnistama, kuid mul on vähemalt midagi, millest see uuesti luua.

Mis puutub kolmandasse punkti, siis ma pole kindel, et pärast koodi esmakordset käivitamist on lappimine võimalik, kuid esimesest kahest punktist piisab.

Algselt genereeriti kood suure lüliti kujul algse baitkoodi käsu aadressil, kuid siis, meenutades artiklit Emscripteni, genereeritud JS-i optimeerimise ja uuesti loopimise kohta, otsustasin genereerida rohkem inimkoodi, eriti kuna empiiriliselt selgus, et tõlkeploki ainus sisenemispunkt on selle algus. Varsti öeldud, kui tehtud, mõne aja pärast oli meil koodigeneraator, mis genereeris koodi if-idega (kuigi ilma silmusteta). Kuid halb õnn, see jooksis kokku, andes teate, et juhised on ebaõige pikkusega. Pealegi oli viimane juhend sellel rekursioonitasemel brcond. Olgu, ma lisan selle juhise genereerimisele identse kontrolli enne ja pärast rekursiivset kõnet ja... ühtegi neist ei käivitatud, kuid pärast kinnituslülitit need ikkagi ebaõnnestusid. Lõpuks, pärast genereeritud koodi uurimist, sain aru, et pärast ümberlülitamist laetakse pinust uuesti jooksva käsu osuti ja genereeritud JavaScripti kood kirjutab tõenäoliselt üle. Ja nii see välja tuli. Puhvri suurendamine ühelt megabaidilt kümnele ei toonud kaasa midagi ja selgus, et koodigeneraator töötab ringi. Pidime kontrollima, et me ei välju praeguse TB piiridest ja kui läksime, siis väljastama järgmise TB aadressi miinusmärgiga, et saaksime täitmist jätkata. Lisaks lahendab see probleemi "millised loodud funktsioonid tuleks kehtetuks tunnistada, kui see baitkooditükk on muutunud?" — ainult sellele tõlkeplokile vastav funktsioon tuleb tühistada. Muide, kuigi silusin kõike Chromiumis (kuna ma kasutan Firefoxi ja mul on lihtsam katseteks kasutada eraldi brauserit), aitas Firefox mul parandada asm.js standardiga vastuolusid, misjärel hakkas kood aastal kiiremini töötama. Kroom.

Näide genereeritud koodist

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

Järeldus

Nii et töö pole ikka veel lõpetatud, aga ma olen väsinud selle pikaajalise ehituse salaja täiuseni viimisest. Seetõttu otsustasin avaldada selle, mis mul praegu on. Kood on kohati pisut hirmutav, sest see on eksperiment ja pole eelnevalt selge, mida teha tuleb. Tõenäoliselt tasub siis Qemu mõne moodsama versiooni peale väljastada tavalised atomaarsed kohustused. Gitas on vahepeal blogiformaadis lõim: iga vähemalt kuidagi läbitud “taseme” kohta on lisatud üksikasjalik venekeelne kommentaar. Tegelikult on see artikkel suures osas järelduse ümberjutustus git log.

Saate seda kõike proovida siin (ettevaatust liiklusele).

Mis juba töötab:

  • x86 virtuaalne protsessor töötab
  • On olemas JIT-koodigeneraatori töötav prototüüp masinkoodist JavaScriptini
  • Muude 32-bitiste külalisarhitektuuride kokkupanemiseks on mall: praegu saate imetleda Linuxi MIPS-arhitektuuri, mis brauseris laadimisetapis hangub.

Mida sa muud saad teha

  • Emuleerimise kiirendamine. Tundub, et isegi JIT-režiimis töötab see aeglasemalt kui Virtual x86 (kuid potentsiaalselt on olemas terve Qemu, millel on palju emuleeritud riistvara ja arhitektuuri)
  • Tavalise liidese loomiseks – ausalt öeldes ei ole ma hea veebiarendaja, nii et praegu olen standardse Emscripteni kesta nii hästi kui võimalik ümber teinud
  • Proovige käivitada keerukamaid Qemu funktsioone - võrgundus, VM-i migratsioon jne.
  • UPD: peate esitama oma mõned arendused ja veateated Emscriptenile ülesvoolu, nagu tegid Qemu ja teiste projektide varasemad kandjad. Tänan neid selle eest, et nad said kaudselt kasutada oma panust Emscripteni osana minu ülesandest.

Allikas: www.habr.com

Lisa kommentaar