Néhány évvel ezelőtt Fabrice Bellard
- 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
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
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
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
É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 -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
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