Qemu.js s podporou JIT: stále môžete otočiť minútu dozadu

Pred niekoľkými rokmi Fabrice Bellard napísal jslinux je PC emulátor napísaný v JavaScripte. Potom toho bolo aspoň viac Virtuálny x86. Ale pokiaľ viem, všetky z nich boli tlmočníci, zatiaľ čo Qemu, napísané oveľa skôr tým istým Fabricem Bellardom, a pravdepodobne každý sebarešpektujúci moderný emulátor, používa JIT kompiláciu kódu hosťa do kódu hostiteľského systému. Zdalo sa mi, že je načase implementovať opačnú úlohu vo vzťahu k tej, ktorú riešia prehliadače: JIT kompiláciu strojového kódu do JavaScriptu, pre ktorú sa mi zdalo najlogickejšie portovať Qemu. Zdalo by sa, prečo Qemu, existujú jednoduchšie a užívateľsky prívetivejšie emulátory - napríklad rovnaký VirtualBox - nainštalované a fungujúce. Ale Qemu má niekoľko zaujímavých funkcií

  • open source
  • schopnosť pracovať bez ovládača jadra
  • schopnosť pracovať v režime tlmočníka
  • podpora veľkého počtu hostiteľských aj hosťujúcich architektúr

Čo sa týka tretieho bodu, teraz môžem vysvetliť, že v skutočnosti v režime TCI nie sú interpretované samotné inštrukcie stroja hosťa, ale bajtkód získaný z nich, ale to nemení podstatu - aby bolo možné zostaviť a spustiť Qemu na novej architektúre, ak budete mať šťastie, stačí C kompilátor - napísanie generátora kódu sa dá odložiť.

A teraz, po dvoch rokoch pokojného motania sa so zdrojovým kódom Qemu vo voľnom čase, sa objavil funkčný prototyp, v ktorom už môžete spustiť napríklad OS Kolibri.

Čo je Emscripten

V súčasnosti sa objavilo veľa kompilátorov, ktorých konečným výsledkom je JavaScript. Niektoré, ako napríklad Type Script, boli pôvodne zamýšľané ako najlepší spôsob písania pre web. Emscripten je zároveň spôsob, ako vziať existujúci kód C alebo C++ a skompilovať ho do podoby čitateľnej v prehliadači. Zapnuté na tejto stránke Zhromaždili sme veľa portov známych programov: tuMôžete sa napríklad pozrieť na PyPy - mimochodom, tvrdia, že už majú JIT. V skutočnosti nie je možné každý program jednoducho skompilovať a spustiť v prehliadači – existuje ich množstvo Vlastnosti, čo si však musíte vytrpieť, keďže nápis na tej istej strane hovorí „Emscripten sa dá použiť na zostavenie takmer akéhokoľvek prenosný C/C++ kód do JavaScriptu". To znamená, že existuje množstvo operácií, ktoré sú podľa štandardu nedefinované, ale zvyčajne fungujú na x86 - napríklad nezarovnaný prístup k premenným, ktorý je na niektorých architektúrach všeobecne zakázaný. Vo všeobecnosti , Qemu je multiplatformový program a chcel som veriť, že neobsahuje veľa nedefinovaného správania – vezmite ho a skompilujte, potom si trochu pohrajte s JIT – a máte hotovo! prípad...

Prvý pokus

Všeobecne povedané, nie som prvý, kto prišiel s myšlienkou preniesť Qemu do JavaScriptu. Na fóre ReactOS bola položená otázka, či je to možné pomocou Emscriptenu. Ešte skôr sa hovorilo, že to urobil Fabrice Bellard osobne, ale hovorili sme o jslinux, ktorý, pokiaľ viem, je len pokusom manuálne dosiahnuť dostatočný výkon v JS a bol napísaný od začiatku. Neskôr bol napísaný Virtual x86 - boli preň zverejnené neprehľadné zdroje a ako bolo uvedené, väčšia „realizmus“ emulácie umožnila použiť SeaBIOS ako firmvér. Okrem toho bol aspoň jeden pokus o portovanie Qemu pomocou Emscriptenu - pokúsil som sa o to pár zásuviek, ale vývoj, pokiaľ som pochopil, bol zmrazený.

Zdalo by sa teda, že tu sú zdroje, tu je Emscripten – vezmite a skompilujte. Existujú však aj knižnice, na ktorých závisí Qemu, a knižnice, od ktorých tieto knižnice závisia atď., a jednou z nich je libffi, od ktorej závisí glib. Na internete sa šírili zvesti, že vo veľkej zbierke knižníc pre Emscripten existuje jeden, ale bolo tomu ťažko uveriť: po prvé, nebol zamýšľaný ako nový kompilátor, po druhé, bol príliš nízkoúrovňový. knižnicu stačí vyzdvihnúť a skompilovať do JS. A nie je to len otázka montážnych vložiek - pravdepodobne, ak to otočíte, pre niektoré konvencie volania môžete vygenerovať potrebné argumenty v zásobníku a zavolať funkciu bez nich. Emscripten je však ošemetná vec: na to, aby vygenerovaný kód vyzeral dobre známy optimalizátorovi motora prehliadača JS, používajú sa niektoré triky. Najmä takzvané relooping - generátor kódu využívajúci prijaté IR LLVM s niektorými abstraktnými prechodovými pokynmi sa snaží znovu vytvoriť hodnoverné if, slučky atď. Ako sa argumenty prenášajú do funkcie? Prirodzene, ako argumenty pre funkcie JS, to znamená, ak je to možné, nie cez zásobník.

Na začiatku bol nápad jednoducho napísať náhradu za libffi s JS a spustiť štandardné testy, ale nakoniec som sa zamotal v tom, ako urobiť moje hlavičkové súbory tak, aby fungovali s existujúcim kódom - čo môžem urobiť, ako sa hovorí: "Sú úlohy také zložité "Sme takí hlúpi?" Musel som takpovediac portovať libffi na inú architektúru - našťastie, Emscripten má makrá na inline zostavenie (v Javascripte áno - no, akákoľvek architektúra, takže assembler), ako aj možnosť spúšťať kód generovaný za behu. Vo všeobecnosti, keď som sa nejaký čas pohrával s fragmentmi libffi závislými od platformy, dostal som nejaký kompilovateľný kód a spustil som ho pri prvom teste, na ktorý som narazil. Na moje prekvapenie bol test úspešný. Ohromený mojou genialitou - bez srandy, fungovalo to od prvého spustenia - stále neveriac vlastným očiam som sa šiel pozrieť na výsledný kód ešte raz, zhodnotiť, kam sa mám hrabať ďalej. Tu som sa zbláznil druhýkrát – jediné, čo robila moja funkcia, bolo ffi_call - toto hlásilo úspešný hovor. Samotný hovor sa nekonal. Poslal som teda svoju prvú požiadavku na ťahanie, ktorá opravila chybu v teste, ktorá je jasná každému študentovi olympiády – reálne čísla by sa nemali porovnávať ako a == b a dokonca ako a - b < EPS - musíte si tiež zapamätať modul, inak sa 0 ukáže ako veľmi rovná 1/3... Vo všeobecnosti som prišiel s určitým portom libffi, ktorý prechádza najjednoduchšími testami a s ktorým je glib zostavené - rozhodol som sa, že to bude potrebné, doplním neskôr. Pri pohľade do budúcnosti poviem, že ako sa ukázalo, kompilátor do konečného kódu ani nezahrnul funkciu libffi.

Ale, ako som už povedal, existujú určité obmedzenia a medzi voľným používaním rôznych nedefinovaných spôsobov správania sa skryla nepríjemnejšia vlastnosť - JavaScript podľa návrhu nepodporuje multithreading so zdieľanou pamäťou. V zásade sa to zvyčajne dá nazvať dobrým nápadom, ale nie pre portovanie kódu, ktorého architektúra je viazaná na vlákna C. Vo všeobecnosti Firefox experimentuje s podporou zdieľaných pracovníkov a Emscripten má pre nich implementáciu pthread, ale nechcel som sa na to spoliehať. Musel som z Qemu kódu pomaly vykoreniť multithreading – teda zistiť, kde vlákna bežia, presunúť telo cyklu bežiaceho v tomto vlákne do samostatnej funkcie a volať takéto funkcie jednu po druhej z hlavného cyklu.

Druhý pokus

V určitom momente sa ukázalo, že problém stále existuje a že náhodné posúvanie bariel okolo kódu nevedie k ničomu dobrému. Záver: musíme nejako systematizovať proces pridávania barlí. Preto bola prevzatá verzia 2.4.1, ktorá bola v tom čase čerstvá (nie 2.5.0, pretože ktovie, v novej verzii budú chyby, ktoré ešte nie sú podchytené, a ja mám dosť vlastných chýb ) a prvou vecou bolo bezpečne ho prepísať thread-posix.c. Teda ako bezpečné: ak sa niekto pokúsil vykonať operáciu vedúcu k zablokovaniu, funkcia bola okamžite vyvolaná abort() - to samozrejme nevyriesilo vsetky problemy naraz, ale aspon to bolo akosi prijemnejsie ako potichu prijimat nekonzistentne data.

Vo všeobecnosti sú možnosti Emscripten veľmi užitočné pri prenose kódu do JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - zachytávajú niektoré typy nedefinovaného správania, ako sú volania na nezarovnanú adresu (čo vôbec nie je v súlade s kódom pre typizované polia, ako napr. HEAP32[addr >> 2] = 1) alebo volanie funkcie s nesprávnym počtom argumentov.

Mimochodom, chyby zarovnania sú samostatným problémom. Ako som už povedal, Qemu má „degenerovaný“ interpretačný backend na generovanie kódu TCI (malý interpret kódu) a na zostavenie a spustenie Qemu na novej architektúre, ak budete mať šťastie, stačí kompilátor C. Kľúčové slová "ak budeš mať šťastie". Mal som smolu a ukázalo sa, že TCI používa pri analýze svojho bajtkódu nezarovnaný prístup. To znamená, že na všetkých druhoch ARM a iných architektúrach s nevyhnutne vyrovnaným prístupom sa Qemu kompiluje, pretože majú normálny TCG backend, ktorý generuje natívny kód, ale či na nich bude fungovať TCI, je iná otázka. Ako sa však ukázalo, dokumentácia TCI jasne naznačovala niečo podobné. V dôsledku toho boli do kódu pridané volania funkcií pre nezarovnané čítanie, ktoré boli objavené v inej časti Qemu.

Zničenie haldy

V dôsledku toho bol opravený nezarovnaný prístup k TCI, vytvorila sa hlavná slučka, ktorá zase volala procesor, RCU a niektoré ďalšie malé veci. A tak spúšťam Qemu s opciou -d exec,in_asm,out_asm, čo znamená, že musíte povedať, ktoré bloky kódu sa vykonávajú, a tiež v čase vysielania napísať, aký bol kód hosťa, aký kód hostiteľa sa stal (v tomto prípade bajtkód). Spustí sa, vykoná niekoľko blokov prekladu, napíše ladiacu správu, ktorú som nechal, že RCU sa teraz spustí a... havaruje abort() vnútri funkcie free(). Pohrávaním sa s funkciou free() Podarilo sa nám zistiť, že v hlavičke bloku haldy, ktorá leží v ôsmich bajtoch pred alokovanou pamäťou, sa namiesto veľkosti bloku alebo niečoho podobného nachádzal odpad.

Zničenie haldy – aké milé... V takom prípade existuje užitočný liek – z (ak je to možné) rovnakých zdrojov zostaviť natívnu binárku a spustiť ju pod Valgrindom. Po nejakom čase bol binárny súbor pripravený. Spúšťam ho s rovnakými možnosťami – zrúti sa aj počas inicializácie, kým sa skutočne nespustí. Je to, samozrejme, nepríjemné - zdroje zrejme neboli úplne rovnaké, čo nie je prekvapujúce, pretože v konfigurácii sa našli trochu iné možnosti, ale mám Valgrind - najprv opravím túto chybu a potom, ak budem mať šťastie , zobrazí sa pôvodný. Spúšťam to isté pod Valgrindom... Y-y-y, y-y-y, uh-uh, začalo to, normálne prešlo inicializáciou a prešlo cez pôvodnú chybu bez jediného varovania o nesprávnom prístupe k pamäti, nehovoriac o pádoch. Život, ako sa hovorí, ma na to nepripravil - padajúci program prestane padať pri spustení pod Walgrindom. Čo to bolo, je záhadou. Moja hypotéza je, že raz v blízkosti aktuálnej inštrukcie po havárii počas inicializácie, gdb ukázal prácu memset-a s platným ukazovateľom pomocou buď mmx, alebo xmm registrov, potom možno išlo o nejaký druh chyby zarovnania, aj keď je stále ťažké uveriť.

Dobre, zdá sa, že Valgrind tu nepomôže. A tu sa začala tá najnechutnejšia vec - zdá sa, že všetko sa dokonca začalo, ale z úplne neznámych príčin havaruje kvôli udalosti, ktorá sa mohla stať pred miliónmi pokynov. Dlho nebolo jasné ani to, ako pristupovať. Nakoniec som si aj tak musel sadnúť a odladiť. Vytlačenie toho, čím bola hlavička prepísaná, ukázalo, že to nevyzerá ako číslo, ale skôr ako nejaký binárny údaj. A hľa, tento binárny reťazec sa našiel v súbore BIOS – teda teraz bolo možné s primeranou istotou povedať, že išlo o pretečenie vyrovnávacej pamäte a dokonca je jasné, že bol zapísaný do tejto vyrovnávacej pamäte. No, potom niečo také - v Emscriptene našťastie neexistuje randomizácia adresného priestoru, nie sú v ňom ani diery, takže môžete písať niekde do stredu kódu na výstup údajov ukazovateľom od posledného spustenia, pozrite sa na údaje, pozrite sa na ukazovateľ a ak sa nezmenil, nechajte sa zamyslieť. Je pravda, že prepojenie po akejkoľvek zmene trvá niekoľko minút, ale čo môžete robiť? V dôsledku toho sa našiel špecifický riadok, ktorý skopíroval BIOS z dočasnej vyrovnávacej pamäte do pamäte hosťa - a skutočne, vo vyrovnávacej pamäti nebolo dosť miesta. Nájdenie zdroja tejto podivnej adresy vyrovnávacej pamäte vyústilo do funkcie qemu_anon_ram_alloc v súbore oslib-posix.c - logika tam bola takáto: niekedy môže byť užitočné zarovnať adresu na veľkú stránku s veľkosťou 2 MB, na to sa vás opýtame mmap najprv trochu viac a potom prebytok vrátime s pomocou munmap. A ak sa takéto zarovnanie nevyžaduje, potom namiesto 2 MB uvedieme výsledok getpagesize() - mmap stále bude poskytovať zarovnanú adresu... Takže v Emscriptene mmap len hovory malloc, ale samozrejme sa nezarovnáva na stránke. Vo všeobecnosti bola chyba, ktorá ma frustrovala niekoľko mesiacov, opravená zmenou двух linky.

Vlastnosti funkcií volania

A teraz procesor niečo počíta, Qemu nepadne, ale obrazovka sa nezapne a procesor rýchlo prejde do slučiek, súdiac podľa výstupu -d exec,in_asm,out_asm. Objavila sa hypotéza: prerušenia časovača (alebo vo všeobecnosti všetky prerušenia) neprichádzajú. A skutočne, ak odskrutkujete prerušenia z natívnej zostavy, ktorá z nejakého dôvodu fungovala, získate podobný obrázok. To však vôbec nebola odpoveď: porovnanie vydaných stôp s vyššie uvedenou možnosťou ukázalo, že trajektórie vykonávania sa rozchádzajú veľmi skoro. Tu treba povedať, že porovnanie toho, čo bolo zaznamenané pomocou launcheru emrun výstup ladenia s výstupom natívnej zostavy nie je úplne mechanický proces. Neviem presne, ako sa program spustený v prehliadači pripája emrun, ale ukázalo sa, že niektoré riadky vo výstupe sú preusporiadané, takže rozdiel v rozdiele ešte nie je dôvodom na to, aby sa predpokladalo, že sa trajektórie rozchádzajú. Vo všeobecnosti sa ukázalo, že podľa pokynov ljmpl dochádza k prechodu na rôzne adresy a vygenerovaný bajtový kód je zásadne odlišný: jeden obsahuje inštrukciu na volanie pomocnej funkcie, druhý nie. Po vygooglovaní pokynov a preštudovaní kódu, ktorý tieto pokyny prekladá, bolo jasné, že po prvé, bezprostredne pred ním v registri cr0 bol urobený záznam - aj pomocou pomocníka - ktorý prepol procesor do chráneného režimu a po druhé, že verzia js sa nikdy neprepla do chráneného režimu. Faktom však je, že ďalšou vlastnosťou Emscriptenu je jeho neochota tolerovať kód, akým je implementácia inštrukcií call v TCI, ktorého výsledkom je typ akéhokoľvek ukazovateľa funkcie long long f(int arg0, .. int arg9) - funkcie musia byť volané so správnym počtom argumentov. Ak dôjde k porušeniu tohto pravidla, v závislosti od nastavení ladenia program buď spadne (čo je dobré), alebo zavolá nesprávnu funkciu (čo bude smutné ladiť). Existuje aj tretia možnosť - povoliť generovanie wrapperov, ktoré pridávajú / odstraňujú argumenty, no celkovo tieto wrappery zaberajú veľa miesta, napriek tomu, že mi v skutočnosti stačí niečo viac ako sto. To samo o sebe je veľmi smutné, ale ukázalo sa, že je tu vážnejší problém: vo vygenerovanom kóde obalových funkcií sa argumenty konvertovali a konvertovali, ale niekedy sa funkcia s vygenerovanými argumentmi nezavolala - no, rovnako ako v moja implementácia libffi. To znamená, že niektorí pomocníci jednoducho neboli popravení.

Našťastie má Qemu strojovo čitateľné zoznamy pomocníkov vo forme hlavičkového súboru ako

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

Používajú sa celkom vtipne: po prvé, makrá sú predefinované tým najbizarnejším spôsobom DEF_HELPER_na potom sa zapne helper.h. Do tej miery, do akej sa makro rozšíri na inicializátor štruktúry a čiarku a potom sa definuje pole a namiesto prvkov - #include <helper.h> Vďaka tomu som mal konečne možnosť vyskúšať si knižnicu v práci pyparsinga bol napísaný skript, ktorý generuje presne tie obaly pre presne tie funkcie, pre ktoré sú potrebné.

A tak sa potom zdalo, že procesor funguje. Zdá sa, že obrazovka nebola nikdy inicializovaná, hoci memtest86+ bol schopný bežať v natívnom zostave. Tu je potrebné objasniť, že I/O kód bloku Qemu sa píše v korutínach. Emscripten má svoju vlastnú veľmi zložitú implementáciu, ale stále je potrebné ju podporovať v kóde Qemu a teraz môžete ladiť procesor: Qemu podporuje možnosti -kernel, -initrd, -append, pomocou ktorého môžete nabootovať Linux alebo napríklad memtest86+ úplne bez použitia blokových zariadení. Ale tu je problém: v natívnom zostave bolo možné vidieť výstup linuxového jadra do konzoly s možnosťou -nographica žiadny výstup z prehliadača do terminálu, z ktorého bol spustený emrun, neprišiel. To znamená, že nie je jasné: nefunguje procesor alebo nefunguje grafický výstup. A potom mi napadlo trochu počkať. Ukázalo sa, že „procesor nespí, ale jednoducho pomaly bliká“ a asi po piatich minútach jadro hodilo na konzolu veľa správ a pokračovalo v zavesení. Ukázalo sa, že procesor vo všeobecnosti funguje a musíme sa ponoriť do kódu pre prácu s SDL2. Bohužiaľ neviem, ako túto knižnicu používať, takže na niektorých miestach som musel konať náhodne. V určitom okamihu na obrazovke na modrom pozadí zablikala čiara rovnobežka0, čo naznačovalo nejaké myšlienky. Nakoniec sa ukázalo, že problém bol v tom, že Qemu otvára niekoľko virtuálnych okien v jednom fyzickom okne, medzi ktorými sa dá prepínať pomocou Ctrl-Alt-n: funguje to v natívnom zostavení, ale nie v Emscriptene. Po odstránení nepotrebných okien pomocou možností -monitor none -parallel none -serial none a pokyny na násilné prekreslenie celej obrazovky na každý snímok, všetko zrazu fungovalo.

Korutíny

Emulácia v prehliadači teda funguje, ale nemôžete v nej spustiť nič zaujímavé na jednej diskete, pretože neexistuje blokový vstup / výstup - musíte implementovať podporu pre korutíny. Qemu už má niekoľko koroutínových backendov, ale vzhľadom na povahu JavaScriptu a generátora kódu Emscripten nemôžete len tak začať žonglovať. Zdalo by sa, že „všetko je preč, omietka sa odstraňuje“, ale vývojári Emscriptenu sa už o všetko postarali. Toto je implementované celkom vtipne: nazvime takéto volanie funkcie podozrivé emscripten_sleep a niekoľko ďalších pomocou mechanizmu Asyncify, ako aj volania ukazovateľa a volania akejkoľvek funkcie, kde sa jeden z predchádzajúcich dvoch prípadov môže vyskytnúť ďalej v zásobníku. A teraz pred každým podozrivým volaním vyberieme asynchrónny kontext a hneď po zavolaní skontrolujeme, či došlo k asynchrónnemu volaniu, a ak áno, uložíme všetky lokálne premenné do tohto asynchrónneho kontextu, označíme, ktorá funkcia preniesť kontrolu na čas, kedy potrebujeme pokračovať vo vykonávaní a ukončiť aktuálnu funkciu. Tu je priestor na skúmanie účinku plytvanie — pre potreby pokračovania vo vykonávaní kódu po návrate z asynchrónneho volania kompilátor generuje „stub“ funkcie začínajúci po podozrivom volaní — takto: ak je n podozrivých volaní, tak sa funkcia rozšíri niekam n/2 časy — to je stále, ak nie. Majte na pamäti, že po každom potenciálne asynchrónnom volaní musíte do pôvodnej funkcie pridať uloženie niektorých lokálnych premenných. Následne som dokonca musel napísať jednoduchý skript v Pythone, ktorý na základe danej množiny obzvlášť nadužívaných funkcií, ktoré vraj „nedovoľujú, aby cez seba prešla asynchrónia“ (čiže podpora zásobníka a všetko, čo som práve opísal, neumožňujú práca v nich), označuje volania prostredníctvom ukazovateľov, v ktorých by mal kompilátor ignorovať funkcie, aby sa tieto funkcie nepovažovali za asynchrónne. A potom sú súbory JS pod 60 MB zjavne príliš veľa – povedzme aspoň 30. Hoci raz som nastavoval skript zostavy a omylom som vyhodil možnosti linkera, medzi ktoré -O3. Spustím vygenerovaný kód a Chromium zaberie pamäť a zlyhá. Potom som sa náhodou pozrel na to, čo sa pokúša stiahnuť... No, čo môžem povedať, aj mňa by zamrzlo, keby som bol požiadaný, aby som si premyslene preštudoval a zoptimalizoval 500+ MB Javascript.

Bohužiaľ, kontroly v kóde knižnice podpory Asyncify neboli úplne priateľské longjmp-s, ktoré sa používajú v kóde virtuálneho procesora, ale po malej oprave, ktorá zakáže tieto kontroly a násilne obnoví kontexty, ako keby bolo všetko v poriadku, kód fungoval. A potom sa začala zvláštna vec: niekedy sa spustili kontroly synchronizačného kódu - tie isté, ktoré zrútia kód, ak by mal byť podľa logiky vykonávania zablokovaný - niekto sa pokúsil chytiť už zachytený mutex. Našťastie sa ukázalo, že to nie je logický problém v serializovanom kóde - jednoducho som používal štandardnú funkčnosť hlavnej slučky, ktorú poskytuje Emscripten, ale niekedy asynchrónne volanie úplne rozbalilo zásobník a v tom momente zlyhalo setTimeout z hlavnej slučky - kód teda vstúpil do iterácie hlavnej slučky bez toho, aby opustil predchádzajúcu iteráciu. Prepísané na nekonečnú slučku a emscripten_sleepa problémy s mutexmi prestali. Kód sa stal ešte logickejším - koniec koncov, v skutočnosti nemám nejaký kód, ktorý pripravuje ďalší animačný rámec - procesor len niečo vypočíta a obrazovka sa pravidelne aktualizuje. Problémy sa tým však neskončili: niekedy sa vykonávanie Qemu jednoducho skončilo ticho bez akýchkoľvek výnimiek alebo chýb. V tej chvíli som to vzdal, ale pri pohľade dopredu poviem, že problém bol v tomto: kód koroutínu v skutočnosti nepoužíva setTimeout (alebo aspoň nie tak často, ako by ste si mysleli): funkcia emscripten_yield jednoducho nastaví príznak asynchrónneho volania. Celá pointa je v tom emscripten_coroutine_next nie je asynchrónna funkcia: interne kontroluje príznak, resetuje ho a prenesie riadenie tam, kde je to potrebné. To znamená, že propagácia zásobníka končí. Problém bol v tom, že v dôsledku use-after-free, ktoré sa objavilo, keď bol fond coroutine deaktivovaný kvôli tomu, že som neskopíroval dôležitý riadok kódu z existujúceho backendu coroutine, funkcia qemu_in_coroutine vrátila hodnotu true, hoci v skutočnosti mala vrátiť hodnotu false. To viedlo k hovoru emscripten_yield, nad ktorým na stohu nikto nebol emscripten_coroutine_next, stoh sa rozvinul až úplne hore, ale nie setTimeout, ako som už povedal, nebol vystavený.

Generovanie kódu JavaScript

A tu je v skutočnosti sľúbené „otočenie mletého mäsa späť“. Nie naozaj. Samozrejme, ak spustíme Qemu v prehliadači a v ňom Node.js, potom, prirodzene, po vygenerovaní kódu v Qemu dostaneme úplne nesprávny JavaScript. Ale predsa len nejaký druh spätnej transformácie.

Najprv trochu o tom, ako Qemu funguje. Okamžite mi odpustite: nie som profesionálny vývojár Qemu a moje závery môžu byť na niektorých miestach nesprávne. Ako sa hovorí, „názor študenta sa nemusí zhodovať s názorom učiteľa, Peanovou axiomatikou a zdravým rozumom“. Qemu má určitý počet podporovaných hosťujúcich architektúr a pre každú existuje adresár ako target-i386. Pri zostavovaní môžete určiť podporu pre niekoľko hosťujúcich architektúr, ale výsledkom bude len niekoľko binárnych súborov. Kód na podporu hosťujúcej architektúry zase generuje niektoré interné operácie Qemu, ktoré už TCG (Tiny Code Generator) premení na strojový kód pre hostiteľskú architektúru. Ako je uvedené v súbore readme umiestnenom v adresári tcg, toto bolo pôvodne súčasťou bežného kompilátora C, ktorý bol neskôr upravený pre JIT. Preto napríklad cieľová architektúra v zmysle tohto dokumentu už nie je hosťujúcou architektúrou, ale hostiteľskou architektúrou. V istom momente sa objavil ďalší komponent – ​​Tiny Code Interpreter (TCI), ktorý by mal vykonávať kód (takmer rovnaké interné operácie) pri absencii generátora kódu pre konkrétnu hostiteľskú architektúru. V skutočnosti, ako uvádza jeho dokumentácia, tento interpret nemusí vždy fungovať tak dobre ako generátor kódu JIT, a to nielen kvantitatívne, pokiaľ ide o rýchlosť, ale aj kvalitatívne. Aj keď si nie som istý, či je jeho popis úplne relevantný.

Najprv som sa snažil spraviť plnohodnotný TCG backend, no rýchlo som sa pomýlil v zdrojovom kóde a nie celkom jasnom popise inštrukcií bajtkódu, tak som sa rozhodol zabaliť TCI interpreter. To prinieslo niekoľko výhod:

  • pri implementácii generátora kódu by ste sa mohli pozrieť nie na popis inštrukcií, ale na kód interpreta
  • môžete generovať funkcie nie pre každý blok prekladu, ktorý sa vyskytuje, ale napríklad až po stom vykonaní
  • ak sa zmení vygenerovaný kód (a to sa zdá byť možné, súdiac podľa funkcií s názvami obsahujúcimi slovo patch), budem musieť vygenerovaný JS kód znehodnotiť, ale aspoň ho budem mať z čoho regenerovať

Pokiaľ ide o tretí bod, nie som si istý, či je oprava možná po prvom spustení kódu, ale stačia prvé dva body.

Pôvodne bol kód generovaný vo forme veľkého prepínača na adrese pôvodnej inštrukcie bajtkódu, ale potom, keď som si spomenul na článok o Emscriptene, optimalizácii generovaného JS a reloopingu, rozhodol som sa vygenerovať viac ľudského kódu, najmä preto, že empiricky to sa ukázalo, že jediným vstupným bodom do bloku prekladu je jeho Štart. Len čo sa povie, ako urobí, po chvíli sme mali generátor kódu, ktorý generoval kód pomocou if (aj keď bez slučiek). Ale smola, zrútil sa a vydal správu, že pokyny majú nesprávnu dĺžku. Navyše posledná inštrukcia na tejto úrovni rekurzie bola brcond. Dobre, pridám identickú kontrolu ku generovaniu tejto inštrukcie pred a po rekurzívnom volaní a... ani jedna z nich nebola vykonaná, ale po prepnutí potvrdenia stále zlyhali. Nakoniec som si po preštudovaní vygenerovaného kódu uvedomil, že po prepnutí sa pointer na aktuálnu inštrukciu znovu načíta zo zásobníka a pravdepodobne je prepísaný vygenerovaným JavaScript kódom. A tak to dopadlo. Zvýšenie vyrovnávacej pamäte z jedného megabajtu na desať neviedlo k ničomu a bolo jasné, že generátor kódu beží v kruhoch. Museli sme skontrolovať, či sme neprekročili hranice aktuálneho TBC, a ak áno, vydať adresu nasledujúceho TBC so znamienkom mínus, aby sme mohli pokračovať v exekúcii. Okrem toho sa tým rieši problém „ktoré vygenerované funkcie by sa mali zrušiť, ak sa tento kus bajtového kódu zmenil? — je potrebné zneplatniť iba funkciu, ktorá zodpovedá tomuto bloku prekladu. Mimochodom, hoci som v Chromiu všetko odladil (keďže používam Firefox a na experimenty je pre mňa jednoduchšie použiť samostatný prehliadač), Firefox mi pomohol opraviť nekompatibility so štandardom asm.js, po ktorom začal kód pracovať rýchlejšie v Chromium.

Príklad vygenerovaného kódu

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áver

Takže práca stále nie je dokončená, ale už ma nebaví potajomky privádzať túto dlhodobú stavbu k dokonalosti. Preto som sa rozhodol zverejniť to, čo zatiaľ mám. Kód je miestami trochu desivý, pretože ide o experiment a vopred nie je jasné, čo je potrebné urobiť. Pravdepodobne potom stojí za to vydať normálne atómové záväzky nad nejakou modernejšou verziou Qemu. Medzitým je v Gite vlákno vo formáte blogu: ku každej „úrovni“, ktorá bola aspoň nejakým spôsobom prejdená, bol pridaný podrobný komentár v ruštine. V skutočnosti je tento článok do značnej miery prerozprávaním záveru git log.

Môžete to všetko vyskúšať tu (pozor na premávku).

Čo už funguje:

  • spustený virtuálny procesor x86
  • Existuje funkčný prototyp generátora JIT kódu od strojového kódu po JavaScript
  • Existuje šablóna na zostavenie ďalších 32-bitových hosťujúcich architektúr: práve teraz môžete obdivovať Linux pre zmrazenie architektúry MIPS v prehliadači vo fáze načítania

Čo ešte môžeš urobiť

  • Urýchlite emuláciu. Dokonca aj v režime JIT sa zdá, že beží pomalšie ako Virtual x86 (ale potenciálne existuje celé Qemu s množstvom emulovaného hardvéru a architektúr)
  • Aby som vytvoril normálne rozhranie - úprimne povedané, nie som dobrý webový vývojár, takže zatiaľ som prerobil štandardný shell Emscripten, ako najlepšie viem
  • Skúste spustiť zložitejšie funkcie Qemu – sieťovanie, migrácia VM atď.
  • UPS: budete musieť odoslať svojich pár vývojov a hlásení o chybách do Emscriptenu upstream, ako to robili predchádzajúci nosiči Qemu a iných projektov. Ďakujem im za to, že mohli implicitne využiť ich príspevok pre Emscripten ako súčasť mojej úlohy.

Zdroj: hab.com

Pridať komentár