Qemu.js JIT-tuella: voit silti kääntää jauhelihaa taaksepäin

Muutama vuosi sitten Fabrice Bellard kirjoittanut jslinux on JavaScriptillä kirjoitettu PC-emulaattori. Sen jälkeen oli ainakin enemmän Virtuaalinen x86. Mutta tietääkseni ne kaikki olivat tulkkeja, kun taas saman Fabrice Bellardin paljon aikaisemmin kirjoittama Qemu ja luultavasti mikä tahansa itseään kunnioittava moderni emulaattori käyttää JIT-käännöstä vieraskoodista isäntäjärjestelmän koodiksi. Minusta tuntui, että oli aika toteuttaa päinvastainen tehtävä suhteessa siihen, jonka selaimet ratkaisevat: konekoodin JIT-kääntäminen JavaScriptiksi, jota varten Qemu-portti tuntui loogisimmalta. Näyttää siltä, ​​miksi Qemu, on olemassa yksinkertaisempia ja käyttäjäystävällisempiä emulaattoreita - sama VirtualBox, esimerkiksi - asennettu ja toimii. Mutta Qemulla on useita mielenkiintoisia ominaisuuksia

  • avoin lähdekoodi
  • kyky työskennellä ilman ydinohjainta
  • kyky työskennellä tulkkitilassa
  • tuki useille isäntä- ja vierasarkkitehtuureille

Kolmannen kohdan osalta voin nyt selittää, että itse asiassa TCI-tilassa ei tulkita itse vieraskoneen käskyjä, vaan niistä saatu tavukoodi, mutta tämä ei muuta oleellista - rakentamista ja suorittamista varten. Qemu uudella arkkitehtuurilla, jos olet onnekas, C-kääntäjä riittää - koodigeneraattorin kirjoittamista voi lykätä.

Ja nyt, kahden vuoden rauhassa vapaa-ajalla Qemu-lähdekoodin parissa puuhailun jälkeen, ilmestyi toimiva prototyyppi, jossa voit jo käyttää esimerkiksi Kolibri OS:ää.

Mikä on Emscripten

Nykyään on ilmestynyt monia kääntäjiä, joiden lopputuloksena on JavaScript. Jotkut, kuten Type Script, oli alun perin tarkoitettu parhaaksi tapaksi kirjoittaa verkkoon. Samalla Emscripten on tapa ottaa olemassa oleva C- tai C++-koodi ja kääntää se selaimen luettavaan muotoon. Päällä tämä sivu Olemme koonneet monia tunnettujen ohjelmien portteja: täälläVoit esimerkiksi katsoa PyPyä - muuten he väittävät, että heillä on jo JIT. Itse asiassa kaikkia ohjelmia ei voi yksinkertaisesti kääntää ja ajaa selaimessa - niitä on useita ominaisuudet, jonka kanssa sinun on kuitenkin siedettävä, sillä samalla sivulla oleva teksti sanoo "Emscriptenillä voidaan kääntää melkein mikä tahansa kannettava C/C++-koodi JavaScriptiin". Toisin sanoen on olemassa joukko toimintoja, jotka ovat standardin mukaan määrittelemätöntä toimintaa, mutta toimivat yleensä x86:ssa - esimerkiksi kohdistamaton pääsy muuttujiin, mikä on yleensä kielletty joissakin arkkitehtuureissa. Yleisesti , Qemu on cross-platform-ohjelma ja halusin uskoa, että se ei jo sisällä paljon määrittelemätöntä käyttäytymistä - ota se ja käännä ja sitten vähän JIT:tä - ja olet valmis! Mutta se ei ole tapaus...

Yritä ensin

Yleisesti ottaen en ole ensimmäinen henkilö, joka on keksinyt ajatuksen Qemun siirtämisestä JavaScriptiin. ReactOS-foorumilla kysyttiin, onko tämä mahdollista Emscriptenillä. Jo aikaisemmin oli huhuja, että Fabrice Bellard teki tämän henkilökohtaisesti, mutta puhuimme jslinuxista, joka tietääkseni on vain yritys saavuttaa manuaalisesti riittävä suorituskyky JS:ssä ja kirjoitettiin tyhjästä. Myöhemmin kirjoitettiin Virtual x86 - sille lähetettiin hämärtämättömät lähteet, ja kuten todettiin, emuloinnin suurempi "realismi" mahdollisti SeaBIOSin käytön laiteohjelmistona. Lisäksi Qemua yritettiin siirtää ainakin kerran Emscriptenillä - yritin tehdä tätä pistorasiapari, mutta ymmärrykseni kehitys oli jäätynyt.

Joten näyttää siltä, ​​​​että tässä ovat lähteet, tässä on Emscripten - ota se ja kokoa. Mutta on myös kirjastoja, joista Qemu on riippuvainen, ja kirjastoja, joista nuo kirjastot ovat riippuvaisia ​​jne., ja yksi niistä on libffi, josta häiriö riippuu. Internetissä huhuttiin, että Emscriptenin suuressa kirjastojen porttien kokoelmassa olisi sellainen, mutta sitä oli jotenkin vaikea uskoa: ensinnäkään sitä ei ollut tarkoitettu uudeksi kääntäjäksi, toiseksi se oli liian matalatasoinen kirjasto vain poimia ja kääntää JS. Eikä kyse ole vain kokoonpanolisäkkeistä - luultavasti, jos käännät sitä, voit joillekin kutsumiskäytännöille luoda tarvittavat argumentit pinoon ja kutsua funktiota ilman niitä. Mutta Emscripten on hankala asia: jotta luotu koodi näyttäisi tutulta selaimen JS-moottorin optimoijalle, käytetään joitain temppuja. Erityisesti ns. relooping - koodigeneraattori, joka käyttää vastaanotettua LLVM IR:ää joidenkin abstraktien siirtymäkäskyjen kera, yrittää luoda uskottavia if-lauseita, silmukoita jne. No, miten argumentit välitetään funktiolle? Luonnollisesti argumentteina JS-funktioille, eli jos mahdollista, ei pinon kautta.

Alussa oli ajatus vain kirjoittaa libffin korvaaminen JS:llä ja tehdä vakiotestejä, mutta loppujen lopuksi jäin hämmentyneeksi, miten saisin otsikkotiedostot toimimaan olemassa olevan koodin kanssa - mitä voin tehdä? kuten he sanovat: "Ovatko tehtävät niin monimutkaisia ​​"Olemmeko niin tyhmiä?" Minun piti siirtää libffi niin sanotusti toiseen arkkitehtuuriin - onneksi Emscriptenillä on sekä makrot inline-kokoonpanoa varten (Javascriptissä kyllä ​​- no, arkkitehtuurista mikä tahansa, joten kokoonpanija) että kyky ajaa lennossa luotua koodia. Yleisesti ottaen alustariippuvaisten libffi-fragmenttien parissa jonkin aikaa puuhailtuani sain käännettävää koodia ja suoritin sen ensimmäisellä törmäsilläni. Yllätyksekseni testi onnistui. Hämmästyneenä neroudestani - ei vitsi, se toimi ensimmäisestä käynnistyksestä lähtien - minä, en edelleenkään uskonut silmiäni, menin katsomaan tuloksena olevaa koodia uudelleen arvioidakseni, mihin kaivaa seuraavaksi. Tässä menin hulluksi toisen kerran - ainoa asia, jonka toimintoni teki, oli ffi_call - tämä ilmoitti onnistuneesta puhelusta. Itse soittoa ei tullut. Joten lähetin ensimmäisen vetopyyntöni, joka korjasi testin virheen, joka oli selvä kaikille olympialaisten oppilaille - todellisia lukuja ei pidä verrata a == b ja jopa kuinka a - b < EPS - sinun on myös muistettava moduuli, muuten 0 on hyvin yhtä suuri kuin 1/3... Yleensä keksin tietyn libffin portin, joka läpäisee yksinkertaisimmat testit ja jolla glib on koottu - päätin, että se olisi tarpeen, lisään sen myöhemmin. Tulevaisuudessa sanon, että kuten kävi ilmi, kääntäjä ei edes sisällyttänyt libffi-funktiota lopulliseen koodiin.

Mutta kuten jo sanoin, on joitain rajoituksia, ja erilaisten määrittelemättömien käyttäytymismallien vapaan käytön joukossa on piilotettu epämiellyttävämpi ominaisuus - JavaScript ei tue monisäikeistystä jaetun muistin kanssa. Periaatteessa tätä voidaan yleensä kutsua jopa hyväksi ideaksi, mutta ei C-säikeisiin sidotun koodin siirtämiseen. Yleisesti ottaen Firefox kokeilee jaettujen työntekijöiden tukemista, ja Emscriptenillä on heille pthread-toteutus, mutta en halunnut olla riippuvainen siitä. Minun piti hitaasti kitkeä monisäikeisyys Qemu-koodista - eli selvittää, missä säikeet kulkevat, siirtää tässä säikeessä käynnissä olevan silmukan runko erilliseksi funktioksi ja kutsua sellaisia ​​​​funktioita yksitellen pääsilmukasta.

Toinen yritys

Jossain vaiheessa kävi selväksi, että ongelma oli edelleen olemassa ja että satunnainen kainalosauvojen työntäminen koodin ympärille ei johda mihinkään. Johtopäätös: meidän on jotenkin systematisoitava kainalosauvojen lisäämisprosessi. Siksi otettiin tuolloin tuore versio 2.4.1 (ei 2.5.0, koska kuka tietää, uudessa versiossa tulee olemaan bugeja, joita ei ole vielä saatu kiinni, ja minulla on tarpeeksi omia bugejani ), ja ensimmäinen asia oli kirjoittaa se uudelleen turvallisesti thread-posix.c. Eli turvallista: jos joku yritti suorittaa tukkoon johtavan toimenpiteen, toiminto kutsuttiin välittömästi abort() - Tämä ei tietenkään ratkaissut kaikkia ongelmia kerralla, mutta ainakin se oli jotenkin mukavampaa kuin hiljainen ristiriitaisten tietojen vastaanottaminen.

Yleensä Emscripten-vaihtoehdot ovat erittäin hyödyllisiä koodin siirtämisessä JS:ään -s ASSERTIONS=1 -s SAFE_HEAP=1 - ne havaitsevat tietyntyyppiset määrittelemättömät käyttäytymiset, kuten kutsut tasaamattomaan osoitteeseen (joka ei ole ollenkaan yhdenmukainen kirjoitettujen taulukoiden koodin kanssa, kuten HEAP32[addr >> 2] = 1) tai kutsumalla funktiota väärällä määrällä argumentteja.

Muuten, kohdistusvirheet ovat erillinen ongelma. Kuten jo totesin, Qemulla on "degeneroitunut" tulkintatausta koodin generointiin TCI (pieni kooditulkki), ja Qemun rakentamiseen ja käyttämiseen uudella arkkitehtuurilla, jos olet onnekas, riittää C-kääntäjä. Avainsanat "jos olet onnekas". Minulla oli epäonnea, ja kävi ilmi, että TCI käyttää tasaamatonta pääsyä jäsentäessään tavukoodiaan. Toisin sanoen kaikenlaisissa ARM- ja muissa arkkitehtuureissa, joissa on välttämättä tasoitettu pääsy, Qemu kääntää, koska niillä on normaali TCG-taustajärjestelmä, joka tuottaa alkuperäistä koodia, mutta toimiiko TCI niillä, on toinen kysymys. Kuitenkin, kuten kävi ilmi, TCI:n dokumentaatio osoitti selvästi jotain vastaavaa. Tämän seurauksena koodiin lisättiin toimintokutsuja kohdistamattomaan lukemiseen, jotka löytyivät toisesta Qemu-osasta.

Kasan tuhoaminen

Tämän seurauksena epätasainen pääsy TCI:hen korjattiin, luotiin pääsilmukka, joka puolestaan ​​kutsui prosessoria, RCU:ta ja joitain muita pieniä asioita. Ja niin käynnistän Qemun vaihtoehdolla -d exec,in_asm,out_asm, mikä tarkoittaa, että sinun on sanottava, mitä koodilohkoja suoritetaan, ja myös lähetyksen yhteydessä kirjoittaaksesi, mikä vieraskoodi oli, mikä isäntäkoodista tuli (tässä tapauksessa tavukoodi). Se käynnistyy, suorittaa useita käännöslohkoja, kirjoittaa jättämäni virheenkorjausviestin, että RCU käynnistyy nyt ja... kaatuu abort() funktion sisällä free(). Huijaamalla toimintoa free() Onnistuimme havaitsemaan, että kasalohkon otsikossa, joka sijaitsee varattua muistia edeltäneissä kahdeksassa tavussa, oli lohkokoon tai vastaavan sijasta roskaa.

Kasan tuhoaminen - kuinka söpöä... Tällaisessa tapauksessa on hyödyllinen lääke - (jos mahdollista) samoista lähteistä, kokoa natiivi binaari ja suorita se Valgrindin alla. Jonkin ajan kuluttua binaari oli valmis. Käynnistän sen samoilla vaihtoehdoilla - se kaatuu jopa alustuksen aikana, ennen kuin toteutuu. Se on tietysti epämiellyttävää - ilmeisesti lähteet eivät olleet aivan samat, mikä ei ole yllättävää, koska konfiguraatiossa selvitettiin hieman erilaisia ​​vaihtoehtoja, mutta minulla on Valgrind - ensin korjaan tämän bugin ja sitten, jos olen onnekas , alkuperäinen tulee näkyviin. Ajan samaa asiaa Valgrindin alla... Y-y-y, y-y-y, öh, se alkoi, meni alustuksen läpi normaalisti ja eteni alkuperäisen bugin ohi ilman varoitusta virheellisestä muistin käytöstä, putoamisesta puhumattakaan. Elämä, kuten sanotaan, ei valmistanut minua tähän - kaatuva ohjelma lakkaa kaatumasta, kun se käynnistetään Walgrindin alla. Mikä se oli, on mysteeri. Hypoteesini on, että kerran nykyisen käskyn läheisyydessä alustuksen aikana tapahtuneen kaatumisen jälkeen gdb näytti toimivan memset-a kelvollisella osoittimella käyttämällä jompaakumpaa mmx, tai xmm rekistereihin, niin ehkä se oli jonkinlainen kohdistusvirhe, vaikka sitä on edelleen vaikea uskoa.

Okei, Valgrind ei näytä auttavan tässä. Ja tästä alkoi inhottavin - kaikki näyttää jopa alkavan, mutta kaatuu täysin tuntemattomista syistä johtuen tapahtumasta, joka olisi voinut tapahtua miljoonia ohjeita sitten. Pitkään aikaan ei ollut edes selvää, miten lähestyä. Lopulta minun piti silti istua alas ja korjata virheitä. Tulostaminen, millä otsikko kirjoitettiin uudelleen, osoitti, että se ei näyttänyt numerolta, vaan jonkinlaisesta binääritiedosta. Ja katso ja katso, tämä binäärimerkkijono löytyi BIOS-tiedostosta - eli nyt voitiin kohtuullisella varmuudella sanoa, että se oli puskurin ylivuoto, ja on jopa selvää, että se kirjoitettiin tähän puskuriin. No sitten jotain tämän kaltaista - Emscriptenissä ei onneksi ole osoiteavaruuden satunnaistamista, siinäkään ei ole reikiä, joten voit kirjoittaa jonnekin koodin keskelle tulostaaksesi dataa osoittimella viimeisestä käynnistyksestä, katso dataa, katso osoitinta ja jos se ei ole muuttunut, hanki ajattelun aihetta. Totta, linkin muodostaminen kestää muutaman minuutin muutoksen jälkeen, mutta mitä voit tehdä? Tämän seurauksena löydettiin erityinen rivi, joka kopioi BIOSin väliaikaisesta puskurista vierasmuistiin - ja puskurissa ei todellakaan ollut tarpeeksi tilaa. Tuon oudon puskuriosoitteen lähteen löytäminen johti funktioon qemu_anon_ram_alloc tiedostossa oslib-posix.c - logiikka oli seuraava: joskus voi olla hyödyllistä kohdistaa osoite valtavalle 2 Mt:n sivulle, tätä varten kysymme mmap ensin vähän lisää, ja sitten palautamme ylimääräisen avun avulla munmap. Ja jos tällaista kohdistusta ei vaadita, ilmoitamme tuloksen 2 megatavun sijasta getpagesize() - mmap se antaa silti tasatun osoitteen... Eli Emscriptenissä mmap vain soittaa malloc, mutta se ei tietenkään sovi sivulle. Yleensä pari kuukautta turhauttanut bugi korjattiin muutoksella двух rivit.

Soittotoimintojen ominaisuudet

Ja nyt prosessori laskee jotain, Qemu ei kaatu, mutta näyttö ei käynnisty, ja prosessori menee nopeasti silmukoihin lähdön perusteella päätellen -d exec,in_asm,out_asm. On syntynyt hypoteesi: ajastinkeskeytykset (tai yleensä kaikki keskeytykset) eivät tule perille. Ja todellakin, jos irrotat keskeytykset alkuperäisestä kokoonpanosta, joka jostain syystä toimi, saat samanlaisen kuvan. Mutta tämä ei ollut vastaus ollenkaan: yllä olevalla vaihtoehdolla annettujen jälkien vertailu osoitti, että teloitusreitit erosivat hyvin aikaisin. Tässä on sanottava, että vertailu siitä, mitä on tallennettu kantoraketilla emrun virheenkorjaus alkuperäisen kokoonpanon tulosteen kanssa ei ole täysin mekaaninen prosessi. En tiedä tarkalleen, miten selaimessa käynnissä oleva ohjelma muodostaa yhteyden emrun, mutta jotkut ulostulon rivit osoittautuvat uudelleenjärjestetyiksi, joten ero ero ei vielä ole syy olettaa, että liikeradat ovat hajonneet. Yleisesti kävi selväksi, että ohjeiden mukaan ljmpl tapahtuu siirtymä eri osoitteisiin, ja luotu tavukoodi on pohjimmiltaan erilainen: yksi sisältää käskyn kutsua aputoimintoa, toinen ei. Googlaamalla ohjeita ja tutkittuaan koodia, joka kääntää nämä ohjeet, kävi selväksi, että ensinnäkin välittömästi ennen sitä rekisterissä cr0 tehtiin - myös auttajalla - nauhoitus, joka vaihtoi prosessorin suojattuun tilaan, ja toiseksi, että js-versio ei koskaan siirtynyt suojattuun tilaan. Mutta tosiasia on, että toinen Emscriptenin ominaisuus on sen haluttomuus sietää koodia, kuten ohjeiden toteuttamista. call TCI:ssä, jonka mikä tahansa toimintoosoitin johtaa tyyppiin long long f(int arg0, .. int arg9) - funktioita on kutsuttava oikealla määrällä argumentteja. Jos tätä sääntöä rikotaan, virheenkorjausasetuksista riippuen ohjelma joko kaatuu (mikä on hyvä) tai kutsuu väärää toimintoa ollenkaan (mitä on surullista jäljittää). On myös kolmas vaihtoehto - ota käyttöön kääreiden luominen, jotka lisäävät / poistavat argumentteja, mutta yhteensä nämä kääreet vievät paljon tilaa huolimatta siitä, että itse asiassa tarvitsen vain hieman yli sata käärettä. Pelkästään tämä on erittäin surullista, mutta siinä ilmeni vakavampi ongelma: kääretoimintojen luodussa koodissa argumentit muunnettiin ja muunnettiin, mutta joskus funktiota generoiduilla argumenteilla ei kutsuttu - no, aivan kuten minun libffi-toteutukseni. Eli joitain auttajia ei yksinkertaisesti teloitettu.

Onneksi Qemulla on koneellisesti luettavia auttajaluetteloita otsikkotiedoston muodossa, kuten

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

Niitä käytetään varsin hauskoina: ensinnäkin makrot määritellään uudelleen mitä oudoimmalla tavalla DEF_HELPER_nja kytkeytyy sitten päälle helper.h. Siinä määrin kuin makro laajennetaan rakenteen alustajaksi ja pilkuksi ja sitten määritetään matriisi, ja elementtien sijaan - #include <helper.h> Tämän seurauksena minulla oli vihdoin mahdollisuus kokeilla kirjastoa töissä pyparsing, ja kirjoitettiin skripti, joka luo juuri ne kääreet juuri niille toiminnoille, joihin niitä tarvitaan.

Ja niin, sen jälkeen prosessori näytti toimivan. Se näyttää johtuvan siitä, että näyttöä ei koskaan alustettu, vaikka memtest86+ pystyi toimimaan alkuperäisessä kokoonpanossa. Tässä on tarpeen selventää, että Qemu-lohkon I/O-koodi kirjoitetaan korutiineissa. Emscriptenillä on oma erittäin hankala toteutus, mutta se piti silti tukea Qemu-koodissa, ja voit korjata prosessorin nyt: Qemu tukee vaihtoehtoja -kernel, -initrd, -append, jolla voit käynnistää Linuxin tai esimerkiksi memtest86+:n ilman lohkolaitteita ollenkaan. Mutta tässä on ongelma: alkuperäisessä kokoonpanossa voi nähdä Linux-ytimen ulostulon konsoliin valinnalla -nographic, eikä selaimesta lähetetä päätelaitteeseen, josta se käynnistettiin emrun, ei tullut. Eli ei ole selvää: prosessori ei toimi tai grafiikkatulostus ei toimi. Ja sitten tuli mieleeni odottaa vähän. Kävi ilmi, että "prosessori ei nuku, vaan yksinkertaisesti vilkkuu hitaasti", ja noin viiden minuutin kuluttua ydin heitti joukon viestejä konsoliin ja jatkoi roikkumista. Kävi selväksi, että prosessori yleensä toimii, ja meidän on kaivettava koodia työskennelläksemme SDL2: n kanssa. Valitettavasti en osaa käyttää tätä kirjastoa, joten joissain paikoissa minun piti toimia satunnaisesti. Jossain vaiheessa viiva parallel0 välähti näytöllä sinisellä pohjalla, mikä herätti ajatuksia. Lopulta kävi ilmi, että ongelma oli se, että Qemu avaa useita virtuaalisia ikkunoita yhteen fyysiseen ikkunaan, joiden välillä voi vaihtaa Ctrl-Alt-n:llä: se toimii natiiviversiossa, mutta ei Emscriptenissä. Kun olet päästänyt eroon tarpeettomista ikkunoista vaihtoehtojen avulla -monitor none -parallel none -serial none ja ohjeita koko näytön väkisin uudelleenpiirtämiseen jokaisessa kehyksessä, kaikki yhtäkkiä toimi.

Korutiinit

Eli emulointi selaimessa toimii, mutta siinä ei voi ajaa mitään mielenkiintoista yksittäislevykkeellä, koska lohko-I/O:ta ei ole - sinun on otettava käyttöön tuki korutiineille. Qemulla on jo useita korutiinitaustaohjelmia, mutta JavaScriptin ja Emscripten-koodigeneraattorin luonteen vuoksi et voi vain aloittaa pinojen jongleerausta. Vaikuttaa siltä, ​​​​että "kaikki on poissa, kipsi poistetaan", mutta Emscripten-kehittäjät ovat jo huolehtineet kaikesta. Tämä on toteutettu melko hauskasti: sanotaanko tällaista funktiokutsua epäilyttäväksi emscripten_sleep ja useat muut, jotka käyttävät Asyncify-mekanismia, sekä osoitinkutsuja ja kutsuja mihin tahansa toimintoon, jossa jompikumpi kahdesta edellisestä tapauksesta voi esiintyä alempana pinossa. Ja nyt, ennen jokaista epäilyttävää puhelua, valitsemme asynkronisen kontekstin ja heti puhelun jälkeen tarkistamme, onko asynkronista puhelua tapahtunut, ja jos on, tallennamme kaikki paikalliset muuttujat tähän asynkroniseen kontekstiin, ilmoita mikä toiminto siirtääksesi ohjauksen kohtaan, kun meidän on jatkettava suoritusta, ja poistumaan nykyisestä toiminnosta. Tässä on tilaa tutkia vaikutusta tuhlaamista — Asynkronisesta kutsusta palatun koodin suorittamisen jatkamisen tarpeisiin kääntäjä luo epäilyttävän kutsun jälkeen alkavan funktion "stubt" - näin: jos epäilyttäviä kutsuja on n, niin toiminto laajenee jonnekin n/2 kertaa — tämä on edelleen, jos ei. Muista, että jokaisen mahdollisesti asynkronisen kutsun jälkeen sinun on lisättävä alkuperäiseen funktioon joidenkin paikallisten muuttujien tallentaminen. Myöhemmin jouduin jopa kirjoittamaan Pythonilla yksinkertaisen skriptin, joka tietyn erityisen ylikäytettyjen funktioiden perusteella, jotka oletettavasti "eivät anna asynkronian kulkea itsestään" (eli pinopromootio ja kaikki mitä juuri kuvailin, ei work in them), osoittaa kutsuja osoittimien kautta, joissa kääntäjän tulee jättää funktiot huomioimatta, jotta näitä toimintoja ei pidetä asynkronisina. Ja sitten alle 60 megatavun JS-tiedostot ovat selvästi liikaa - sanotaan ainakin 30. Tosin kun olin asentamassa kokoonpanoskriptiä, ja vahingossa heitin pois linkkivaihtoehdot, joihin kuului mm. -O3. Suoritan luodun koodin, ja Chromium kuluttaa muistia ja kaatuu. Katsoin sitten vahingossa, mitä hän yritti ladata... No, mitä voin sanoa, minäkin olisin jäätynyt, jos minua olisi pyydetty tutkimaan ja optimoimaan 500+ Mt Javascriptiä.

Valitettavasti Asyncify-tukikirjastokoodin tarkistukset eivät olleet täysin ystävällisiä longjmp-s, joita käytetään virtuaalisen prosessorin koodissa, mutta pienen korjauksen jälkeen, joka poistaa nämä tarkistukset käytöstä ja palauttaa väkisin kontekstit ikään kuin kaikki olisi kunnossa, koodi toimi. Ja sitten alkoi outo asia: joskus laukaistiin synkronointikoodin tarkistukset - samat, jotka kaatavat koodin, jos suorituslogiikan mukaan se pitäisi estää - joku yritti napata jo kaapatun mutexin. Onneksi tämä ei osoittautunut loogiseksi ongelmaksi serialisoidussa koodissa - käytin vain Emscriptenin tarjoamaa standardia pääsilmukkatoimintoa, mutta joskus asynkroninen puhelu purki pinon kokonaan, ja sillä hetkellä se epäonnistui. setTimeout pääsilmukasta - näin ollen koodi tuli pääsilmukan iteraatioon poistumatta edellisestä iteraatiosta. Kirjoitti uudelleen äärettömälle silmukalle ja emscripten_sleep, ja mutexien ongelmat loppuivat. Koodi on jopa tullut loogisemmaksi - loppujen lopuksi minulla ei itse asiassa ole koodia, joka valmistelee seuraavan animaatiokehyksen - prosessori vain laskee jotain ja näyttöä päivitetään säännöllisesti. Ongelmat eivät kuitenkaan loppuneet tähän: joskus Qemu-suoritus vain päättyi hiljaa ilman poikkeuksia tai virheitä. Sillä hetkellä luovuin siitä, mutta eteenpäin katsoessani sanon, että ongelma oli tämä: korutiinikoodi ei itse asiassa käytä setTimeout (tai ei ainakaan niin usein kuin luulisi): toiminto emscripten_yield asettaa yksinkertaisesti asynkronisen kutsulipun. Koko pointti on se emscripten_coroutine_next ei ole asynkroninen toiminto: sisäisesti se tarkistaa lipun, nollaa sen ja siirtää ohjauksen sinne, missä sitä tarvitaan. Eli pinon mainostaminen päättyy siihen. Ongelmana oli se, että use-af-free, joka ilmeni, kun korutiinipooli poistettiin käytöstä, koska en kopioinut tärkeää koodiriviä olemassa olevasta korutiinitaustasta, toiminto qemu_in_coroutine palautettiin tosi, vaikka todellisuudessa sen olisi pitänyt palauttaa epätosi. Tämä johti puheluun emscripten_yield, jonka yläpuolella ei ollut ketään pinossa emscripten_coroutine_next, pino avautui aivan huipulle, mutta ei setTimeout, kuten jo sanoin, ei ollut esillä.

JavaScript-koodin luominen

Ja tässä on itse asiassa luvattu "jauhelihan kääntäminen takaisin". Ei oikeastaan. Tietenkin, jos käytämme Qemua selaimessa ja Node.js:ää siinä, niin luonnollisesti koodin luomisen jälkeen Qemussa saamme täysin väärän JavaScriptin. Mutta silti, jonkinlainen käänteinen muutos.

Ensinnäkin vähän siitä, miten Qemu toimii. Anteeksi heti: en ole ammattimainen Qemu-kehittäjä ja johtopäätökseni voivat olla joissain paikoissa virheellisiä. Kuten he sanovat, "oppilaan mielipiteen ei tarvitse olla sama kuin opettajan mielipiteen, Peanon aksiomaatiikan ja terveen järjen kanssa." Qemulla on tietty määrä tuettuja vierasarkkitehtuureja ja jokaiselle on oma hakemistonsa target-i386. Rakentaessasi voit määrittää tuen useille vierasarkkitehtuureille, mutta tuloksena on vain useita binaaritiedostoja. Vierasarkkitehtuuria tukeva koodi puolestaan ​​generoi joitain sisäisiä Qemu-operaatioita, jotka TCG (Tiny Code Generator) muuntaa jo konekoodiksi isäntäarkkitehtuurille. Kuten tcg-hakemistossa sijaitsevassa readme-tiedostossa todettiin, tämä oli alun perin osa tavallista C-kääntäjää, joka myöhemmin mukautettiin JIT:tä varten. Siksi esimerkiksi tämän asiakirjan kohdearkkitehtuuri ei ole enää vierasarkkitehtuuri, vaan isäntäarkkitehtuuri. Jossain vaiheessa ilmestyi toinen komponentti - Tiny Code Interpreter (TCI), jonka pitäisi suorittaa koodia (melkein samat sisäiset toiminnot) ilman koodigeneraattoria tietylle isäntäarkkitehtuurille. Itse asiassa, kuten sen dokumentaatiossa todetaan, tämä tulkki ei välttämättä aina toimi yhtä hyvin kuin JIT-koodigeneraattori, ei vain määrällisesti nopeuden suhteen, vaan myös laadullisesti. Vaikka en ole varma, onko hänen kuvauksensa täysin relevantti.

Aluksi yritin tehdä täysimittaista TCG-taustaohjelmaa, mutta hämmentyin nopeasti lähdekoodissa ja tavukoodiohjeiden epäselvässä kuvauksessa, joten päätin kääriä TCI-tulkin. Tämä antoi useita etuja:

  • koodigeneraattoria toteutettaessa voisi katsoa ei ohjeiden kuvausta, vaan tulkin koodia
  • et voi luoda funktioita jokaiselle käännöslohkolle, vaan esimerkiksi vasta sadannen suorituksen jälkeen
  • jos luotu koodi muuttuu (ja tämä näyttää olevan mahdollista, päätellen funktioista, joiden nimet sisältävät sanan patch), minun on mitätöitävä luotu JS-koodi, mutta ainakin minulla on jotain mistä se luoda uudelleen.

Mitä tulee kolmanteen kohtaan, en ole varma, onko korjaus mahdollista koodin ensimmäisen suorittamisen jälkeen, mutta kaksi ensimmäistä kohtaa riittävät.

Aluksi koodi luotiin suuren kytkimen muodossa alkuperäisen tavukoodikäskyn osoitteeseen, mutta sitten, kun muistin artikkelin Emscriptenistä, generoidun JS:n optimoinnista ja uudelleensilmukoinnista, päätin luoda lisää ihmiskoodia, varsinkin kun empiirisesti se osoittautui, että ainoa sisääntulokohta käännöslohkoon on sen aloitus. Hetken kuluttua meillä oli koodigeneraattori, joka loi koodin ifs:illä (tosin ilman silmukoita). Mutta huono tuuri, se kaatui ja antoi viestin, että ohjeet olivat väärän pituisia. Lisäksi viimeinen ohje tällä rekursiotasolla oli brcond. Okei, lisään identtisen tarkistuksen tämän käskyn luomiseen ennen ja jälkeen rekursiivisen kutsun ja... yhtäkään niistä ei suoritettu, mutta assert-kytkimen jälkeen ne silti epäonnistuivat. Lopulta, tutkittuani luotua koodia, tajusin, että vaihdon jälkeen osoitin nykyiseen käskyyn ladataan uudelleen pinosta ja todennäköisesti ylikirjoitetaan generoidulla JavaScript-koodilla. Ja niin kävi. Puskurin kasvattaminen yhdestä megatavusta kymmeneen ei johtanut mihinkään, ja kävi selväksi, että koodigeneraattori pyöri ympyröissä. Meidän piti tarkistaa, ettemme ylittäneet nykyisen TB:n rajoja, ja jos menimme, anna seuraavan TB:n osoite miinusmerkillä, jotta voimme jatkaa suorittamista. Lisäksi tämä ratkaisee ongelman "mitkä luodut funktiot pitäisi mitätöidä, jos tämä tavukoodin pala on muuttunut?" — vain tätä käännöslohkoa vastaava funktio on mitätöitävä. Muuten, vaikka tein virheenkorjauksen kaikesta Chromiumissa (koska käytän Firefoxia ja minun on helpompi käyttää erillistä selainta kokeiluihin), Firefox auttoi minua korjaamaan yhteensopimattomuudet asm.js-standardin kanssa, minkä jälkeen koodi alkoi toimia nopeammin Kromi.

Esimerkki luodusta koodista

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

Johtopäätös

Työ ei siis ole vieläkään valmis, mutta olen kyllästynyt viemään tätä pitkäkestoista rakentamista salaa täydellisyyteen. Siksi päätin julkaista sen, mitä minulla on tällä hetkellä. Koodi on paikoin hieman pelottava, koska tämä on kokeilu, eikä ole etukäteen selvää, mitä on tehtävä. Todennäköisesti silloin kannattaa tehdä normaalit atomisitoumukset jonkin Qemun nykyaikaisemman version päälle. Sillä välin Gitassa on ketju blogimuodossa: jokaiselle ainakin jollain tavalla läpäistylle "tasolle" on lisätty yksityiskohtainen venäjänkielinen kommentti. Itse asiassa tämä artikkeli on suurelta osin johtopäätösten uudelleenkerto git log.

Voit kokeilla kaikkea täällä (varo liikennettä).

Mikä jo toimii:

  • x86 virtuaalinen prosessori käynnissä
  • JIT-koodigeneraattorista on toimiva prototyyppi konekoodista JavaScriptiin
  • Muiden 32-bittisten vierasarkkitehtuurien kokoamiseen on malli: juuri nyt voit ihailla Linuxia MIPS-arkkitehtuurin jumiutuessa selaimessa latausvaiheessa.

Mitä muuta voit tehdä

  • Nopeuta emulointia. Jopa JIT-tilassa se näyttää toimivan hitaammin kuin Virtual x86 (mutta siellä on mahdollisesti koko Qemu, jossa on paljon emuloituja laitteita ja arkkitehtuureja)
  • Normaalin käyttöliittymän tekeminen - suoraan sanottuna en ole hyvä web-kehittäjä, joten olen toistaiseksi muokannut Emscriptenin vakiokuoren parhaani mukaan
  • Yritä käynnistää monimutkaisempia Qemu-toimintoja - verkko, VM-migraatio jne.
  • UPD: sinun on lähetettävä muutama kehitys- ja vikaraporttisi Emscriptenille ylävirtaan, kuten aiemmat Qemun ja muiden projektien kantajat tekivät. Kiitos heille siitä, että he voivat implisiittisesti käyttää panoksensa Emscriptenille osana tehtävääni.

Lähde: will.com

Lisää kommentti