Néhány évvel ezelőtt Fabrice Bellard — egy JavaScript-ben írt PC emulátort. Ezt követően legalább még . De, amennyire tudom, mindezek értelmezők voltak, míg a jóval korábban, szintén ugyanaz által, Fabrice Bellard által írt Qemu, és valószínűleg bármely önmagát tisztelő modern emulátor, JIT-kompilációt használ a vendégkód hívásának a gazdarendszer kódjává való átkonvertálásához. Úgy gondoltam, hogy itt az ideje, hogy megvalósítsuk a visszafelé irányuló feladatot a böngészők által megoldott feladattal szemben: a gépi kód JIT-kompilációját JavaScript-be, amihez logikusnak tűnt a Qemu portolása. Hogy miért éppen a Qemu, amikor léteznek egyszerűbb és felhasználóbarát emulátorok — mint például a VirtualBox —, ami csak telepít és már működik. De a Qemu-nak van néhány érdekes tulajdonsága
- nyílt forráskód
- lehetőség arra, hogy kernel driver nélkül működjön
- lehetőség egy értelmező módban való működésre
- támogatás számos gazda és vendég architektúrához
A harmadik pontról most már meg tudom magyarázni, hogy valójában a TCI módban nem magukat a vendég gépi utasításokat értelmezik, hanem az azokból származó bytekódot, de ez nem változtat a lényegen — ahhoz, hogy a Qemu-t új architektúrán össze tudjuk állítani és futtatni, ha szerencsénk van, elegendő a C fordító — a kódgenerátor megírását el lehet halasztani.
Íme, két évnyi lassú, szabadidőmben végzett munka után a Qemu forrásain, megjelent egy működő prototípus, amelyben már lehet futtatni például a Kolibri OS-t.
Mi az Emscripten
Napjainkban sok fordító létezik, amelyek végső eredményeként JavaScript-et hoznak létre. Egyesek, mint például a TypeScript, eredetileg a legjobb módot szánták a webes programozásra. Ugyanakkor az Emscripten — egy módszer a meglévő C vagy C++ kódok JavaScript formába történő fordítására, amelyet a böngésző érthet. Számos neves program portját készítették el: , például a PyPy-ra is érdemes ránézni — amúgy azt állítják, hogy már van JIT-jük. Valójában nem minden programot lehet egyszerűen lefordítani és futtatni a böngészőben — vannak különböző , amelyekkel szembe kell nézni, de ahogy a bejegyzés is mondja az oldalon: "Az Emscripten szinte bármilyen hordozható C/C++ kód JavaScript-ra. Vagyis létezik egy sor olyan művelet, amely a szabvány szerint meghatározatlan viselkedés, de általában x86-on működik — például a nem igazított változókhoz való hozzáférés, ami bizonyos architektúrákon teljesen tilos. Általánosságban a Qemu egy cross-platform program, és remélhetőleg nem tartalmaz nagy mennyiségű meghatározatlan viselkedést — csak le kell fordítani, aztán kicsit bajlódni a JIT-tel — és kész! De nem így volt ez...
Első kísérlet
Általában nem én vagyok az első, akinek eszébe jutott a Qemu JavaScript-ra való portolásának ötlete. A ReactOS fórumán merült fel a kérdés, hogy vajon lehetséges-e ez az Emscripten segítségével. Korábban pletykák keringtek arról, hogy ezt személyesen Fabrice Bellard csinálta, de az jslinux-ról volt szó, ami, amennyire tudom, éppen próbálkozás volt arra, hogy kézzel elérjék a megfelelő teljesítményt JS-en, és nulláról írták. Később megjelent a Virtual x86 — ehhez nyílt forráskódot tettek közzé, és állítólag a „realizmus” fokozott mértéke lehetővé tette a SeaBIOS firmware-ként való használatát. Ezen kívül legalább egy próbálkozás volt a Qemu Emscripten segítségével történő portolására — ezt próbálták meg. , de a fejlesztés, amennyire megértettem, leállt.
Tehát úgy tűnne, hogy itt vannak a forráskódok, itt az Emscripten — csak le kell fordítani. De vannak még könyvtárak is, amelyekre a Qemu támaszkodik, és a könyvtárak is támaszkodnak más könyvtárakra stb., és az egyik ezek közül — , amely a glib-tól függ. Az interneten hallottam, hogy a nagy Emscripten könyvtárgyűjteményben van egy, de ez valahogy hitetlennek tűnt: egyrészt, az új fordítóval nem fordult, másrészt, ez túl alacsony szintű könyvtár ahhoz, hogy csak úgy átforduljon JS-re. Az ügy nem csak az assembly betétekről szól — valószínűleg, ha ki van fordítva, akkor néhány hívási konvencióhoz lehet, hogy ezeket el lehet kerülni, és a szükséges argumentumokat a veremben el lehet helyezni és meghívni a funkciót. De az Emscripten — egy okos dolog: ahhoz, hogy a generált kód ismerős legyen a böngésző JS-motorjának optimalizálói számára, különböző trükköket használnak. Különösen a úgynevezett relooping — a kódgenerátor a kapott LLVM IR-re néhány absztrakt ugrási utasítással próbálja meg újraalkotni a plauzibilis if-eket, ciklusokat stb. Nos, és az argumentumok a funkciókhoz hogyan kerülnek átadásra? Természetesen, mint a JS-funkciók argumentumai, azaz lehetőség szerint nem a veremben.
Az elején az volt a gondolatom, hogy egyszerűen írok egy JS-es helyettesítést a libffi-hez, és lefuttatom a megszokott teszteket, de végül összezavarodtam abban, hogyan készítsem el a fejléceimet, hogy működjenek a meglévő kóddal — hát, mit mondjak, ahogy mondani szokták: "Vagy annyira nehezek a feladatok, vagy mi vagyunk annyira hülyék". Kényszerültem a libffi-t egy másik architektúrára portolni, ha szabad így mondani — szerencsére, az Emscripten-ben vannak inline assembly makrók (JavaScript-en, igen — nos, milyen architektúra, olyan assembly), valamint lehetőség, hogy az azonnal generált kódot futtassuk. Szóval, egy kis időt töltöttem a libffi platformfüggő darabjaival, és kaptam egy lefordítható kódot, amit lefuttattam az első adódó teszten. Meglepetésemre a teszt sikeres volt. A saját zsenialitásom miatt ledöbbentem — nem vicc, elsőre működött — még mindig nem hittem a szememnek, újra megnéztem a kapott kódot, értékeltem, hova kellene mélyebben nézni. Itt másodszor is megdöbbentem — az egyetlen dolgot, amit a funkcióm tett ffi_call — az a sikeres meghívásról számolt be. Maga a hívás nem volt. Így küldtem el az első pull request-emet, ami egy minden olimpiai versenyző számára érthető hibát javított a tesztben — a valós számokat nem szabad összehasonlítani mint a == b és még mint a - b < EPS — ne felejtsük el a modult, mert akkor a 0 bizony 1/3-nak fog számítani... Szóval, lett egy libffi portom, ami a legegyszerűbb teszteken átmegy, és amivel a glib fordítható — úgy döntöttem, hogy később befejezem. Elöljáróban annyit mondok, hogy a libffi végső kódjába a fordító egyáltalán nem építette be.
De ahogy már említettem, vannak bizonyos korlátozások, és a különféle meghatározatlan viselkedések között egy különösen nem kívánt jellemző bújt meg — a JavaScript alapértelmezés szerint nem támogatja a megosztott memóriás több szálat. Alapvetően ezt nem is lehetne rossz ötletnek nevezni, de nem a C szálakra építő kód portolásánál. Egyébként a Firefoxban kísérleteznek a megosztott munkások támogatásával, és a pthread megvalósítása Emscriptenben is jelen van, de nem akartam, hogy ez legyen az alap. Lassan ki kellett tűrnöm a több szálat a Qemu kódból — azaz keresni, hol indulnak a szálak, kiemelni a szálban futó ciklus törzsét egy külön funkcióba, és felváltva hívni ezeket a funkciókat a fő ciklusból.
Második kísérlet
Valamikor világossá vált, hogy a helyzet nem változik, és a rendszertelen kódkalapálás nem fog jóra vezetni. Következtetés: valahogy rendszereznem kell a kódbeillesztés folyamatát. Ezért a friss 2.4.1-es verziót vettem (nem a 2.5.0-t, mert ki tudja, lehet, hogy ott további, javítatlan hibák találhatók, és nekem elég a saját hibámból), és első lépésként biztonságosan átírtam thread-posix.c. Szóval, hogyan is biztonságosan: ha valaki megpróbált egy blokkoló műveletet végrehajtani, azonnal meghívódott a abort() — természetesen ez nem oldott meg minden problémát, de legalább ez jobban hangzik, mint csendben inkonzisztenciákat kapni az adatokban.
Egyébként a kód JS-re portolásához az Emscripten opciók nagyon segítenek -s ASSERTIONS=1 -s SAFE_HEAP=1 — ezek bizonyos típusú meghatározatlan viselkedéseket, például nem megfelelően a címhez igazított címekhez való hozzáféréseket ejtenek ki (ami egyáltalán nem vélekedik a typed arrays kódjával, mint például HEAP32[addr >> 2] = 1) vagy a függvény hibás számú argumentummal való meghívását.
Egyébként a nem megfelelő igazítás egy külön téma. Ahogyan már említettem, a Qemu-ban van egy "degenerált" TCI (tiny code interpreter) hídkód-generáló backend, és hogy egy új architektúrára össze tudjam állítani és elindítani a Qemu-t, ha szerencsém van, elég a C fordító. Kulcsszavak "ha szerencsém van". Nekem azonban nem volt szerencsém, és kiderült, hogy a TCI a bájt-kódjának parse-ja során nem igazított hozzáférést használ. Tehát a mindenféle ARM és hasonló architektúrákra, ahol kötelező az igazított hozzáférés, a Qemu összeáll, mert ezekhez van megfelelő TCG backend, amely natív kódot generál, de hogy a TCI működni fog-e rajtuk, az még kérdéses. Mindazonáltal kiderült, hogy a TCI dokumentációjában valami hasonló világosan szerepelt. Végül a kódhoz hozzáadtunk hívásokat a nem igazított olvasásra, amelyeket a Qemu egy másik részében fedeztek fel.
Heap korruptálás
Végül a TCI-ben a nem igazított hozzáférést kijavították, elkészült a fő ciklus, amely sorban hívta a processzort, az RCU-t és néhány apróságot. Most elindítom a Qemu-t a -d exec,in_asm,out_asm, opcióval, ami azt jelenti, hogy jelezni kell, mely kódkockák futnak, valamint a fordítás pillanatában írni, hogy milyen vendégkód volt, és milyen házigazda kód lett (ebben az esetben bájt kód). Elindul, végrehajt néhány fordítási blokkot, kiírja az általam hátrahagyott hibakeresési üzenetet, hogy most fog elindulni az RCU, és... összeomlik a abort() belül a funkcióban free(). A funkció "böngészésével" sikerült kideríteni, hogy a heap blokk fejlécében, amely a lefoglalt memória előtti nyolc bájtban található, a blokk mérete vagy valami hasonló helyett szemét volt. free() Kiderült, hogy a memória területét megelőző nyolc byte-ban a halom blokk fejlécében a blokk mérete vagy valami hasonló helyett szemét található.
A verés rombolása — milyen kedves... Ilyen esetben van egy hasznos eszköz — az (amennyire lehet) ugyanabból a forrásból összeállítani egy natív bináris fájlt, és futtatni Valgrind alatt. Néhány idő múlva elkészült a bináris. Ugyanezekkel az opciókkal indítom — ismét összeomlik az inicializálásnál, el sem jutva a tényleges végrehajtásig. Persze kellemetlen — úgy látszik, a források nem voltak teljesen azok, ami nem meglepő, hiszen a configure több más opciót talált, de nekem ott van a Valgrind — először ezt a hibát javítom, aztán, ha szerencsém van, a kezdeti hibát is. Ugyanezt futtatom Valgrind alatt... A-ha, he-he-he, hát, elindult, normálisan végigment az inicializáción, és továbbment a kezdeti hiba mellett anélkül, hogy bármilyen figyelmeztetést adott volna a memóriahelytelen hozzáférésről, nem is beszélve az összeomlásokról. Ilyenre az élet nem készített fel — a lezuhanó program nem omlik össze Valgrind alatt. Mi történhetett — rejtély. Az én hipotézisem az, hogy mivel a jelenlegi utasítás környezetében az inicializálás utáni zuhanáskor a gdb munkahelyet mutatott. memset-a érvényes mutatóval, használva akár mmx, akár xmm regisztereket, talán ez valamilyen igazítási hiba volt, habár úgysem hiszem igazán.
Rendben, úgy tűnik, hogy a Valgrind itt nem segít. És itt kezdődött a legrosszabb — minden úgy nézett ki, hogy működik, de abszolút ismeretlen okokból összeomlik egy esemény miatt, ami akár millió utasítással ezelőtt is megtörténhetett. Hosszú ideig még azt sem tudtam, hogyan közelítsem meg a problémát. Végül le kellett ülnöm és debuggolnom. A fejléc átírásának kinyomtatása azt mutatta, hogy ez nem egy szám, sokkal inkább valamilyen bináris adat. És, csoda, ez a bináris sor megtalálható a BIOS fájlban — ami azt jelenti, hogy most elég magabiztosan mondható, hogy buffer túlcsordulás történt, és az is világos, hogy mi került ebbe a bufferbe. Aztán onnan egyfajta módon — az Emscriptenben, szerencsére, nincs címek randomizálása, és abban semmi rés nem található, ezért lehetőség van arra, hogy valahol a kód közepén írási műveletet végezzünk egy múltbeli futás során használt pointerrel, megnézzük az adatokat, megnézzük a pointert, és ha az nem változott, akkor információt nyerünk a gondolkodáshoz. Igaz, hogy bármely módosítás után a linkelés pár percet vesz igénybe, de ezt el kell fogadni. Végül rátaláltam a konkrét sorra, amely a BIOS-t a temporális bufferből a vendég memóriába másolja — és valóban, a bufferben nem volt elegendő hely. A fura buffer címének forrása egy funkcióhoz vezetett qemu_anon_ram_alloc a fájlban oslib-posix.c — a logika így alakult: néha hasznos lehet az 2 MB-os huge page-hez igazítani a címet, ezért kérjünk tőle mmap először egy kicsit többet, majd a felesleget visszaadjuk a munmap. Ha viszont nem szükséges a kiegyenlítés, akkor a 2 MB helyett a getpagesize() — mmap elérhetően kiadja a kiegyenlített címet... Szóval az Emscriptenben mmap egyszerűen megkeresi a malloc, ami természetesen nem kiegyenlített oldalanként. Összességében, a hiba, ami pár hónapig bosszantott, két sorban javításával megoldódott.
Funkcióhívási sajátosságok
És már a processzor is számít valamit, a Qemu nem omlik össze, de a képernyő nem kapcsol be, és a processzor gyorsan ciklikusan működik, a kimenet alapján. -d exec,in_asm,out_asm. Megjelent egy hipotézis: nem érkeznek a timer megszakításai (vagy általában az összes megszakítás). Tényleg, ha a natív build-ből, ami valamilyen okból működött, eltávolítjuk a megszakításokat, hasonló kép áll elő. De a megfejtés nem is ebben volt: az említett opcióval kiadott trace-ek összehasonlítása azt mutatta, hogy a végrehajtási pályák nagyon korán eltérnek egymástól. Itt meg kell említeni, hogy az indítóprogrammal rögzített és a natív build kimenetével való összehasonlítás nem éppen mechanikus folyamat. emrun Nem tudom pontosan, hogyan kapcsolódik a böngészőben futó program a emrun, de néhány sor a kimenetben felcserélődik, így a különbség a diff-ben — ez még nem ok arra, hogy azt gondoljuk, hogy a pályák eltértek. Összességében világossá vált, hogy az utasítás szerint ljmpl különböző címekre történik a váltás, és a bytekód is alapvetően eltérő: az egyik esetében van egy hívás a C függvény segédprogramjára, a másik esetében pedig nincs. Az utasítások után való guglizás és a kód tanulmányozása után világos lett, hogy egyrészt közvetlenül előtte a regiszterbe cr0 történt írás — szintén segédprogram révén —, ami a processzort védett üzemmódba váltotta, másrészt hogy a js verzió nem váltott védett üzemmódba. Az a helyzet, hogy az Emscripten egy másik sajátossága, hogy nem tolerál kódot, mint a call TCI implementációjában, amely bármilyen függvényhívás mutatót a long long f(int arg0, .. int arg9) típusra alakít — a függvényeket a megfelelő argumentumok számával kell hívni. E szabály megszegése esetén a debug beállításoktól függően a program vagy összeomlik (ami jó), vagy nem azt a függvényt hívja fel (amit nehéz lesz debugolni). Van még egy harmadik lehetőség — a wrapper generálása, amely argumentumokat ad hozzá vagy távolít el, de ezek a wrapper-ek összességében nagyon sok helyet foglalnak el, míg nekem valójában csupán kicsivel több mint száz wrapper-re van szükségem. Csak ez önmagában is nagyon szomorú, de egy még komolyabb probléma adódott: a generált wrapper függvények kódjában az argumentumok konvertálódtak, csak éppen a generált argumentumokkal rendelkező függvényt néha nem hívták meg — hát mint a libffi megvalósításomban. Tehát néhány segédprogram egyszerűen nem futott le.
Szerencsére a Qemu-ban gép által olvasható segédprogramok listái találhatók olyan fejlécfájl formájában, mint
DEF_HELPER_0(lock, void)
DEF_HELPER_0(unlock, void)
DEF_HELPER_3(write_eflags, void, env, tl, i32)Ezek meglehetősen szórakoztató módon használatosak: először a legfurcsább módon felülírják a makrókat DEF_HELPER_n, majd beillesztésre kerül helper.h. Akár a makrók szerkezeti inicializálókba és vesszőkbe torkollanak, majd tömböt határoznak meg, ahelyett, hogy elemeket kapnánk — #include <helper.h> Végül végre lehetőségem volt kipróbálni a , és írtam egy scriptet, amely pontosan azokat és csak azokat a funkciókat generálja, amelyek szükségesek.
Miután ez megtörtént, a processzor úgy tűnik, hogy működik. Úgy tűnik, mert a képernyő nem inicializálódott, habár natív build alatt sikerült elindítani a memtest86+-ot. Itt érdemes megjegyezni, hogy a Qemu blokk-IO kódja korutinokban íródott. Az Emscripten-nek van egy elég csavaros megvalósítása, de ezt még támogatni kellett a Qemu kódjában, míg a processzort jelenleg már lehet debuggolni: a Qemu támogatja a -kernel, -initrd, -append, amely segítségével Linuxot vagy például memtest86+-t lehet betölteni, anélkül hogy blokk eszközöket használnánk. De itt a nehézség: a natív build alatt láthattuk a Linux kernel kilépését a konzolon a -nographic, opcióval, de a böngészőből nem jött ki semmilyen kimenet a terminálba, ahonnan elindították, emrun. Tehát nem világos, hogy a processzor nem működik, vagy a grafikus kimenet. Aztán eszembe jutott, hogy egy kicsit várni kellene. Kiderült, hogy "a processzor nem alszik, csak lassan villog", és öt perc múlva a mag kidobott egy rakás üzenetet a konzolra, majd ismét függni kezdett. Egyértelművé vált, hogy a processzor alapvetően működik, és a SDL2 kóddal kell mélyebben foglalkozni. Sajnos ezt a könyvtárat nem tudom használni, ezért helyenként a sötétben kellett tapogatóznom. Egy pillanatban egy párhuzamos0 felirat villant fel kék háttér előtt, ami elgondolkodtatott. Végül kiderült, hogy a Qemu több virtuális ablakot nyit egy fizikai ablakon belül, amelyek között a Ctrl-Alt-n billentyűkombinációval lehet váltani: natív build alatt működik, Emscripten alatt - nem. A felesleges ablakok eltávolítása után a következő opciók segítségével -monitor none -parallel none -serial none és a képernyő minden keretnél történő kényszerített újrarajzolásának megadásával hirtelen minden működni kezdett.
Korutinok
Tehát az emuláció a böngészőben működik, de semmi érdekes egyetlen lemezes dolgot nem lehet rajta futtatni, mert hiányzik a blokk bemenet-kimenet — meg kell valósítani a coroutine támogatást. A Qemu-ban már van néhány coroutine backend, de a JavaScript és az Emscripten kódgeneráló sajátosságai miatt nem lehet csak úgy elkezdeni a stack-ek zsonglőrködését. Úgy tűnhet, hogy "minden elveszett, a gipsz lejött", de az Emscripten fejlesztői már mindent elintéztek. Ez elég szórakoztató módon van megvalósítva: tegyük fel, hogy nevezzük gyanúsnak az olyan függvényhívásokat, mint emscripten_sleep és néhány másik, amelyek az Asyncify mechanizmust használják, valamint a mutatóval történő hívásokat és bármely függvény hívásait, ahol a stack alatt bármelyik korábban említett két eset bekövetkezhet. Most pedig mielőtt minden gyanús hívás előtt létrehozunk egy async kontextust, és miután a hívás megtörtént — ellenőrizzük, hogy történt-e aszinkron hívás, és ha igen, akkor elmentjük az összes lokális változót ebbe az async kontextusba, megadjuk, hogy melyik függvényhez kell irányítanunk a vezérlést, amikor folytatni kell a végrehajtást, és kilépünk a jelenlegi függvényből. Itt már rengeteg lehetőség van a vizsgálatára — a kód végrehajtásának folytatásához szükséges, hogy a fordító "darabokra törje" a függvényt, amely a gyanús hívás után kezdődik — így: ha n gyanús hívás van, a függvény n/2-szer töredezett lesz — ez még akkor is, ha figyelembe vesszük, hogy az alap függvényhez minden potenciálisan aszinkron hívás után hozzá kell adni a lokális változók egy részének megőrzését. Idővel még írtam egy egyszerű Python scriptet, amely a megadott különösen töredezett függvények halmazán keresztül, amelyek feltételezhetően "nem engedik át az aszinkronitást" (azaz ezekben nem aktiválódik a stack görgetése és mindaz, amit most leírtam), megadja, hogy mely függvényekben kell figyelmen kívül hagyni a mutatóval történő hívásokat a fordító számára, hogy ezek a függvények ne számítanak aszinkronnak. Mert a 60 MB-os JS-fájlok már nyilvánvalóan túlzás — legyen inkább 30. Bár egyszer, amikor a build scriptet állítottam be, véletlenül kidobtam a linker opciókat, amelyek között volt -O3Futtatom a generált kódot, és a Chromium memóriát eszik és összeomlik. Utána véletlenül ránéztem, hogy mit próbál betölteni… Nos, mit mondhatnék, én is lefagynék, ha arra kérnének, hogy figyelmesen tanulmányozzam és optimalizáljam a JavaScriptet 500+ MB-on.
Sajnos az Asyncify támogatási könyvtár kódjában végzett ellenőrzések nem igazán voltak kompatibilisek a longjmp-val, melyeket a virtuális processzor kódjában használnak, de egy kis patch után, amely letiltotta ezeket az ellenőrzéseket és kényszerítette a kontextusok visszaállítását, mintha minden rendben lenne, a kód működni kezdett. És itt kezdődtek a furcsaságok: néha a szinkronizáló kód ellenőrzései léptek működésbe – azok, amelyek váratlanul leállítják a kódot, ha a végrehajtási logika szerint be kellene záródnia – valaki próbálta megragadni a már megszerzett mutexet. Szerencsére ez nem logikai probléma volt a sorozatban lévő kódokban – egyszerűen csak a fő ciklust biztosító Emscripten beépített funkcióját használtam, de néha az aszinkron hívás teljesen kibővítette a veremet, és ebben a pillanatban lépett működésbe a setTimeout a fő ciklustól – így a kód belépett a fő ciklus iterációjába anélkül, hogy kilépett volna az előző iterációból. Átírtam végtelen ciklusra, és emscripten_sleep, és a mutex problémák megszűntek. A kód még logikusabbá vált – hiszen valójában nincs egy kód, amely előkészíti a következő animációs keretet – egyszerűen csak a processzor számít valamit, és a képernyő időnként frissül. Azonban ezzel nem álltak meg a problémák: néha a Qemu végrehajtása csendben befejeződött bármilyen kivétel és hiba nélkül. Ekkor nem foglalkoztam vele, de visszatekintve elmondhatom, hogy a problémát az okozta: a coroutine kód valójában nem használja setTimeout (vagy legalábbis nem olyan gyakran, mint gondolhatnád): a emscripten_yield csak beállít egy aszinkron hívás zászlót. A lényeg az, hogy az emscripten_coroutine_next nem egy aszinkron függvény: ellenőrzi a zászlót, nullázza azt, és továbbítja az irányítást, ahová szükséges. Vagyis itt végződik a verem kibővítése. A probléma az volt, hogy a use-after-free miatt, ami akkor jelentkezett, amikor a coroutine pool ki volt kapcsolva, mivel nem másoltam be egy fontos kódsort a meglévő coroutine backendből, a qemu_in_coroutine igazat adott, amikor valójában hamisat kellett volna visszaadnia. Ez pedig a híváshoz vezetett. emscripten_yield, amely fölött a verem nem volt emscripten_coroutine_next, a verem egészen a csúcsáig kiterjedt, de semmi setTimeout, ahogy már említettem, nem volt kiadva.
JavaScript kódgenerálás
Itt végre a megígért "húsgyúrás visszafelé". Valójában nem. Persze, ha a böngészőben Qemu-t futtatunk, és abban — Node.js-t, akkor természetesen, a Qemu-ban végzett kódgenerálás után teljesen nem azt a JavaScript-et kapjuk. De mindenesetre, valami fajta visszafelé konvertálás mégis létezik.
Először is egy kis információ arról, hogyan működik a Qemu. Azonnal szeretném megkérni a türelmét: nem vagyok professzionális Qemu fejlesztő, és a megállapításaim helyenként tévesek lehetnek. Ahogy mondani szokták, "a diák véleménye nem köteles egybeesni a tanár véleményével, a Peano axiómákkal és a józan ésszel". A Qemu-nak van egy bizonyos számú támogatott vendégarchitektúrája, és mindegyikhez van egy könyvtár, mint például target-i386. Fordításkor meg lehet adni több vendégarchitektúra támogatását, de az eredmény több bináris lesz. A vendégarchitektúra támogatásához szükséges kódot viszont a Qemu belső műveletei generálják, amelyeket a TCG (Tiny Code Generator) már a gazdaarchitektúra gépi kódjává alakít. Ahogy a tcg könyvtárban található readme fájl állítja, ez eredetileg egy normál C fordító része volt, amit később JIT-re alakítottak. Tehát például a célarchitektúra e dokumentum szempontjából már nem vendég-, hanem gazdaarchitektúra. Egy bizonyos ponton megjelent egy újabb komponens — Tiny Code Interpreter (TCI), amely a kódot (majdnem ugyanazokat a belső műveleteket) végzi a konkrét gazdaarchitektúra kódgenerálója nélkül. Valójában, ahogy a dokumentációja is mondja, ez az értelmező nem mindig működik olyan jól, mint a JIT-kódgeneráló, nemcsak mennyiségében a sebesség szempontjából, hanem minőségében is. Bár nem vagyok biztos abban, hogy a leírása teljesen aktuális.
Először úgy próbáltam létrehozni egy teljes TCG hátsó végű komponenst, de hamar zavarba jöttem a forráskódokban és a nem teljesen érthető bájtparancsok leírásában, ezért úgy döntöttem, hogy a TCI értelmezőt burkolom. Ez azonnal több előnyt is hozott:
- a kódgenerátor megvalósítása során nem kellett a parancsok leírásába nézni, hanem az értelmező kódjára.
- A funkciókat nem szükséges minden egyes találkozott fordítási blokk után generálni, hanem például csak a századik végrehajtás után.
- Ha a generált kód megváltozik (ami úgy tűnik, lehetséges, a patch szóval rendelkező funkciók alapján), akkor le kell tiltanom a generált JS kódot, de legalább lesz mivel újragenerálni.
A harmadik pontról nem vagyok biztos, hogy a patchelés lehetséges, miután a kód először végrehajtásra került, de az első két pont elegendő.
Kezdetben a kód nagy switch formájában generálódott a belső bytekód utasítás címénél, de emlékezve az Emscripten cikkre, a generált JS optimalizálására és reloopingra, úgy döntöttem, hogy emberibb kódot generálok, különösen, mivel tapasztalat szerint az egyetlen belépési pont a fordítási blokkra annak kezdete. Megtörtént, és egy idő után megérkezett a kódgenerátor, amely if-es kódot generál (bár ciklusok nélkül). De itt a probléma, leállt, és azt jelezte, hogy az utasítás valamilyen hibás hosszúságú volt. Ezen a rekurziós szinten az utolsó utasítás a következő volt: brcond. Jól van, egy azonos ellenőrzést fogok hozzáadni ezen utasítás generálásához a rekurzív hívás előtt és után, és... egyik sem futott le, de az assert után a switch-el mégis leestek. Végül, miután tanulmányoztam a generált kódot, rájöttem, hogy a switch után a mutató a jelenlegi utasításra a veremből újratöltődik, és valószínűleg felülíródik a generált JavaScript kóddal. Így is lett. A puffer egy megabájtról tízre növelése nem vezetett sehova, és világossá vált, hogy a kódgenerátor körbe fut. Ellenőrizni kellett, hogy nem léptük-e túl a jelenlegi TB határait, és ha igen, akkor a következő TB címét negatív értékkel kellett visszaadni, hogy folytathassuk a végrehajtást. Ezen kívül ez megoldja a problémát, "milyen generált funkciókat kell érvényteleníteni, ha megváltozott ez a bytekód darab?" — csak azt a funkciót kell érvényteleníteni, amely megfelel ennek a fordítási blokknak. Egyébként, bár mindezt a Chromiumban debuggoltam (mivel Firefoxot használok, és könnyebb külön böngészőt használni a kísérletekhez), de a Firefox segített megoldani az asm.js szabvánnyal kapcsolatos inkompatibilitásokat, ami után a kód gyorsabban működött a Chromiumban.
Generált kód példája
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"]Zárás
Tehát a munka még mindig nem fejeződött be, de titokban már unom ezt a végtelen építkezést. Ezért úgy döntöttem, hogy publikálom, amit eddig elkészítettem. A kód helyenként ijesztő, mivel kísérlet, és előre nem világos, mit kell tenni. Valószínűleg később érdemes normális atomikus commitokat készíteni egy modernebb Qemu verzió fölé. Jelenleg van egy blogformátumú Git ágazat: minden, legalább valamilyen szinten elkészített "szinthez" hozzáadtam egy részletes megjegyzést oroszul. Szóval ez a cikk nagy mértékben – a kimenet átírása. git log.
Ezt az egészet ki lehet próbálni (óvatosan, forgalom).
Ami már most működik:
- Működik az x86 virtuális processzor
- Van egy működő prototípus JIT kódgenerátorról gépi kód és JavaScript között
- Van egy keret más 32 bites vendégarchitektúrák összeállításához: éppen most megcsodálhatja a Linuxot, ami a MIPS architektúrára töltődik a böngészőben
Még mit lehetne tenni
- Felgyorsítani az emulációt. Még JIT módban is úgy tűnik, lassabban működik, mint a Virtual x86 (viszont potenciálisan van egy komplett Qemu sok emulált hardverrel és architektúrával)
- Normális felületet készíteni – webfejlesztőként, hogy őszinte legyek, nem vagyok valami jó, ezért eddig a standard Emscripten shellt alakítottam át, ahogy tudtam
- Megpróbálni bonyolultabb funkciókat indítani a Qemu-ban – hálózat, VM migráció stb.
- UPD: El kellene juttatnom az Emscripten-hez a kevés eddigi munkámat és hiba-jelentésemet, ahogy a korábbi Qemu és más projektek portolói tették. Köszönöm nekik, hogy lehetőségem volt élni az Emscriptenbe való hozzájárulásukra a feladatom alapján.
Forrás: habr.com
