Qemu.js JIT-támogatással: a darált még mindig hátra lehet forgatni

Néhány évvel ezelőtt Fabrice Bellard írta: jslinux egy JavaScriptben írt PC-emulátor. Utána legalább volt több Virtuális x86. De amennyire én tudom, mindegyikük tolmács volt, míg a Qemu, amit jóval korábban írt ugyanaz a Fabrice Bellard, és valószínűleg minden önbecsülő modern emulátor, a vendégkód JIT fordítását használja a gazdagép rendszer kódjává. Úgy tűnt számomra, hogy itt az ideje végrehajtani az ellenkező feladatot azzal kapcsolatban, amit a böngészők megoldanak: a gépi kód JIT fordítását JavaScript-be, amelyhez a Qemu portolása tűnt a leglogikusabbnak. Úgy tűnik, miért a Qemu, vannak egyszerűbb és felhasználóbarát emulátorok - például ugyanaz a VirtualBox - telepítve és működik. A Qemu azonban számos érdekes tulajdonsággal rendelkezik

  • nyílt forráskód
  • kernel-illesztőprogram nélkül is képes dolgozni
  • képes tolmács módban dolgozni
  • nagyszámú gazdagép és vendég architektúra támogatása

A harmadik ponttal kapcsolatban most elmagyarázom, hogy valójában TCI módban nem magukat a vendéggép utasításait értelmezik, hanem az azokból kapott bájtkódot, de ez a lényegen nem változtat - a felépítés és a futtatás érdekében. Qemu új architektúrán, ha szerencséd van, elég egy C fordító is - a kódgenerátor megírása elodázható.

És most, két év szabadidőmben a Qemu forráskóddal való laza trükközés után megjelent egy működő prototípus, amiben már futhat például a Kolibri OS.

Mi az az Emscripten

Napjainkra számos fordítóprogram jelent meg, amelyeknek a végeredménye a JavaScript. Néhányat, például a Type Scriptet eredetileg a webírás legjobb módjának szánták. Ugyanakkor az Emscripten egy módja annak, hogy a meglévő C vagy C++ kódot átvegye és böngésző által olvasható formába fordítsa. Tovább ez az oldal Összegyűjtöttünk számos jól ismert program portot: ittPéldául megnézheti a PyPy-t – egyébként azt állítják, hogy már van JIT. Valójában nem minden programot lehet egyszerűen lefordítani és futtatni egy böngészőben – számos ilyen van jellemzők, amit viszont el kell viselned, mivel ugyanazon az oldalon a felirat azt írja: „Az Emscripten segítségével szinte bármilyen hordozható C/C++ kód a JavaScripthez". Vagyis számos olyan művelet létezik, amelyek a szabvány szerint nem definiált viselkedésűek, de általában x86-on működnek – például a változókhoz való igazított hozzáférés, ami bizonyos architektúrákon általában tiltott. Általában , A Qemu egy többplatformos program és , el akartam hinni, és még nem tartalmaz sok definiálatlan viselkedést – vedd le és fordítsd le, aztán bütykölj egy kicsit a JIT-tel – és kész! ügy...

Első próba

Általánosságban elmondható, hogy nem én vagyok az első, aki felötlött az ötlettel, hogy a Qemu-t át kell vinni JavaScriptre. Volt egy kérdés a ReactOS fórumon, hogy ez lehetséges-e az Emscripten használatával. Már korábban is voltak pletykák, hogy ezt Fabrice Bellard személyesen csinálta, de itt a jslinuxról beszéltünk, ami tudtommal csak egy kísérlet a kellő teljesítmény manuális elérésére JS-ben, és a nulláról íródott. Később megírták a Virtual x86-ot - rejtett forrásokat tettek közzé hozzá, és amint azt már említettük, az emuláció nagyobb „realizmusa” lehetővé tette a SeaBIOS firmware-ként való használatát. Ezen kívül volt legalább egy kísérlet a Qemu portolására az Emscripten használatával – ezt próbáltam megtenni aljzatpár, de a fejlesztés, amennyire értem, lefagyott.

Szóval, úgy tűnik, itt vannak a források, itt az Emscripten - vedd és fordítsd össze. De vannak olyan könyvtárak is, amelyektől a Qemu függ, és olyan könyvtárak is, amelyektől ezek a könyvtárak függenek stb., és ezek egyike a libffi, amitől függ. Az interneten olyan pletykák keringtek, hogy van egy az Emscripten könyvtárainak nagy portjainak gyűjteményében, de valahogy nehéz volt elhinni: egyrészt nem új fordítónak szánták, másrészt túl alacsony szintű volt. könyvtárat, hogy csak vegye fel, és fordítsa le JS-re. És ez nem csak összeállítási beszúrások kérdése - valószínűleg, ha megcsavarja, bizonyos hívási konvenciók esetén előállíthatja a szükséges argumentumokat a veremben, és ezek nélkül is meghívhatja a függvényt. De az Emscripten egy trükkös dolog: annak érdekében, hogy a generált kód ismerősnek tűnjön a böngésző JS motoroptimalizálója számára, néhány trükköt alkalmaznak. Különösen az úgynevezett relooping – a vett LLVM IR-t használó kódgenerátor néhány absztrakt átmeneti utasítással megpróbálja újra létrehozni a hihető if-eket, ciklusokat stb. Nos, hogyan adják át az argumentumokat a függvénynek? Természetesen a JS függvények argumentumaiként, vagyis ha lehetséges, nem a veremen keresztül.

Kezdetben az volt az ötlet, hogy egyszerűen írok egy helyettesítőt a libffi-re JS-sel, és futtassam le a szabványos teszteket, de végül összezavarodtam, hogyan csináljam a fejléceimet úgy, hogy a meglévő kóddal működjenek - mit tegyek? ahogy mondják: "Olyan összetettek a feladatok "Olyan hülyék vagyunk?" Úgymond más architektúrára kellett portolnom a libffi-t - szerencsére az Emscriptennek van makrók az inline assemblyhez (Javascriptben igen - nos, bármilyen architektúra, tehát az assembler), és a menet közben generált kód futtatásának lehetősége is. Általánosságban elmondható, hogy miután egy ideig platformfüggő libffi-töredékekkel bütykölgettem, kaptam néhány lefordítható kódot, és lefuttattam az első teszt alkalmával, amivel találkoztam. Meglepetésemre a teszt sikeres volt. A zsenialitásomtól megdöbbenve - nem vicc, az első indítástól kezdve működött -, továbbra sem hittem a szememnek, újra megnéztem a kapott kódot, hogy kiértékeljem, merre ássam tovább. Itt megőrültem másodszor – az egyetlen dolog, amit a feladatom teljesített ffi_call - ez sikeres hívásról számolt be. Nem volt hívás maga. Ezért elküldtem az első lehívási kérelmemet, amely kijavított egy hibát a tesztben, amely minden olimpiai diák számára egyértelmű – a valós számokat nem szabad összehasonlítani a == b és még azt is, hogyan a - b < EPS - emlékeznie kell a modulra is, különben a 0 nagyban megegyezik az 1/3-mal... Általánosságban elmondható, hogy egy bizonyos libffi portot találtam ki, amely a legegyszerűbb teszteken is megfelel, és amivel a glib összeállítva - úgy döntöttem, hogy szükség lesz rá, később kiegészítem. Előretekintve elmondom, hogy mint kiderült, a fordító még a libffi függvényt sem tette bele a végső kódba.

De, mint már mondtam, vannak korlátok, és a különféle definiálatlan viselkedések szabad használata között egy kellemetlenebb funkció is el lett rejtve - a JavaScript tervezése szerint nem támogatja a megosztott memóriával való többszálú feldolgozást. Ezt elvileg még jó ötletnek is lehet nevezni, de nem olyan kód portolására, amelynek architektúrája C szálhoz van kötve. Általánosságban elmondható, hogy a Firefox kísérletezik a megosztott dolgozók támogatásával, és az Emscriptennek van egy pthread implementációja a számukra, de nem akartam tőle függeni. Lassan ki kellett rootolni a Qemu kódból a multithreadinget - vagyis ki kellett derítenem, hogy hol futnak a szálak, az ebben a szálban futó ciklus törzsét külön függvénybe kell mozgatni, és az ilyen függvényeket egyenként meghívni a főhurokból.

Második kísérlet

Egy ponton világossá vált, hogy a probléma továbbra is fennáll, és hogy a kód körüli mankók véletlenül lökése nem vezet semmi jóra. Következtetés: valahogy rendszereznünk kell a mankók hozzáadásának folyamatát. Ezért az akkor még friss 2.4.1-es verziót vették (nem a 2.5.0-t, mert soha nem tudhatod, lesznek még nem elkapott hibák az új verzióban, és van elég a sajátomból is) hibák), és az első dolgom az volt, hogy biztonságosan átírtam thread-posix.c. Nos, biztonságosan: ha valaki blokkoláshoz vezető műveletet próbált végrehajtani, a funkció azonnal meghívásra került abort() - ez persze nem oldotta meg egyszerre az összes problémát, de legalább valahogy kellemesebb volt, mint csendesen fogadni az inkonzisztens adatokat.

Általában az Emscripten opciók nagyon hasznosak a kód JS-re történő áthordásakor -s ASSERTIONS=1 -s SAFE_HEAP=1 - elkapnak bizonyos típusú nem definiált viselkedéseket, mint például a nem igazított címek hívásait (ami egyáltalán nem konzisztens a begépelt tömbök kódjával, mint pl. HEAP32[addr >> 2] = 1) vagy nem megfelelő számú argumentumot tartalmazó függvény meghívása.

Egyébként az igazítási hibák egy külön kérdés. Ahogy már mondtam, a Qemu rendelkezik egy „degenerált” értelmező háttérrendszerrel a kódgeneráló TCI-hez (apró kódértelmező), és a Qemu új architektúrán való felépítéséhez és futtatásához, ha szerencséd van, elég egy C fordító. Kulcsszavak "ha szerencséd van". Nem volt szerencsém, és kiderült, hogy a TCI nem igazított hozzáférést használ a bájtkód elemzésekor. Vagyis mindenféle ARM-en és egyéb, szükségszerűen szintezett hozzáférésű architektúrákon a Qemu fordít, mert van egy normál TCG háttérprogramjuk, ami natív kódot generál, de hogy a TCI működni fog-e rajtuk, az más kérdés. Azonban, mint kiderült, a TCI dokumentációja egyértelműen valami hasonlót jelez. Ennek eredményeként a kódhoz adták a nem igazított olvasás funkcióhívásait, amelyek a Qemu másik részében találhatók.

Halompusztítás

Ennek eredményeként a TCI-hez való igazodás nélküli hozzáférést kijavították, egy fő hurkot hoztak létre, amely a processzort, az RCU-t és néhány egyéb apróságot hívta. Így elindítom a Qemut az opcióval -d exec,in_asm,out_asm, ami azt jelenti, hogy meg kell mondani, hogy mely kódblokkok futnak, és az adáskor meg kell írni, hogy mi volt a vendégkód, mi lett a gazdagép kódja (jelen esetben bájtkód). Elindul, végrehajt több fordítási blokkot, kiírja az általam hagyott hibakereső üzenetet, hogy az RCU most elindul és... összeomlik abort() egy függvényen belül free(). A funkcióval való trükközéssel free() Sikerült kiderítenünk, hogy a halom blokk fejlécében, amely a lefoglalt memóriát megelőző nyolc bájtban található, a blokkméret vagy valami hasonló helyett szemét volt.

A kupac elpusztítása - milyen cuki... Ilyenkor van egy hasznos szer - (ha lehet) ugyanabból a forrásból, állíts össze egy natív binárist és futtasd Valgrind alatt. Egy idő után a bináris készen állt. Ugyanazokkal az opciókkal indítom el - még inicializálás közben is összeomlik, mielőtt ténylegesen elérné a végrehajtást. Ez persze kellemetlen - úgy tűnik, a források nem voltak teljesen azonosak, ami nem meglepő, mert a configure kicsit más lehetőségeket keresett, de nekem Valgrind van - először kijavítom ezt a hibát, majd ha szerencsém van , megjelenik az eredeti. Ugyanezt futtatom Valgrind alatt... Y-y-y, y-y-y, uh-uh, elindult, normálisan átment az inicializáláson, és továbbment az eredeti hibán, anélkül, hogy egyetlen figyelmeztetést is kapott volna a helytelen memóriaelérésről, nem beszélve az esésekről. Az élet, ahogy mondani szokták, nem készített fel erre – egy összeomló program leáll, ha Walgrind alatt elindítják. Hogy mi volt, az rejtély. Az a hipotézisem, hogy egyszer az aktuális utasítás közelében az inicializálás során bekövetkezett összeomlás után a gdb működést mutatott memset-a érvényes mutatóval bármelyiket használva mmx, vagy xmm regiszterek, akkor talán valami igazítási hiba volt, bár még mindig nehéz elhinni.

Oké, úgy tűnik, hogy Valgrind itt nem segít. És itt kezdődött a legundorítóbb dolog - úgy tűnik, hogy minden elindul, de teljesen ismeretlen okokból összeomlik egy esemény miatt, amely több millió utasítással ezelőtt megtörténhetett. Sokáig nem is volt világos, hogyan kell megközelíteni. A végén még le kellett ülnöm és hibakeresést végeznem. Kinyomtatva, hogy mivel írták át a fejlécet, kiderült, hogy nem számnak, hanem valami bináris adatnak tűnik. És lám, ez a bináris karakterlánc megtalálható a BIOS fájlban – vagyis most már kellő biztonsággal ki lehetett állítani, hogy puffertúlcsordulásról van szó, sőt az is világos, hogy ebbe a pufferbe írták. Na, akkor valami ilyesmi - az Emscriptenben szerencsére nincs véletlenszerű a címtér, nincsenek benne lyukak sem, így valahova a kód közepére lehet írni, hogy az utolsó indításból mutatónként kiadja az adatokat, nézd meg az adatokat, nézd meg a mutatót, és ha nem változott, kapj elgondolkodtatót. Igaz, minden változtatás után néhány percet vesz igénybe a linkelés, de mit tehetsz? Ennek eredményeként egy speciális sort találtak, amely átmásolta a BIOS-t az ideiglenes pufferből a vendégmemóriába - és valóban, nem volt elég hely a pufferben. Ennek a furcsa puffercímnek a forrásának megtalálása egy függvényt eredményezett qemu_anon_ram_alloc fájlban oslib-posix.c - a logika a következő volt: néha hasznos lehet egy hatalmas, 2 MB méretű oldalhoz igazítani a címet, ehhez megkérdezzük mmap először még egy kicsit, majd a felesleget visszaadjuk a segítséggel munmap. És ha nincs szükség ilyen igazításra, akkor 2 MB helyett az eredményt jelezzük getpagesize() - mmap akkor is kiad egy igazított címet... Tehát az Emscriptenben mmap csak hív malloc, de természetesen nem igazodik az oldalhoz. Általánosságban elmondható, hogy egy olyan hibát, amely néhány hónapig frusztrált, egy változtatással kijavítottak двух vonalak.

A hívási funkciók jellemzői

És most a processzor számol valamit, a Qemu nem omlik össze, de a képernyő nem kapcsol be, és a processzor gyorsan hurokba megy, a kimenetből ítélve -d exec,in_asm,out_asm. Felmerült egy hipotézis: az időzítő megszakítások (vagy általában minden megszakítás) nem érkeznek meg. És valóban, ha lecsavarja a megszakításokat a natív összeállításról, amely valamiért működött, hasonló képet kap. De egyáltalán nem ez volt a válasz: a fenti opcióval kiadott nyomok összehasonlítása azt mutatta, hogy a végrehajtási pályák nagyon korán szétváltak. Itt el kell mondani, hogy összehasonlítjuk az indítóval rögzítetteket emrun a kimenet hibakeresése a natív összeállítás kimenetével nem teljesen mechanikus folyamat. Nem tudom pontosan, hogyan csatlakozik egy böngészőben futó program emrun, de a kimenet egyes sorai átrendeződnek, így a különbség különbsége még nem ad okot annak feltételezésére, hogy a pályák szétváltak. Általában világossá vált, hogy az utasításoknak megfelelően ljmpl van átmenet a különböző címekre, és a generált bájtkód alapvetően más: az egyik tartalmaz egy helper függvény meghívására vonatkozó utasítást, a másik nem. Az utasítások guglizása és az utasításokat lefordító kód tanulmányozása után világossá vált, hogy először is közvetlenül előtte a nyilvántartásban cr0 felvétel készült - szintén segéd segítségével -, ami a processzort védett módba kapcsolta, másodszor pedig arról, hogy a js verzió soha nem vált védett módba. A tény azonban az, hogy az Emscripten másik jellemzője, hogy nem hajlandó elviselni az olyan kódokat, mint például az utasítások végrehajtása. call TCI-ben, amely bármely függvénymutató típust eredményez long long f(int arg0, .. int arg9) - a függvényeket a megfelelő számú argumentummal kell meghívni. Ha ezt a szabályt megsértik, a hibakeresési beállításoktól függően a program vagy összeomlik (ami jó), vagy egyáltalán nem a megfelelő függvényt hívja meg (amit szomorú lesz a hibakeresés). Van egy harmadik lehetőség is - engedélyezze az argumentumokat hozzáadó / eltávolító burkolók generálását, de összességében ezek a burkolók sok helyet foglalnak el, annak ellenére, hogy valójában csak valamivel több mint száz burkolóra van szükségem. Ez már önmagában nagyon szomorú, de kiderült, hogy volt egy komolyabb probléma: a wrapper függvények generált kódjában az argumentumokat konvertálták és konvertálták, de előfordult, hogy a generált argumentumokkal rendelkező függvényt nem hívták meg - nos, pont úgy, mint pl. az én libffi implementációmat. Vagyis néhány segítőt egyszerűen nem végeztek ki.

Szerencsére a Qemu rendelkezik a segítők géppel olvasható listáival, például fejlécfájl formájában

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

Elég viccesen használják őket: először is a makrókat a legfurcsább módon definiálják újra DEF_HELPER_n, majd bekapcsol helper.h. Olyan mértékben, hogy a makrót kibontjuk szerkezet-inicializálóvá és vesszővé, majd egy tömböt definiálunk, és elemek helyett - #include <helper.h> Ennek eredményeként végre lehetőségem nyílt kipróbálni a könyvtárat a munkahelyemen pyparsing, és egy szkriptet írtak, amely pontosan azokat a wrappereket állítja elő, amelyekhez pontosan azokra a funkciókra van szükség, amelyekre szükség van.

És így, ezek után a processzor működni látszott. Úgy tűnik, azért, mert a képernyő soha nem volt inicializálva, bár a memtest86+ futni tudott a natív összeállításban. Itt tisztázni kell, hogy a Qemu blokk I/O kódja korutinokban van írva. Az Emscriptennek megvan a maga nagyon trükkös megvalósítása, de még mindig támogatni kellett a Qemu kódban, és most már hibakeresheti a processzort: a Qemu támogatja az opciókat -kernel, -initrd, -append, amellyel a Linux vagy például a memtest86+ indítható, blokkeszközök használata nélkül. De itt van a probléma: a natív összeállításban láthatjuk a Linux kernel kimenetét a konzolra az opcióval -nographic, és nincs kimenet a böngészőből arra a terminálra, ahonnan elindult emrun, nem jött. Vagyis nem egyértelmű: a processzor nem működik, vagy a grafikus kimenet nem működik. És akkor eszembe jutott, hogy várjak egy kicsit. Kiderült, hogy „a processzor nem alszik, hanem egyszerűen csak lassan villog”, és körülbelül öt perc múlva a kernel egy csomó üzenetet dobott a konzolra, és tovább lógott. Világossá vált, hogy a processzor általában működik, és bele kell ásnunk a kódba az SDL2-vel való együttműködéshez. Sajnos nem tudom, hogyan kell használni ezt a könyvtárat, így néhány helyen véletlenszerűen kellett cselekednem. Valamikor a párhuzamos0 vonal kék alapon felvillant a képernyőn, ami gondolatokat sugallt. Végül kiderült, hogy a probléma az volt, hogy a Qemu több virtuális ablakot nyit meg egy fizikai ablakban, amelyek között Ctrl-Alt-n segítségével lehet váltani: natív buildben működik, Emscriptenben viszont nem. Miután megszabadult a felesleges ablakoktól az opciók használatával -monitor none -parallel none -serial none és utasításokat a teljes képernyő erőszakos újrarajzolására minden képkockán, hirtelen minden működött.

Korutinok

Tehát az emuláció a böngészőben működik, de nem lehet benne semmi érdekeset futtatni egy hajlékonylemezen, mert nincs blokk I/O - meg kell valósítani a korutinok támogatását. A Qemu-nak már több korutin backendje van, de a JavaScript és az Emscripten kódgenerátor természete miatt nem lehet csak úgy elkezdeni zsonglőrködni a veremekkel. Úgy tűnik, hogy „minden eltűnt, a vakolatot eltávolítják”, de az Emscripten fejlesztői már mindent elintéztek. Ez elég viccesen van megvalósítva: nevezzünk egy ilyen függvényhívást gyanúsnak emscripten_sleep és számos más, az Asyncify mechanizmust használó, valamint mutatóhívások és bármely olyan funkció hívása, ahol az előző két eset valamelyike ​​előfordulhat lejjebb a veremben. És most minden gyanús hívás előtt kiválasztunk egy aszinkron kontextust, és közvetlenül a hívás után ellenőrizzük, hogy történt-e aszinkron hívás, és ha igen, akkor az összes helyi változót elmentjük ebben az aszinkron kontextusban, jelezve, hogy melyik függvény a vezérlés átadásához, amikor folytatnunk kell a végrehajtást, és kilépünk az aktuális funkcióból. Itt van lehetőség a hatás tanulmányozására pazarlás — az aszinkron hívásból való visszatérés utáni kódvégrehajtás folytatásához a fordító egy gyanús hívás után induló függvény „csonkjait” generálja – így: ha van n gyanús hívás, akkor a függvény valahol n/2 kibővül. alkalommal – ez még mindig, ha nem Ne feledje, hogy minden potenciálisan aszinkron hívás után hozzá kell adnia néhány helyi változó mentését az eredeti függvényhez. Utána még egy egyszerű szkriptet is kellett írnom Pythonban, ami a különösen túlzottan használt függvények adott halmaza alapján, amelyek állítólag „nem engedik át az aszinkront magukon” (vagyis a verempromóció és minden, amit az imént leírtam). munka bennük), mutatókon keresztül jelzi azokat a hívásokat, amelyekben a függvényeket figyelmen kívül kell hagynia a fordítónak, hogy ezeket a függvényeket ne tekintsék aszinkronnak. És akkor a 60 MB alatti JS fájlok egyértelműen túl sok - mondjuk legalább 30. Bár, amikor egy assembly scriptet állítottam be, véletlenül kidobtam a linker opciókat, amelyek között volt -O3. Futtatom a generált kódot, és a Chromium felemészti a memóriát, és összeomlik. Utána véletlenül megnéztem, hogy mit próbál letölteni... Hát mit mondjak, én is lefagytam volna, ha megkérnek egy 500+ MB Javascript átgondolt tanulmányozására és optimalizálására.

Sajnos az Asyncify támogatási könyvtár kódjának ellenőrzései nem voltak teljesen barátságosak longjmp-s, amelyeket a virtuális processzor kódjában használnak, de egy kis javítás után, amely letiltja ezeket az ellenőrzéseket, és erőszakosan visszaállítja a kontextusokat, mintha minden rendben lenne, a kód működött. És ekkor egy furcsa dolog kezdődött: néha a szinkronizációs kód ellenőrzése indult el – ugyanazok, amelyek összeomlik a kódot, ha a végrehajtási logika szerint blokkolni kellene –, valaki megpróbált megragadni egy már rögzített mutexet. Szerencsére kiderült, hogy ez nem logikai probléma a soros kódban - egyszerűen az Emscripten által biztosított szabványos főhurok funkciót használtam, de néha az aszinkron hívás teljesen kibontotta a veremet, és abban a pillanatban meghiúsult. setTimeout a fő hurokból - így a kód belépett a fő hurok iterációjába anélkül, hogy elhagyta volna az előző iterációt. Végtelen cikluson újraírt és emscripten_sleep, és a mutexekkel kapcsolatos problémák megszűntek. A kód még logikusabb is lett - elvégre valójában nincs olyan kódom, amely előkészíti a következő animációs képkockát - a processzor csak kiszámít valamit, és a képernyő rendszeresen frissül. A problémák azonban nem álltak meg itt: néha a Qemu végrehajtása egyszerűen leállt, kivételek és hibák nélkül. Abban a pillanatban lemondtam róla, de előre tekintve azt mondom, hogy a probléma a következő volt: a korutin kód valójában nem használja setTimeout (vagy legalábbis nem olyan gyakran, mint gondolná): funkció emscripten_yield egyszerűen beállítja az aszinkron hívásjelzőt. Az egész lényege az emscripten_coroutine_next nem aszinkron funkció: belsőleg ellenőrzi a jelzőt, alaphelyzetbe állítja és átadja a vezérlést oda, ahol szükséges. Vagyis a verem promóciója ezzel véget is ér. A probléma az volt, hogy a használat utáni szabad használat miatt, ami a korutinkészlet letiltásakor jelent meg, mivel nem másoltam át egy fontos kódsort a meglévő korutine háttérprogramból, a funkció qemu_in_coroutine igazat adott vissza, pedig valójában hamisat kellett volna visszaadnia. Ez híváshoz vezetett emscripten_yield, amely felett nem volt senki a veremben emscripten_coroutine_next, a verem a legtetejéig kibontakozott, de nem setTimeout, mint már mondtam, nem volt kiállítva.

JavaScript kód generálása

És valójában itt van a beígért „a darált hús visszafordítása”. Nem igazán. Természetesen, ha a böngészőben futtatjuk a Qemu-t, és abban a Node.js-t, akkor természetesen a Qemu-ban kódgenerálás után teljesen rossz JavaScriptet kapunk. De mégis, valamiféle fordított átalakulás.

Először is egy kicsit a Qemu működéséről. Kérlek, azonnal bocsáss meg: nem vagyok profi Qemu fejlesztő, és a következtetéseim helyenként hibásak lehetnek. Ahogy mondják: „a diák véleményének nem kell egybeesnie a tanár véleményével, Peano axiomatikájával és józan eszével”. A Qemu bizonyos számú támogatott vendégarchitektúrával rendelkezik, és mindegyikhez van egy-egy könyvtár target-i386. Építéskor több vendégarchitektúra támogatását is megadhatja, de az eredmény csak több bináris lesz. A vendég architektúrát támogató kód viszont előállít néhány belső Qemu műveletet, amelyeket a TCG (Tiny Code Generator) már gépi kóddá alakít a gazdagép architektúra számára. Ahogy a tcg könyvtárban található readme fájlban szerepel, ez eredetileg egy szokásos C fordító része volt, amelyet később a JIT-hez adaptáltak. Ezért például a célarchitektúra ebben a dokumentumban már nem vendég architektúra, hanem gazdagép architektúra. Valamikor megjelent egy másik összetevő - a Tiny Code Interpreter (TCI), amelynek kódot kell végrehajtania (majdnem ugyanazokat a belső műveleteket), ha nincs kódgenerátor egy adott gazdagép architektúrához. Valójában, amint azt a dokumentáció is írja, ez az értelmező nem mindig teljesít olyan jól, mint egy JIT kódgenerátor, nemcsak mennyiségileg a sebesség, hanem minőségi szempontból is. Bár nem vagyok benne biztos, hogy a leírása teljesen releváns.

Eleinte megpróbáltam egy teljes értékű TCG backendet készíteni, de hamar összezavarodtam a forráskódban és a bájtkód utasítások nem teljesen világos leírásában, így a TCI interpreter becsomagolása mellett döntöttem. Ez több előnnyel járt:

  • kódgenerátor implementálásakor nem az utasítások leírását, hanem az értelmező kódot lehetett nézni
  • nem minden előforduló fordítási blokkhoz generálhat függvényeket, hanem például csak a századik végrehajtás után
  • ha a generált kód megváltozik (és ez lehetségesnek tűnik, a patch szót tartalmazó függvényekből ítélve), akkor érvényteleníteni kell a generált JS kódot, de legalább lesz miből újragenerálni.

A harmadik ponttal kapcsolatban nem vagyok benne biztos, hogy a kód első végrehajtása után lehetséges-e a foltozás, de az első két pont elég.

Kezdetben a kódot egy nagy kapcsoló formájában generálták az eredeti bájtkód utasítás címén, de aztán, emlékezve az Emscriptenről szóló cikkre, a generált JS optimalizálására és újrahurkolására, úgy döntöttem, hogy több emberi kódot generálok, különösen mivel empirikusan ez kiderült, hogy a fordítási blokk egyetlen belépési pontja a Start. Alighogy megtörtént, egy idő után volt egy kódgenerátorunk, amely if-ekkel generált kódot (bár ciklusok nélkül). De balszerencse, lezuhant, és azt jelezte, hogy az utasítások nem megfelelő hosszúságúak. Sőt, az utolsó utasítás ezen a rekurziós szinten az volt brcond. Oké, hozzáadok egy azonos ellenőrzést ennek az utasításnak a generálásához a rekurzív hívás előtt és után, és... egyik sem futott le, de az assert váltás után még mindig meghiúsult. Végül a generált kód tanulmányozása után jöttem rá, hogy a váltás után az aktuális utasításra mutató mutató újra betöltődik a veremből, és valószínűleg felülírja a generált JavaScript kód. És így is lett. A puffer egy megabájtról tízre való növelése nem vezetett semmire, és egyértelművé vált, hogy a kódgenerátor körökben fut. Ellenőriznünk kellett, hogy nem léptük-e túl a jelenlegi TB határait, és ha igen, akkor mínuszjellel adjuk ki a következő TB címét, hogy folytatni tudjuk a végrehajtást. Ezenkívül ez megoldja azt a problémát, hogy „melyik generált függvényeket kell érvényteleníteni, ha ez a bájtkód megváltozott?” — csak az ennek a fordítási blokknak megfelelő függvényt kell érvényteleníteni. Egyébként, bár mindent hibakerestem a Chromiumban (mivel Firefoxot használok, és könnyebb külön böngészőt használni a kísérletekhez), a Firefox segített kijavítani az asm.js szabvánnyal való összeférhetetlenségeket, ami után a kód gyorsabban kezdett működni Króm.

Példa generált kódra

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

Következtetés

Tehát a munka még mindig nem fejeződött be, de elegem van abból, hogy ezt a hosszú távú konstrukciót titokban tökéletesítsem. Ezért úgy döntöttem, hogy közzéteszem, ami most van. A kód helyenként kissé ijesztő, mert ez egy kísérlet, és nem világos előre, hogy mit kell tenni. Valószínűleg akkor érdemes normális atomcommitokat kiadni a Qemu modernebb verziója mellé. Addig is van egy szál a Gitában blog formátumban: minden olyan „szinthez”, amelyet legalább valahogy átmentek, egy részletes orosz nyelvű kommentár került. Valójában ez a cikk nagymértékben a következtetés újramondása git log.

Kipróbálhatod az egészet itt (Óvakodj a forgalomtól).

Ami már működik:

  • x86 virtuális processzor fut
  • Létezik egy működő prototípus a JIT kódgenerátornak gépi kódtól JavaScriptig
  • Van egy sablon más 32 bites vendégarchitektúrák összeállításához: most megcsodálhatja a Linuxot, mert a MIPS architektúra lefagy a böngészőben a betöltési szakaszban

Mi mást tudnál tenni

  • Emuláció felgyorsítása. Még JIT módban is úgy tűnik, hogy lassabban fut, mint a Virtual x86 (de lehetséges, hogy egy egész Qemu van, sok emulált hardverrel és architektúrával)
  • Normál felület létrehozásához - őszintén szólva, nem vagyok jó webfejlesztő, ezért most a legjobb tudásom szerint újrakészítettem a szabványos Emscripten shellt
  • Próbáljon meg bonyolultabb Qemu-funkciókat indítani - hálózatépítés, virtuális gép-migráció stb.
  • UPS: el kell küldened néhány fejlesztésedet és hibajelentésedet az Emscriptennek upstream, ahogy a Qemu és más projektek korábbi portékái tették. Köszönöm nekik, hogy hallgatólagosan felhasználhatták az Emscriptenhez való hozzájárulásukat a feladatom részeként.

Forrás: will.com

Hozzászólás