Qemu.js su JIT palaikymu: vis tiek galite pasukti faršą atgal

Prieš keletą metų Fabrice'as Bellardas parašė jslinux yra kompiuterio emuliatorius, parašytas JavaScript. Po to buvo bent daugiau Virtualus x86. Bet visi jie, kiek aš žinau, buvo vertėjai, o Qemu, kurį daug anksčiau parašė tas pats Fabrice'as Bellardas, ir, ko gero, bet kuris save gerbiantis šiuolaikinis emuliatorius, naudoja svečio kodo JIT kompiliavimą į pagrindinio kompiuterio kodą. Man atrodė, kad atėjo laikas įgyvendinti priešingą užduotį, susijusią su ta, kurią sprendžia naršyklės: JIT mašininio kodo kompiliavimą į JavaScript, kuriam logiškiausia atrodė prijungti Qemu. Atrodytų, kodėl Qemu yra paprastesni ir patogesni emuliatoriai - pavyzdžiui, tas pats "VirtualBox" - įdiegti ir veikia. Tačiau Qemu turi keletą įdomių funkcijų

  • atviro kodo
  • galimybė dirbti be branduolio tvarkyklės
  • gebėjimas dirbti vertėjo režimu
  • palaikymas daugeliui priimančiosios ir svečių architektūros

Dėl trečio punkto dabar galiu paaiškinti, kad iš tikrųjų TCI režime interpretuojamos ne pačios svečių mašinos instrukcijos, o iš jų gautas baitų kodas, tačiau tai nekeičia esmės – norint sukurti ir paleisti Qemu ant naujos architektūros, jei pasiseks, užtenka C kompiliatoriaus – kodų generatoriaus rašymas gali būti atidėtas.

Ir dabar, po dvejų metų laisvalaikiu neskubančio „Qemu“ šaltinio kodo, pasirodė veikiantis prototipas, kuriame jau galite paleisti, pavyzdžiui, „Kolibri“ OS.

Kas yra Emscripten

Šiais laikais atsirado daug kompiliatorių, kurių galutinis rezultatas – JavaScript. Kai kurie, pavyzdžiui, „Type Script“, iš pradžių buvo skirti kaip geriausias būdas rašyti žiniatinklyje. Tuo pačiu metu „Emscripten“ yra būdas paimti esamą C arba C++ kodą ir sudaryti jį į naršyklėje skaitomą formą. Įjungta šiuo puslapiu Mes surinkome daugybę gerai žinomų programų prievadų: čiaPavyzdžiui, galite pažvelgti į PyPy – beje, jie teigia jau turintys JIT. Tiesą sakant, ne kiekvieną programą galima tiesiog sukompiliuoti ir paleisti naršyklėje – yra keletas funkcijos, su kuria vis dėlto tenka susitaikyti, nes tame pačiame puslapyje esantis užrašas sako „Emscripten gali būti naudojamas beveik bet kokiam nešiojamų C/C++ kodas į „JavaScript“. Tai yra, yra keletas operacijų, kurios pagal standartą yra neapibrėžtos, tačiau dažniausiai veikia x86 – pavyzdžiui, nesuderinta prieiga prie kintamųjų, o tai paprastai draudžiama kai kuriose architektūrose. Apskritai , „Qemu“ yra kelių platformų programa ir, norėjau tikėti, ir joje jau nėra daug neapibrėžto elgesio – paimkite ir sukompiliuokite, tada šiek tiek padirbėkite su JIT – ir viskas! Bet tai nėra atvejis...

Pirmiausia pabandykite

Apskritai, aš nesu pirmasis žmogus, kuris sugalvojo perkelti Qemu į JavaScript. ReactOS forume buvo užduotas klausimas, ar tai įmanoma naudojant Emscripten. Dar anksčiau sklandė gandai, kad Fabrice'as Bellardas tai padarė asmeniškai, bet mes kalbėjome apie jslinux, kuris, kiek žinau, yra tik bandymas rankiniu būdu pasiekti pakankamą našumą JS ir buvo parašytas nuo nulio. Vėliau buvo parašytas „Virtual x86“ - jam buvo paskelbti neužmaskuoti šaltiniai, ir, kaip minėta, didesnis emuliacijos „realizmas“ leido naudoti SeaBIOS kaip programinę-aparatinę įrangą. Be to, buvo bent vienas bandymas perjungti Qemu naudojant Emscripten – bandžiau tai padaryti lizdų pora, bet plėtra, kiek suprantu, buvo užšaldyta.

Taigi, atrodytų, čia yra šaltiniai, čia yra Emscripten - imkite ir surinkite. Tačiau yra ir bibliotekų, nuo kurių priklauso Qemu, ir bibliotekų, nuo kurių priklauso tos bibliotekos ir pan., ir viena iš jų yra libffi, nuo kurios priklauso glotnumas. Internete sklandė gandai, kad didelėje Emscripten bibliotekų prievadų kolekcijoje toks yra, bet kažkaip buvo sunku patikėti: pirma, tai nebuvo numatyta kaip naujas kompiliatorius, antra, tai buvo per žemo lygio. biblioteką tiesiog pasiimti ir sukompiliuoti į JS. Ir tai ne tik surinkimo įdėklai - tikriausiai, jei jį pasuksite, kai kurioms iškvietimo sutartims galite sugeneruoti reikalingus argumentus krūvoje ir iškviesti funkciją be jų. Tačiau „Emscripten“ yra keblus dalykas: norint, kad sukurtas kodas naršyklės JS variklio optimizavimo įrankiui atrodytų pažįstamas, naudojami tam tikri triukai. Visų pirma, vadinamasis relooping - kodų generatorius, naudodamas gautą LLVM IR su kai kuriomis abstrakčiomis perėjimo instrukcijomis, bando atkurti tikėtinus ifs, ciklus ir pan. Na, kaip argumentai perduodami funkcijai? Natūralu, kad kaip argumentus JS funkcijoms, tai yra, jei įmanoma, ne per krūvą.

Pradžioje buvo mintis tiesiog parašyti libffi pakaitalą su JS ir paleisti standartinius testus, bet galiausiai susimąsčiau, kaip padaryti savo antraštės failus, kad jie veiktų su esamu kodu – ką daryti? kaip sakoma: "Ar užduotys tokios sudėtingos "Ar mes tokie kvaili?" Teko perkelti libffi į kitą architektūrą, taip sakant – laimei, Emscripten turi ir makrokomandas inline assembly (Javascripte taip – ​​na, bet kokia architektūra, taigi surinkėjas), ir galimybę paleisti skrydžio metu sugeneruotą kodą. Apskritai, kurį laiką padirbinėjęs su nuo platformos priklausančiais libffi fragmentais, gavau kompiliuojamą kodą ir paleidau jį per pirmą pasitaikiusį testą. Mano nuostabai, testas buvo sėkmingas. Apsvaigintas savo genialumo – ne juokai, tai veikė nuo pat pirmo paleidimo – aš, vis dar netikėdamas savo akimis, nuėjau dar kartą pasižiūrėti gauto kodo, įvertinti, kur toliau kasti. Čia aš išprotėjau antrą kartą – vienintelė mano funkcija buvo ffi_call - šis pranešė apie sėkmingą skambutį. Pačio skambučio nebuvo. Taigi išsiunčiau savo pirmąjį traukimo prašymą, kuris ištaisė testo klaidą, kuri yra aiški bet kuriam olimpiados mokiniui – realūs skaičiai neturėtų būti lyginami kaip a == b ir net kaip a - b < EPS - reikia atsiminti ir modulį, antraip 0 pasirodys labai lygus 1/3... Apskritai aš sugalvojau tam tikrą libffi prievadą, kuris išlaiko paprasčiausius testus ir su kuriuo yra glibas sukompiliuota – nusprendžiau, kad reikės, vėliau papildysiu. Žvelgdamas į ateitį, pasakysiu, kad, kaip paaiškėjo, kompiliatorius net neįtraukė libffi funkcijos į galutinį kodą.

Tačiau, kaip jau sakiau, yra tam tikrų apribojimų, o tarp laisvo įvairių neapibrėžtų elgsenų naudojimo buvo paslėpta nemalonesnė funkcija - „JavaScript“ pagal dizainą nepalaiko kelių gijų su bendra atmintimi. Iš esmės tai paprastai netgi gali būti vadinama gera idėja, bet ne kodo, kurio architektūra susieta su C gijomis, perkėlimui. Paprastai tariant, „Firefox“ eksperimentuoja su bendrų darbuotojų palaikymu, o „Emscripten“ turi jiems pritaikytą pthread, bet aš nenorėjau nuo to priklausyti. Teko pamažu iš Qemu kodo išrauti multithreading – tai yra išsiaiškinti, kur eina gijos, perkelti šioje gijoje einančio ciklo korpusą į atskirą funkciją ir iškviesti tokias funkcijas po vieną iš pagrindinės kilpos.

Antras bandymas

Kažkuriuo momentu tapo aišku, kad problema vis dar išliko ir kad atsitiktinis ramentų stumdymas aplink kodą nieko gero neduos. Išvada: turime kažkaip susisteminti ramentų pridėjimo procesą. Todėl buvo paimta 2.4.1 versija, kuri tuo metu buvo šviežia (ne 2.5.0, nes, kas žino, naujojoje versijoje bus klaidų, kurios dar nepagautos, o aš turiu pakankamai savo klaidų ), ir pirmas dalykas buvo jį saugiai perrašyti thread-posix.c. Na, tai yra kaip saugu: jei kas nors bandė atlikti operaciją, vedančią prie blokavimo, funkcija buvo nedelsiant iškviesta abort() - žinoma, tai neišsprendė visų problemų iš karto, bet bent jau buvo kažkaip maloniau nei tyliai gauti nenuoseklius duomenis.

Apskritai, Emscripten parinktys yra labai naudingos perkeliant kodą į JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - jie užfiksuoja kai kurių tipų neapibrėžtą elgesį, pvz., iškvietimus nesuderintu adresu (kuris visiškai neatitinka įvestų masyvų kodo, pvz. HEAP32[addr >> 2] = 1) arba funkcijos iškvietimas su netinkamu argumentų skaičiumi.

Beje, derinimo klaidos yra atskira problema. Kaip jau sakiau, „Qemu“ turi „išsigimusią“ aiškinamąją programinę įrangą, skirtą kodų generavimui TCI (tiny code interpreter), o norint sukurti ir paleisti Qemu naujoje architektūroje, jei pasiseks, užtenka C kompiliatoriaus. Raktiniai žodžiai "jei tau pasiseks". Man nepasisekė ir paaiškėjo, kad analizuodamas baito kodą TCI naudoja nesuderintą prieigą. Tai yra, visų rūšių ARM ir kitose architektūrose su būtinai išlyginta prieiga, Qemu kompiliuoja, nes turi įprastą TCG užpakalinę programą, kuri generuoja vietinį kodą, bet ar TCI veiks su jais, yra kitas klausimas. Tačiau, kaip paaiškėjo, TCI dokumentacijoje aiškiai nurodyta kažkas panašaus. Dėl to į kodą buvo įtraukti nesuderinto skaitymo funkcijų iškvietimai, kurie buvo rasti kitoje Qemu dalyje.

Krūvos sunaikinimas

Dėl to buvo ištaisyta nesuderinta prieiga prie TCI, sukurta pagrindinė kilpa, kuri savo ruožtu vadinama procesoriumi, RCU ir kai kuriomis kitomis smulkmenomis. Taigi aš paleidžiu Qemu su galimybe -d exec,in_asm,out_asm, o tai reiškia, kad reikia pasakyti, kurie kodo blokai yra vykdomi, taip pat transliacijos metu parašyti, koks buvo svečio kodas, koks tapo pagrindinio kompiuterio kodu (šiuo atveju baito kodu). Jis paleidžiamas, vykdo kelis vertimo blokus, parašo derinimo pranešimą, kurį palikau, kad RCU dabar prasidės ir... sugenda abort() funkcijos viduje free(). Tvarkydami funkciją free() Mums pavyko išsiaiškinti, kad krūvos bloko, esančio aštuoniuose baituose prieš paskirtą atmintį, antraštėje vietoj bloko dydžio ar panašiai buvo šiukšlių.

Krūvos sunaikinimas - kaip miela... Tokiu atveju yra naudinga priemonė - iš (jei įmanoma) tų pačių šaltinių surinkti gimtąjį dvejetainį ir paleisti jį pagal Valgrind. Po kurio laiko dvejetainis failas buvo paruoštas. Aš paleidžiu jį su tomis pačiomis parinktimis - jis sugenda net inicijavimo metu, kol iš tikrųjų nepasiekia vykdymo. Žinoma, nemalonu – matyt, šaltiniai buvo ne visai tie patys, kas nenuostabu, nes konfigūruoti išžvalgė šiek tiek kitokius variantus, bet aš turiu Valgrind – pirma ištaisysiu šią klaidą, o tada, jei pasiseks , pasirodys originalus. Aš vykdau tą patį su Valgrind... Y-y-y, y-y-y, uh-uh, jis prasidėjo, normaliai buvo inicijuotas ir praėjo pirminė klaida be jokio įspėjimo apie neteisingą prieigą prie atminties, jau nekalbant apie kritimus. Gyvenimas, kaip sakoma, manęs tam neparuošė – užstringanti programa nustoja strigti, kai paleidžiama pagal „Walgrind“. Kas tai buvo, yra paslaptis. Mano hipotezė yra tokia, kad kartą šalia dabartinės instrukcijos po avarijos inicijavimo metu gdb parodė darbą memset-a su galiojančia žymekliu, naudojant bet kurį mmx, arba xmm registrų, tada galbūt tai buvo kažkokia derinimo klaida, nors vis dar sunku patikėti.

Gerai, Valgrind čia nepadės. Ir čia prasidėjo šlykščiausias dalykas – atrodo, kad viskas net prasideda, bet sugenda dėl visiškai nežinomų priežasčių dėl įvykio, kuris galėjo įvykti prieš milijonus instrukcijų. Ilgą laiką net nebuvo aišku, kaip prieiti. Galų gale vis tiek turėjau sėdėti ir derinti. Išspausdinus tai, su kuo buvo perrašyta antraštė, paaiškėjo, kad tai atrodo ne kaip skaičius, o veikiau kažkokie dvejetainiai duomenys. Ir štai, ši dvejetainė eilutė buvo rasta BIOS faile – tai yra, dabar buvo galima pakankamai užtikrintai teigti, kad tai buferio perpildymas, ir net aišku, kad ji buvo įrašyta į šį buferį. Na, tada kažkas panašaus - Emscripten, laimei, nėra adresų erdvės atsitiktinio atskyrimo, joje taip pat nėra skylių, todėl galite įrašyti kur nors kodo viduryje, kad išvestumėte duomenis pagal žymeklį nuo paskutinio paleidimo, pažvelkite į duomenis, pažiūrėkite į rodyklę ir, jei jis nepasikeitė, gaukite peno apmąstymams. Tiesa, po bet kokio pakeitimo susiejimas užtrunka porą minučių, bet ką daryti? Dėl to buvo rasta konkreti eilutė, kuri nukopijavo BIOS iš laikinojo buferio į svečio atmintį - ir iš tikrųjų buferyje nebuvo pakankamai vietos. Radus to keisto buferio adreso šaltinį, atsirado funkcija qemu_anon_ram_alloc faile oslib-posix.c - logika buvo tokia: kartais gali būti naudinga sulygiuoti adresą į didžiulį 2 MB dydžio puslapį, todėl mes paprašysime mmap iš pradžių dar šiek tiek, o tada su pagalba grąžinsime perteklių munmap. Ir jei toks lygiavimas nereikalingas, mes nurodysime rezultatą, o ne 2 MB getpagesize() - mmap jis vis tiek išduos suderintą adresą... Taigi Emscripten mmap tik skambina malloc, bet, žinoma, jis nesutampa puslapyje. Apskritai klaida, kuri mane nuvylė porą mėnesių, buvo ištaisyta pakeitus du linijos.

Skambinimo funkcijų ypatybės

Ir dabar procesorius kažką skaičiuoja, Qemu nesugenda, bet ekranas neįsijungia, o procesorius greitai pereina į kilpas, sprendžiant iš išvesties -d exec,in_asm,out_asm. Iškilo hipotezė: laikmačio pertraukimai (arba apskritai visi pertraukimai) neateina. Ir iš tiesų, jei atsuksite pertraukas nuo gimtojo surinkimo, kuris dėl kokių nors priežasčių veikė, gausite panašų vaizdą. Tačiau tai nebuvo atsakymas: palyginus gautus pėdsakus su aukščiau pateikta parinktimi, paaiškėjo, kad vykdymo trajektorijos labai anksti išsiskyrė. Čia reikia pasakyti, kad palyginimas to, kas buvo įrašyta naudojant paleidimo priemonę emrun išvesties derinimas naudojant savosios rinkinio išvestį nėra visiškai mechaninis procesas. Tiksliai nežinau, kaip jungiasi naršyklėje veikianti programa emrun, tačiau kai kurios išvesties eilutės yra pertvarkytos, todėl skirtumo skirtumas dar nėra priežastis manyti, kad trajektorijos išsiskyrė. Apskritai tapo aišku, kad pagal instrukcijas ljmpl vyksta perėjimas prie skirtingų adresų, o generuojamas baito kodas iš esmės skiriasi: viename yra nurodymas iškviesti pagalbinę funkciją, kitame – ne. Pagilinus su instrukcijomis ir išstudijavus šias instrukcijas verčiantį kodą, paaiškėjo, kad, pirma, iškart prieš jį registre cr0 buvo padarytas įrašas – taip pat pasitelkus pagalbininką – kuris perjungė procesorių į apsaugotą režimą, antra, kad js versija niekada nepersijungė į apsaugotą režimą. Tačiau faktas yra tas, kad kita Emscripten ypatybė yra jos nenoras toleruoti kodą, pavyzdžiui, instrukcijų įgyvendinimą. call TCI, kurią bet kuri funkcijos rodyklė lemia tipą long long f(int arg0, .. int arg9) - funkcijos turi būti iškviestos su teisingu argumentų skaičiumi. Pažeidus šią taisyklę, priklausomai nuo derinimo nustatymų, programa arba sugenda (tai yra gerai), arba išvis iškvies netinkamą funkciją (tai bus liūdna derinti). Taip pat yra ir trečias variantas – įjungti įvynioklių generavimą, kurie prideda/pašalina argumentus, tačiau iš viso šie įvyniojimai užima daug vietos, nepaisant to, kad iš tikrųjų man reikia tik šiek tiek daugiau nei šimto įvyniojimų. Jau vien dėl to labai liūdna, bet pasirodė rimtesnė bėda: sugeneruotame wrapper funkcijų kode buvo konvertuojami ir konvertuojami argumentai, bet kartais funkcija su sugeneruotais argumentais nebuvo iškviesta - na, kaip ir mano libffi įgyvendinimas. Tai yra, kai kuriems pagalbininkams tiesiog nebuvo įvykdyta mirties bausmė.

Laimei, „Qemu“ turi mašininiu būdu nuskaitomus pagalbininkų sąrašus antraštės failo pavidalu

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

Jie naudojami gana juokingai: pirma, makrokomandos iš naujo apibrėžiamos pačiu keisčiausiu būdu DEF_HELPER_n, o tada įsijungia helper.h. Tiek, kiek makrokomanda išplečiama į struktūros iniciatorių ir kablelį, tada apibrėžiamas masyvas, o vietoj elementų - #include <helper.h> Dėl to pagaliau turėjau galimybę išbandyti biblioteką darbe pyparsing, ir buvo parašytas scenarijus, kuris sugeneruoja būtent tuos įpakavimus būtent toms funkcijoms, kurioms jie reikalingi.

Ir taip, po to procesorius atrodė, kad veikė. Panašu, kad taip yra dėl to, kad ekranas niekada nebuvo inicijuotas, nors memtest86+ galėjo veikti pradinėje komplektacijoje. Čia reikia patikslinti, kad Qemu bloko įvesties/išvesties kodas rašomas korutinėmis. „Emscripten“ turi savo labai sudėtingą įgyvendinimą, tačiau jį vis tiek reikėjo palaikyti „Qemu“ kode ir dabar galite derinti procesorių: „Qemu“ palaiko parinktis -kernel, -initrd, -append, su kuria galite paleisti Linux arba, pavyzdžiui, memtest86+, visiškai nenaudodami blokavimo įrenginių. Bet čia yra problema: savajame rinkinyje galima pamatyti Linux branduolio išvestį į konsolę su parinktimi -nographic, ir jokios išvesties iš naršyklės į terminalą, iš kurio jis buvo paleistas emrun, neatėjo. Tai yra, neaišku: neveikia procesorius arba neveikia grafikos išvestis. Ir tada man kilo mintis šiek tiek palaukti. Paaiškėjo, kad „procesorius nemiega, o tiesiog lėtai mirksi“, o maždaug po penkių minučių branduolys išmetė krūvą pranešimų į konsolę ir toliau kabo. Tapo aišku, kad procesorius apskritai veikia, ir mums reikia įsigilinti į kodą darbui su SDL2. Deja, aš nemoku naudotis šia biblioteka, todėl kai kuriose vietose teko veikti atsitiktinai. Tam tikru momentu ekrane mėlyname fone blykstelėjo linija lygiagrečiai 0, o tai sukėlė kai kurių minčių. Galų gale paaiškėjo, kad problema buvo ta, kad „Qemu“ atidaro kelis virtualius langus viename fiziniame lange, tarp kurių galite perjungti naudodami „Ctrl-Alt-n“: tai veikia vietinėje versijoje, bet ne „Emscripten“. Atsikratę nereikalingų langų naudodami parinktis -monitor none -parallel none -serial none ir nurodymai priverstinai perpiešti visą ekraną kiekviename kadre, viskas staiga suveikė.

Korutinos

Taigi, emuliacija naršyklėje veikia, tačiau joje negalite paleisti nieko įdomaus viename diskelyje, nes nėra blokinio I/O - reikia įdiegti korutinų palaikymą. „Qemu“ jau turi keletą pagrindinių programų, tačiau dėl „JavaScript“ ir „Emscripten“ kodų generatoriaus prigimties negalite tiesiog pradėti žongliruoti krūvomis. Atrodytų, kad „visko nebėra, tinkas nuimamas“, bet „Emscripten“ kūrėjai jau viskuo pasirūpino. Tai įgyvendinta gana juokingai: pavadinkime tokį funkcijos iškvietimą įtartinu emscripten_sleep ir keli kiti, naudojantys Asyncify mechanizmą, taip pat žymiklio iškvietimai ir iškvietimai į bet kurią funkciją, kai vienas iš dviejų ankstesnių atvejų gali atsirasti toliau kamino apačioje. O dabar prieš kiekvieną įtartiną skambutį parinksime asinchroninį kontekstą, o iškart po skambučio patikrinsime ar neįvyko asinchroninis skambutis, o jei įvyko, šiame asinchroniniame kontekste išsaugosime visus vietinius kintamuosius, nurodysime, kuri funkcija norėdami perkelti valdymą, kai reikia tęsti vykdymą, ir išeiti iš dabartinės funkcijos. Čia yra erdvės tirti poveikį švaistymas — norint tęsti kodo vykdymą grįžus iš asinchroninio iškvietimo, kompiliatorius sugeneruoja funkcijos, prasidėjusios po įtartino iškvietimo, „stubus“ – taip: jei yra n įtartinų iškvietimų, tada funkcija bus išplėsta kažkur n/2 kartų – tai vis tiek, jei ne. Atminkite, kad po kiekvieno potencialiai asinchroninio skambučio turite pridėti kai kurių vietinių kintamųjų išsaugojimą prie pradinės funkcijos. Vėliau net turėjau parašyti paprastą scenarijų Python, kuris, remiantis tam tikru ypač per daug naudojamų funkcijų rinkiniu, tariamai „neleidžia asinchronijai pereiti per save“ (tai yra, kamino reklama ir viskas, ką ką tik aprašiau, nėra dirbti juose), nurodo iškvietimus per nuorodas, kurių funkcijų kompiliatorius turėtų nepaisyti, kad šios funkcijos nebūtų laikomos asinchroninėmis. Ir tada JS failų, kurių dydis mažesnis nei 60 MB, yra aiškiai per daug – tarkime, bent 30. Nors kažkada kūriau surinkimo scenarijų ir netyčia išmečiau linkerio parinktis, tarp kurių buvo -O3. Paleidžiu sugeneruotą kodą, o „Chromium“ eikvoja atmintį ir sugenda. Tada netyčia pažiūrėjau, ką jis bando parsisiųsti... Na, ką aš galiu pasakyti, aš irgi būčiau sušalęs, jei manęs būtų paprašyta apgalvotai išstudijuoti ir optimizuoti 500+ MB Javascript.

Deja, „Asyncify“ palaikymo bibliotekos kodo patikrinimai nebuvo visiškai draugiški longjmp-s, kurie naudojami virtualaus procesoriaus kode, bet po nedidelio pataisymo, kuris išjungia šiuos patikrinimus ir jėga atkuria kontekstus, tarsi viskas būtų gerai, kodas veikė. Ir tada prasidėjo keistas dalykas: kartais suveikė sinchronizavimo kodo patikros - tie patys, kurie sulaužo kodą, jei pagal vykdymo logiką jis turėtų būti užblokuotas - kažkas bandė patraukti jau užfiksuotą mutexą. Laimei, tai pasirodė ne logiška serializuoto kodo problema – tiesiog naudojau standartinę Emscripten teikiamą pagrindinės kilpos funkcionalumą, tačiau kartais asinchroninis skambutis visiškai išvyniodavo steką ir tuo metu nepavykdavo. setTimeout iš pagrindinės kilpos - taigi kodas įėjo į pagrindinės ciklo iteraciją, nepalikdamas ankstesnės iteracijos. Perrašė ant begalinės kilpos ir emscripten_sleep, ir problemos su mutexes nutrūko. Kodas netgi tapo logiškesnis - juk iš tikrųjų aš neturiu kodo, kuris paruoštų kitą animacijos kadrą - procesorius tiesiog kažką apskaičiuoja ir ekranas periodiškai atnaujinamas. Tačiau problemos tuo nesibaigė: kartais Qemu vykdymas tiesiog nutrūkdavo tyliai, be jokių išimčių ar klaidų. Tuo metu aš jo atsisakiau, bet žvelgdamas į priekį, pasakysiu, kad problema buvo tokia: korutinos kodas iš tikrųjų nenaudoja setTimeout (arba bent jau ne taip dažnai, kaip manote): funkcija emscripten_yield tiesiog nustato asinchroninio skambučio vėliavėlę. Visa esmė ta emscripten_coroutine_next nėra asinchroninė funkcija: viduje ji patikrina vėliavėlę, atkuria ją ir perduoda valdymą ten, kur reikia. Tai yra, krūvos reklama tuo ir baigiasi. Problema buvo ta, kad dėl naudojimo po nemokamo naudojimo, kuris atsirado, kai korutinos baseinas buvo išjungtas dėl to, kad nenukopijavau svarbios kodo eilutės iš esamos korutinos užpakalinės programos, funkcija qemu_in_coroutine grąžino teisingą, nors iš tikrųjų turėjo būti klaidinga. Tai paskatino skambutį emscripten_yield, virš kurio ant rietuvės nebuvo nė vieno emscripten_coroutine_next, rietuvė išsiskleidė iki pat viršaus, bet ne setTimeout, kaip jau sakiau, nebuvo eksponuojamas.

JavaScript kodo generavimas

Ir čia, tiesą sakant, yra pažadėtas „maltos mėsos apvertimas atgal“. Ne visai. Žinoma, jei naršyklėje paleisime Qemu, o joje – Node.js, tai, žinoma, po kodo sugeneravimo Qemu gausime visiškai neteisingą JavaScript. Bet vis tiek kažkokia atvirkštinė transformacija.

Pirma, šiek tiek apie tai, kaip veikia Qemu. Iš karto atleiskite: nesu profesionalus Qemu kūrėjas ir mano išvados kai kur gali būti klaidingos. Kaip sakoma, „mokinio nuomonė neturi sutapti su mokytojo nuomone, Peano aksiomatika ir sveiku protu“. „Qemu“ turi tam tikrą skaičių palaikomų svečių architektūrų ir kiekvienai jų yra panašus katalogas target-i386. Kurdami galite nurodyti kelių svečių architektūrų palaikymą, tačiau rezultatas bus tik keli dvejetainiai. Kodas, skirtas palaikyti svečių architektūrą, savo ruožtu generuoja kai kurias vidines Qemu operacijas, kurias TCG (Tiny Code Generator) jau paverčia pagrindinio kompiuterio architektūros mašininiu kodu. Kaip nurodyta tcg kataloge esančiame readme faile, tai iš pradžių buvo įprasto C kompiliatoriaus dalis, kuri vėliau buvo pritaikyta JIT. Todėl, pavyzdžiui, tikslinė architektūra pagal šį dokumentą nebėra svečių, o pagrindinio kompiuterio architektūra. Tam tikru momentu pasirodė kitas komponentas - Tiny Code Interpreter (TCI), kuris turėtų vykdyti kodą (beveik tas pačias vidines operacijas), jei nėra kodų generatoriaus, skirto konkrečiai pagrindinio kompiuterio architektūrai. Tiesą sakant, kaip teigiama dokumentacijoje, šis vertėjas ne visada gali veikti taip gerai, kaip JIT kodų generatorius, ne tik kiekybiškai, kalbant apie greitį, bet ir kokybiškai. Nors nesu tikras, kad jo aprašymas visiškai tinkamas.

Iš pradžių bandžiau sukurti visavertį TCG backendą, bet greitai susipainiojau šaltinio kode ir ne visai aiškaus baito kodo nurodymų aprašyme, todėl nusprendžiau apvynioti TCI interpretatorių. Tai suteikė keletą privalumų:

  • diegiant kodų generatorių, galėjai žiūrėti ne į instrukcijų aprašymą, o į interpretatoriaus kodą
  • Galite generuoti funkcijas ne kiekvienam pasitaikiusiam vertimo blokui, o, pavyzdžiui, tik po šimto vykdymo
  • pasikeitus sugeneruotam kodui (o tai atrodo įmanoma, sprendžiant iš funkcijų su pavadinimais, kuriuose yra žodis patch), reikės anuliuoti sugeneruotą JS kodą, bet bent jau turėsiu iš ko jį atkurti

Dėl trečiojo punkto nesu tikras, kad pataisymas įmanomas pirmą kartą paleidus kodą, tačiau pakanka pirmųjų dviejų punktų.

Iš pradžių kodas buvo generuojamas kaip didelis jungiklis originalios baito instrukcijos adresu, bet vėliau, prisiminęs straipsnį apie Emscripten, sugeneruoto JS optimizavimą ir perkūrimą, nusprendžiau sugeneruoti daugiau žmogaus kodo, juolab kad empiriškai tai pasirodė, kad vienintelis įėjimo taškas į vertimo bloką yra jo pradžia. Netrukus pasakyta, po kurio laiko turėjome kodų generatorių, kuris sugeneravo kodą su ifs (nors ir be kilpų). Tačiau nepasisekė, jis sudužo, pranešdamas, kad instrukcijos buvo netinkamo ilgio. Be to, paskutinė šio rekursijos lygio instrukcija buvo brcond. Gerai, aš pridėsiu identišką patikrinimą prie šios instrukcijos generavimo prieš ir po rekursinio skambučio ir... ne vienas iš jų nebuvo įvykdytas, bet po assert perjungimo vis tiek nepavyko. Galų gale, išstudijavus sugeneruotą kodą, supratau, kad po perjungimo žymeklis į esamą instrukciją perkraunamas iš kamino ir tikriausiai perrašomas sugeneruoto JavaScript kodo. Ir taip išėjo. Buferio padidinimas nuo vieno megabaito iki dešimties nieko neprivedė ir tapo aišku, kad kodų generatorius sukasi ratu. Turėjome patikrinti, ar neperžengėme dabartinės TB ribų, o jei neperžengėme, tada išduoti kitos TB adresą su minuso ženklu, kad galėtume tęsti vykdymą. Be to, tai išsprendžia problemą „kurios sugeneruotos funkcijos turėtų būti pripažintos negaliojančiomis, jei pasikeitė šis baito kodas? — reikia pripažinti negaliojančia tik tą funkciją, kuri atitinka šį vertimo bloką. Beje, nors viską derinau „Chromium“ programoje (kadangi naudoju „Firefox“ ir man lengviau eksperimentams naudoti atskirą naršyklę), „Firefox“ padėjo ištaisyti nesuderinamumą su asm.js standartu, po kurio kodas pradėjo veikti greičiau. Chromas.

Sukurto kodo pavyzdys

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

išvada

Taigi, darbas vis dar nebaigtas, bet pavargau slapčia tobulinti šią ilgalaikę konstrukciją. Todėl nusprendžiau paskelbti tai, ką dabar turiu. Kodas vietomis yra šiek tiek baisus, nes tai yra eksperimentas ir iš anksto neaišku, ką reikia padaryti. Tikriausiai tada verta išleisti įprastus atominius įsipareigojimus ant modernesnės Qemu versijos. Tuo tarpu Gitoje yra tinklaraščio formato gija: kiekvienam bent kažkaip įveiktam „lygiui“ pridėtas išsamus komentaras rusų kalba. Tiesą sakant, šis straipsnis didžiąja dalimi yra išvados perpasakojimas git log.

Galite išbandyti viską čia (saugokitės eismo).

Kas jau veikia:

  • Veikia x86 virtualus procesorius
  • Yra veikiantis JIT kodo generatoriaus prototipas iš mašininio kodo į JavaScript
  • Yra šablonas kitoms 32 bitų svečių architektūroms surinkti: šiuo metu galite grožėtis „Linux“, nes MIPS architektūra užstringa naršyklėje įkėlimo etape.

Ką dar gali padaryti

  • Paspartinkite emuliaciją. Net JIT režimu atrodo, kad jis veikia lėčiau nei „Virtual x86“ (tačiau potencialiai yra visas „Qemu“ su daugybe emuliuotos aparatinės įrangos ir architektūrų)
  • Norėdami sukurti įprastą sąsają – atvirai kalbant, nesu geras žiniatinklio kūrėjas, todėl kol kas kiek galėdamas perdariau standartinį Emscripten apvalkalą
  • Pabandykite paleisti sudėtingesnes Qemu funkcijas – tinklų kūrimą, VM perkėlimą ir kt.
  • UPD: turėsite pateikti savo keletą patobulinimų ir klaidų ataskaitas Emscripten prieš srovę, kaip tai padarė ankstesni Qemu ir kitų projektų nešėjai. Dėkoju jiems už galimybę netiesiogiai panaudoti savo indėlį į Emscripten kaip mano užduoties dalį.

Šaltinis: www.habr.com

Добавить комментарий