Qemu.js s podporou JIT: stále můžete otočit sekanou dozadu

Před několika lety Fabrice Bellard napsal jslinux je PC emulátor napsaný v JavaScriptu. Poté toho bylo alespoň víc Virtuální x86. Ale pokud vím, všechny z nich byly interprety, zatímco Qemu, napsané mnohem dříve stejným Fabricem Bellardem, a pravděpodobně jakýkoli seberespektující moderní emulátor, používá JIT kompilaci kódu hosta do kódu hostitelského systému. Zdálo se mi, že nastal čas implementovat opačný úkol ve vztahu k tomu, který řeší prohlížeče: JIT kompilaci strojového kódu do JavaScriptu, pro kterou se mi zdálo nejlogičtější portovat Qemu. Zdálo by se, proč Qemu, existují jednodušší a uživatelsky přívětivější emulátory - například stejný VirtualBox - nainstalované a fungující. Ale Qemu má několik zajímavých funkcí

  • open source
  • schopnost pracovat bez ovladače jádra
  • schopnost pracovat v režimu tlumočníka
  • podpora velkého počtu hostitelských i hostujících architektur

Pokud jde o třetí bod, mohu nyní vysvětlit, že ve skutečnosti v režimu TCI nejsou interpretovány samotné instrukce hostujícího stroje, ale bytekód získaný z nich, ale to nemění podstatu - za účelem sestavení a spuštění Qemu na nové architektuře, pokud budete mít štěstí, stačí kompilátor C - psaní generátoru kódu se může odložit.

A nyní, po dvou letech poklidného šťourání se zdrojovým kódem Qemu ve volném čase, se objevil funkční prototyp, ve kterém již můžete provozovat například Kolibri OS.

Co je Emscripten

V dnešní době se objevilo mnoho kompilátorů, jejichž konečným výsledkem je JavaScript. Některé, jako Type Script, byly původně zamýšleny jako nejlepší způsob psaní pro web. Emscripten je zároveň způsob, jak vzít existující kód v C nebo C++ a zkompilovat ho do podoby čitelné pro prohlížeč. Na na této stránce Shromáždili jsme mnoho portů známých programů: zdeMůžete se například podívat na PyPy - mimochodem tvrdí, že už mají JIT. Ve skutečnosti nelze každý program jednoduše zkompilovat a spustit v prohlížeči – existuje jich celá řada funkce, se kterým se však musíte smířit, jelikož nápis na stejné stránce říká „Emscripten lze použít k sestavení téměř jakéhokoli přenosný C/C++ kód do JavaScriptu". To znamená, že existuje řada operací, které jsou podle standardu nedefinovaným chováním, ale obvykle fungují na x86 - například nezarovnaný přístup k proměnným, který je na některých architekturách obecně zakázán. Obecně , Qemu je multiplatformní program a , chtěl jsem věřit, že už neobsahuje mnoho nedefinovaného chování – vezměte ho a zkompilujte, pak si trochu pohrajte s JIT – a máte hotovo! Ale to není ono pouzdro...

První pokus

Obecně řečeno, nejsem první, kdo přišel s myšlenkou přenést Qemu do JavaScriptu. Na fóru ReactOS byla položena otázka, zda je to možné pomocí Emscriptenu. Ještě dříve se objevily zvěsti, že to udělal Fabrice Bellard osobně, ale mluvili jsme o jslinuxu, který, pokud vím, je jen pokusem ručně dosáhnout dostatečného výkonu v JS a byl napsán od nuly. Později byl napsán Virtual x86 – byly pro něj zveřejněny nezastřené zdroje a jak již bylo řečeno, větší „realističnost“ emulace umožnila použít SeaBIOS jako firmware. Kromě toho byl alespoň jeden pokus o portování Qemu pomocí Emscriptenu - zkusil jsem to udělat pár zásuvek, ale vývoj, pokud jsem pochopil, byl zmrazen.

Zdá se tedy, že zde jsou zdroje, zde je Emscripten - vezměte a zkompilujte. Existují ale také knihovny, na kterých závisí Qemu, a knihovny, na kterých tyto knihovny závisí atd., a jednou z nich je libffi, na kterém závisí glib. Na internetu se objevily zvěsti, že ve velké sbírce knihoven pro Emscripten existuje jeden, ale bylo tomu tak nějak těžko uvěřit: zaprvé to nebylo zamýšleno jako nový kompilátor, zadruhé byl příliš nízkoúrovňový. knihovnu, kterou stačí vyzvednout a zkompilovat do JS. A není to jen záležitost vložení sestavy – pravděpodobně, pokud to zkroutíte, pro některé konvence volání můžete vygenerovat potřebné argumenty na zásobníku a volat funkci bez nich. Emscripten je ale ošemetná věc: aby vygenerovaný kód vypadal povědomě optimalizátoru enginu prohlížeče JS, používají se některé triky. Zejména takzvané relooping - generátor kódu využívající přijaté IR LLVM s některými abstraktními přechodovými instrukcemi se snaží znovu vytvořit věrohodné if, smyčky atd. Jak jsou argumenty předány funkci? Přirozeně jako argumenty funkcí JS, tedy pokud možno ne přes zásobník.

Na začátku byl nápad jednoduše napsat náhradu za libffi s JS a spustit standardní testy, ale nakonec jsem byl zmatený, jak udělat moje hlavičkové soubory tak, aby fungovaly se stávajícím kódem - co mohu dělat, jak se říká: "Jsou úkoly tak složité "Jsme tak hloupí?" Musel jsem portovat libffi na jinou architekturu, abych tak řekl - naštěstí Emscripten má jak makra pro inline sestavení (v Javascriptu jo - no, jakákoliv architektura, takže assembler), tak možnost spouštět kód generovaný za běhu. Obecně platí, že poté, co jsem si nějakou dobu pohrával s fragmenty libffi závislými na platformě, získal jsem nějaký kompilovatelný kód a spustil jsem ho při prvním testu, na který jsem narazil. K mému překvapení byl test úspěšný. Ohromený svojí genialitou – žádná sranda, fungovalo to od prvního spuštění – jsem se, stále nevěříc svým očím, šel znovu podívat na výsledný kód, abych zhodnotil, kam dál kopat. Tady jsem se zbláznil podruhé – jediné, co dělala moje funkce, bylo ffi_call - toto ohlásilo úspěšný hovor. Samotné volání nebylo. Odeslal jsem tedy svůj první požadavek na vytažení, který opravil chybu v testu, která je každému studentovi olympiády jasná - skutečná čísla by se neměla porovnávat jako a == b a dokonce jak a - b < EPS - také si musíte pamatovat modul, jinak se 0 ukáže jako velmi rovná 1/3... Obecně jsem přišel s určitým portem libffi, který projde nejjednoduššími testy a se kterým je glib sestaveno - rozhodl jsem se, že to bude nutné, doplním později. Při pohledu do budoucna řeknu, že jak se ukázalo, kompilátor do konečného kódu ani nezahrnul funkci libffi.

Ale jak jsem již řekl, existují určitá omezení a mezi bezplatné používání různého nedefinovaného chování se schovala nepříjemnější vlastnost – JavaScript podle návrhu nepodporuje multithreading se sdílenou pamětí. V zásadě to lze obvykle dokonce nazvat dobrým nápadem, ale ne pro portování kódu, jehož architektura je svázána s vlákny C. Obecně řečeno, Firefox experimentuje s podporou sdílených pracovníků a Emscripten pro ně má implementaci pthread, ale nechtěl jsem se na to spoléhat. Musel jsem z kódu Qemu pomalu vykořenit multithreading – tedy zjistit, kde vlákna běží, přesunout tělo cyklu běžícího v tomto vlákně do samostatné funkce a volat takové funkce jednu po druhé z hlavní smyčky.

Druhý pokus

V určitém okamžiku se ukázalo, že problém stále existuje a že nahodilé strkání berlí kolem kódu k ničemu dobrému nevede. Závěr: musíme nějak systematizovat proces přidávání berliček. Proto byla převzata verze 2.4.1, která byla v té době čerstvá (ne 2.5.0, protože kdo ví, v nové verzi budou chyby, které ještě nebyly zachyceny, a já mám dost vlastních chyb ), a první věcí bylo ji bezpečně přepsat thread-posix.c. Tedy jako bezpečné: pokud se někdo pokusil provést operaci vedoucí k zablokování, funkce byla okamžitě volána abort() - samozřejmě to nevyřešilo všechny problémy najednou, ale aspoň to bylo tak nějak příjemnější, než tiše přijímat nekonzistentní data.

Obecně jsou možnosti Emscripten velmi užitečné při portování kódu do JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - zachycují některé typy nedefinovaného chování, jako jsou volání na nezarovnanou adresu (což není vůbec v souladu s kódem pro typovaná pole, jako je HEAP32[addr >> 2] = 1) nebo volání funkce s nesprávným počtem argumentů.

Mimochodem, chyby zarovnání jsou samostatný problém. Jak jsem již řekl, Qemu má „degenerovaný“ interpretační backend pro generování kódu TCI (malý interpret kódu) a k sestavení a spuštění Qemu na nové architektuře, pokud budete mít štěstí, stačí kompilátor C. Klíčová slova "pokud budeš mít štěstí". Měl jsem smůlu a ukázalo se, že TCI používá při analýze svého bajtkódu nezarovnaný přístup. To znamená, že na všemožných architekturách ARM a dalších architekturách s nutně úrovňovým přístupem se Qemu kompiluje, protože mají normální backend TCG, který generuje nativní kód, ale zda na nich bude TCI fungovat, je jiná otázka. Jak se však ukázalo, dokumentace TCI jasně naznačovala něco podobného. V důsledku toho byla do kódu přidána volání funkcí pro nezarovnané čtení, která byla objevena v jiné části Qemu.

Ničení haldy

V důsledku toho byl opraven nezarovnaný přístup k TCI, byla vytvořena hlavní smyčka, která zase volala procesor, RCU a některé další drobnosti. A tak spouštím Qemu s opcí -d exec,in_asm,out_asm, což znamená, že musíte říci, které bloky kódu se provádějí, a také v době vysílání napsat, jaký byl kód hosta, jaký kód hostitele se stal (v tomto případě bajtkód). Spustí se, provede několik bloků překladu, zapíše ladicí zprávu, kterou jsem nechal, že RCU se nyní spustí a... havaruje abort() uvnitř funkce free(). Pohrávám si s funkcí free() Podařilo se nám zjistit, že v hlavičce bloku haldy, který leží v osmi bytech předcházejících přidělené paměti, byl místo velikosti bloku nebo něčeho podobného smetí.

Zničení haldy - jak roztomilé... V takovém případě existuje užitečný lék - z (pokud možno) stejných zdrojů sestavit nativní binárku a spustit ji pod Valgrind. Po nějaké době byl binární soubor připraven. Spouštím to se stejnými možnostmi - padá i během inicializace, než skutečně dosáhne spuštění. Je to samozřejmě nepříjemné - zdroje zřejmě nebyly úplně stejné, což není překvapivé, protože konfigurace prozkoumala trochu jiné možnosti, ale mám Valgrind - nejprve opravím tuto chybu a pak, pokud budu mít štěstí , objeví se původní. Spouštím to samé pod Valgrindem... Y-y-y, y-y-y, uh-uh, začalo to, normálně prošlo inicializací a přešlo přes původní chybu bez jediného varování o nesprávném přístupu do paměti, nemluvě o pádech. Život mě na to, jak se říká, nepřipravil - padající program přestane padat při spuštění pod Walgrindem. Co to bylo, je záhadou. Moje hypotéza je, že jakmile v blízkosti aktuální instrukce po pádu během inicializace, gdb ukázal práci memset-a s platným ukazatelem pomocí buď mmxnebo xmm registrů, pak možná šlo o nějaký druh chyby zarovnání, i když je stále těžké uvěřit.

Dobře, nezdá se, že by tady Valgrind pomohl. A tady začala ta nejhnusnější věc – zdá se, že vše dokonce začíná, ale z naprosto neznámých důvodů havaruje kvůli události, která se mohla stát před miliony pokynů. Dlouho ani nebylo jasné, jak přistupovat. Nakonec jsem si stejně musel sednout a ladit. Tisk toho, čím byla hlavička přepsána, ukázal, že to nevypadá jako číslo, ale spíše jako nějaká binární data. A ejhle, tento binární řetězec byl nalezen v souboru BIOS - tedy nyní bylo možné s přiměřenou jistotou říci, že šlo o přetečení bufferu, a dokonce je jasné, že byl zapsán do tohoto bufferu. No a pak něco takového - v Emscriptenu naštěstí neexistuje randomizace adresního prostoru, ani v něm nejsou žádné díry, takže můžete psát někam doprostřed kódu na výstup dat ukazatelem od posledního spuštění, podívejte se na data, podívejte se na ukazatel, a pokud se nezměnil, nechte se zamyslet. Je pravda, že propojení po jakékoli změně trvá několik minut, ale co můžete dělat? V důsledku toho byl nalezen konkrétní řádek, který zkopíroval BIOS z dočasné vyrovnávací paměti do paměti hosta – a ve vyrovnávací paměti skutečně nebylo dost místa. Nalezení zdroje té podivné adresy vyrovnávací paměti vedlo k funkci qemu_anon_ram_alloc v souboru oslib-posix.c - logika tam byla taková: někdy může být užitečné zarovnat adresu na obrovskou stránku o velikosti 2 MB, na to se zeptáme mmap nejprve trochu víc a pak přebytek s pomocí vrátíme munmap. A pokud takové zarovnání není vyžadováno, pak místo 2 MB uvedeme výsledek getpagesize() - mmap stále bude vydávat zarovnanou adresu... Takže v Emscriptenu mmap jen volá malloc, ale samozřejmě se nezarovná na stránce. Obecně platí, že chyba, která mě frustrovala několik měsíců, byla opravena změnou dvouh linky.

Vlastnosti funkcí volání

A teď procesor něco počítá, Qemu nespadne, ale obrazovka se nezapne a procesor rychle přejde do smyček, soudě podle výstupu -d exec,in_asm,out_asm. Objevila se hypotéza: přerušení časovače (nebo obecně všechna přerušení) nepřicházejí. A skutečně, pokud odšroubujete přerušení z nativní sestavy, která z nějakého důvodu fungovala, získáte podobný obrázek. To však vůbec nebyla odpověď: srovnání vydaných stop s výše uvedenou možností ukázalo, že trajektorie provádění se rozcházely velmi brzy. Zde je třeba říci, že srovnání toho, co bylo zaznamenáno pomocí launcheru emrun výstup ladění s výstupem nativní sestavy není zcela mechanický proces. Nevím přesně, jak se program běžící v prohlížeči připojuje emrun, ale ukázalo se, že některé řádky na výstupu jsou přeskupené, takže rozdíl v rozdílu ještě není důvodem k domněnce, že se trajektorie rozcházely. Obecně se ukázalo, že podle pokynů ljmpl dochází k přechodu na různé adresy a generovaný bytekód je zásadně odlišný: jeden obsahuje instrukci pro volání pomocné funkce, druhý nikoli. Po vygooglování pokynů a prostudování kódu, který tyto pokyny překládá, bylo jasné, že za prvé, bezprostředně před ním v registru cr0 byl pořízen záznam - i pomocí pomocníka - který přepnul procesor do chráněného režimu a za druhé, že verze js nikdy nepřešla do chráněného režimu. Faktem ale je, že další vlastností Emscriptenu je jeho neochota tolerovat kód, jako je implementace instrukcí call v TCI, jehož výsledkem je typ jakéhokoli ukazatele funkce long long f(int arg0, .. int arg9) - funkce musí být volány se správným počtem argumentů. Pokud je toto pravidlo porušeno, v závislosti na nastavení ladění program buď spadne (což je dobře), nebo vůbec zavolá špatnou funkci (což bude smutné ladit). Existuje také třetí možnost - povolit generování wrapperů, které přidávají / odebírají argumenty, ale celkově tyto wrappery zabírají hodně místa, přestože mi ve skutečnosti stačí něco málo přes stovku wrapperů. To samo o sobě je velmi smutné, ale ukázalo se, že je zde závažnější problém: ve vygenerovaném kódu funkcí wrapperu byly argumenty převedeny a převedeny, ale někdy nebyla funkce s vygenerovanými argumenty volána - no, stejně jako v moje implementace libffi. To znamená, že někteří pomocníci prostě nebyli popraveni.

Naštěstí má Qemu strojově čitelné seznamy pomocníků ve formě hlavičkového souboru jako

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

Používají se docela vtipně: zaprvé se makra předefinují tím nejbizarnějším způsobem DEF_HELPER_na poté se zapne helper.h. Do té míry, že je makro rozšířeno do inicializátoru struktury a čárky, a pak je definováno pole a místo prvků - #include <helper.h> Díky tomu jsem měl konečně možnost si knihovnu vyzkoušet v práci pyparsinga byl napsán skript, který generuje přesně ty obaly pro přesně ty funkce, pro které jsou potřeba.

A tak se po tom zdálo, že procesor funguje. Zdá se, že obrazovka nebyla nikdy inicializována, ačkoli memtest86+ byl schopen běžet v nativním sestavení. Zde je nutné upřesnit, že I/O kód bloku Qemu je zapsán v korutínách. Emscripten má svou vlastní velmi složitou implementaci, ale stále je třeba ji podporovat v kódu Qemu a nyní můžete ladit procesor: Qemu podporuje možnosti -kernel, -initrd, -append, se kterým můžete nabootovat Linux nebo například memtest86+, a to zcela bez použití blokových zařízení. Ale tady je problém: v nativním sestavení bylo možné vidět výstup linuxového jádra do konzole s možností -nographica žádný výstup z prohlížeče do terminálu, odkud byl spuštěn emrun, nepřišel. To znamená, že není jasné: nefunguje procesor nebo nefunguje grafický výstup. A pak mě napadlo trochu počkat. Ukázalo se, že „procesor nespí, ale jednoduše pomalu bliká“ a asi po pěti minutách jádro hodilo na konzolu spoustu zpráv a pokračovalo v zavěšení. Bylo jasné, že procesor obecně funguje a musíme se ponořit do kódu pro práci s SDL2. Bohužel nevím, jak tuto knihovnu používat, takže jsem na některých místech musel jednat náhodně. V určitém okamžiku na obrazovce zablikala čára rovnoběžka0 na modrém pozadí, což naznačovalo nějaké myšlenky. Nakonec se ukázalo, že problém je v tom, že Qemu otevírá několik virtuálních oken v jednom fyzickém okně, mezi kterými lze přepínat pomocí Ctrl-Alt-n: funguje to v nativním sestavení, ale ne v Emscriptenu. Po zbavení se zbytečných oken pomocí voleb -monitor none -parallel none -serial none a pokyny k násilnému překreslení celé obrazovky na každý snímek, vše najednou fungovalo.

Corutines

Emulace v prohlížeči tedy funguje, ale nemůžete v ní spustit nic zajímavého na jedné disketě, protože neexistuje žádný blokový I/O - musíte implementovat podporu pro korutiny. Qemu již má několik koroutinových backendů, ale vzhledem k povaze JavaScriptu a generátoru kódu Emscripten nemůžete jen tak začít žonglovat se zásobníky. Zdálo by se, že „všechno je pryč, omítka se odstraňuje“, ale vývojáři Emscriptenu se již o všechno postarali. To je implementováno docela vtipně: říkejme volání funkce jako toto podezřelé emscripten_sleep a několik dalších pomocí mechanismu Asyncify, stejně jako volání ukazatelů a volání jakékoli funkce, kde může nastat jeden z předchozích dvou případů dále v zásobníku. A nyní před každým podezřelým voláním vybereme asynchronní kontext a hned po volání zkontrolujeme, zda došlo k asynchronnímu volání, a pokud ano, uložíme všechny lokální proměnné do tohoto asynchronního kontextu, uvedeme, která funkce přenést kontrolu na dobu, kdy potřebujeme pokračovat v provádění a ukončit aktuální funkci. Zde je prostor pro studium účinku plýtvání — pro potřeby pokračování ve vykonávání kódu po návratu z asynchronního volání kompilátor generuje „stub“ funkce začínající po podezřelém volání — takto: pokud je n podezřelých volání, tak se funkce rozšíří někam n/2 times — to je stále, pokud ne. Mějte na paměti, že po každém potenciálně asynchronním volání musíte do původní funkce přidat uložení některých lokálních proměnných. Následně jsem dokonce musel napsat jednoduchý skript v Pythonu, který na základě dané sady zvláště nadužívaných funkcí, které prý „neumožňují průchod asynchronii“ (tedy propagace zásobníku a vše, co jsem právě popsal, neumožňují práce v nich), označuje volání přes ukazatele, ve kterých by měly být funkce kompilátorem ignorovány, aby tyto funkce nebyly považovány za asynchronní. A pak jsou soubory JS pod 60 MB zjevně příliš mnoho – řekněme alespoň 30. I když, jednou jsem nastavoval skript sestavení a omylem jsem vyhodil možnosti linkeru, mezi které -O3. Spustím vygenerovaný kód a Chromium zabere paměť a spadne. Náhodou jsem se pak podíval na to, co se snažil stáhnout... No, co můžu říct, taky bych zamrzl, kdybych byl požádán, abych si promyšleně prostudoval a optimalizoval 500+ MB Javascript.

Bohužel kontroly v kódu knihovny podpory Asyncify nebyly úplně přátelské longjmp-s, které se používají v kódu virtuálního procesoru, ale po malé opravě, která tyto kontroly zakáže a násilně obnoví kontexty, jako by bylo vše v pořádku, kód fungoval. A pak začala podivná věc: někdy se spustily kontroly synchronizačního kódu - ty samé, které zhroutí kód, pokud by měl být podle prováděcí logiky zablokován - někdo se pokusil zachytit již zachycený mutex. Naštěstí se ukázalo, že to nebyl logický problém v serializovaném kódu - prostě jsem používal standardní funkci hlavní smyčky, kterou poskytuje Emscripten, ale někdy asynchronní volání úplně rozbalilo zásobník a v tu chvíli selhalo setTimeout z hlavní smyčky - kód tedy vstoupil do iterace hlavní smyčky, aniž by opustil předchozí iteraci. Přepsáno na nekonečnou smyčku a emscripten_sleepa problémy s mutexy přestaly. Kód se stal ještě logičtějším - koneckonců, ve skutečnosti nemám žádný kód, který připravuje další snímek animace - procesor jen něco vypočítá a obrazovka se pravidelně aktualizuje. Tím však problémy neskončily: někdy se provádění Qemu jednoduše ukončilo tiše bez jakýchkoli výjimek nebo chyb. V tu chvíli jsem to vzdal, ale při pohledu dopředu řeknu, že problém byl v tomto: kód koroutinu ve skutečnosti nepoužívá setTimeout (nebo alespoň ne tak často, jak si možná myslíte): funkce emscripten_yield jednoduše nastaví příznak asynchronního volání. Celá podstata je v tom emscripten_coroutine_next není asynchronní funkce: interně kontroluje příznak, resetuje jej a přenáší řízení tam, kde je potřeba. To znamená, že propagace zásobníku končí. Problém byl v tom, že kvůli use-after-free, které se objevilo, když byl fond coroutine deaktivován kvůli tomu, že jsem nezkopíroval důležitý řádek kódu ze stávajícího backendu coroutine, funkce qemu_in_coroutine vrátil true, i když ve skutečnosti měl vrátit hodnotu false. To vedlo k hovoru emscripten_yield, nad kterým na stohu nikdo nebyl emscripten_coroutine_next, stoh se rozvinul až úplně nahoru, ale ne setTimeout, jak jsem již řekl, nebyl vystaven.

Generování kódu JavaScript

A tady je ve skutečnosti slíbené „otočení mletého masa zpět“. Spíš ne. Samozřejmě, pokud v prohlížeči spustíme Qemu a v něm Node.js, pak přirozeně po vygenerování kódu v Qemu dostaneme úplně špatný JavaScript. Ale přece jen nějaká obrácená transformace.

Nejprve něco o tom, jak Qemu funguje. Okamžitě mi prosím odpusťte: nejsem profesionální vývojář Qemu a mé závěry mohou být na některých místech chybné. Jak se říká, „názor studenta se nemusí shodovat s názorem učitele, Peanovou axiomatikou a zdravým rozumem“. Qemu má určitý počet podporovaných hostujících architektur a pro každou existuje adresář jako target-i386. Při sestavování můžete určit podporu pro několik hostujících architektur, ale výsledkem bude pouze několik binárních souborů. Kód pro podporu hostované architektury zase generuje některé interní operace Qemu, které TCG (Tiny Code Generator) již převádí na strojový kód pro architekturu hostitele. Jak je uvedeno v souboru readme umístěném v adresáři tcg, toto bylo původně součástí běžného kompilátoru C, který byl později upraven pro JIT. Proto například cílová architektura ve smyslu tohoto dokumentu již není hostující architektura, ale hostitelská architektura. V určitém okamžiku se objevila další komponenta - Tiny Code Interpreter (TCI), která by měla provádět kód (téměř stejné interní operace) při absenci generátoru kódu pro konkrétní hostitelskou architekturu. Ve skutečnosti, jak uvádí jeho dokumentace, tento interpret nemusí vždy fungovat tak dobře jako generátor kódu JIT, a to nejen kvantitativně z hlediska rychlosti, ale také kvalitativně. I když si nejsem jistý, že jeho popis je zcela relevantní.

Nejprve jsem se pokoušel udělat plnohodnotný TCG backend, ale rychle jsem se zmátl ve zdrojovém kódu a ne zcela jasném popisu instrukcí bytecode, takže jsem se rozhodl zabalit TCI interpreter. To přineslo několik výhod:

  • při implementaci generátoru kódu byste se mohli podívat nikoli na popis instrukcí, ale na kód interpretu
  • můžete generovat funkce ne pro každý nalezený překladový blok, ale například až po stém provedení
  • pokud se vygenerovaný kód změní (a to se zdá být možné, soudě podle funkcí s názvy obsahujícími slovo patch), budu muset vygenerovaný JS kód zneplatnit, ale alespoň ho budu mít z čeho regenerovat

Pokud jde o třetí bod, nejsem si jistý, zda je oprava možná po prvním spuštění kódu, ale první dva body stačí.

Zpočátku byl kód generován ve formě velkého přepínače na adrese původní instrukce bajtového kódu, ale pak jsem si vzpomněl na článek o Emscripten, optimalizaci generovaného JS a reloopingu, rozhodl jsem se vygenerovat více lidského kódu, zvláště když empiricky ukázalo se, že jediným vstupním bodem do překladového bloku je jeho Start. Sotva řečeno, než uděláno, po chvíli jsme měli generátor kódu, který generoval kód s ifs (i když bez smyček). Ale smůla, havaroval a vydal zprávu, že pokyny mají nějakou nesprávnou délku. Navíc poslední instrukce na této úrovni rekurze byla brcond. Dobře, přidám identickou kontrolu ke generování této instrukce před a po rekurzivním volání a... ani jedna z nich nebyla provedena, ale po přepnutí assesstrace stále selhávaly. Nakonec jsem si po prostudování vygenerovaného kódu uvědomil, že po přepnutí se ukazatel na aktuální instrukci znovu načte ze zásobníku a pravděpodobně je přepsán vygenerovaným JavaScript kódem. A tak to dopadlo. Zvýšení vyrovnávací paměti z jednoho megabajtu na deset k ničemu nevedlo a bylo jasné, že generátor kódu běží v kruzích. Museli jsme zkontrolovat, že jsme nepřekročili hranice aktuálního TBC, a pokud ano, vydat adresu dalšího TBC se znaménkem mínus, abychom mohli pokračovat v exekuci. Kromě toho to řeší problém „které generované funkce by měly být zrušeny, pokud se tento kus bajtového kódu změnil? — pouze funkce, která odpovídá tomuto bloku překladu, musí být zneplatněna. Mimochodem, ačkoliv jsem vše odladil v Chromiu (jelikož používám Firefox a je pro mě jednodušší používat pro experimenty samostatný prohlížeč), Firefox mi pomohl opravit nekompatibility se standardem asm.js, po kterém začal kód pracovat rychleji v Chrom.

Pří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ávěr

Takže práce stále není dokončena, ale už mě nebaví tajně dotahovat tuto dlouhodobou stavbu k dokonalosti. Proto jsem se rozhodl zveřejnit to, co zatím mám. Kód je místy trochu děsivý, protože se jedná o experiment a není předem jasné, co je třeba udělat. Pravděpodobně pak stojí za to vydávat normální atomové commity nad nějakou modernější verzí Qemu. Mezitím je v Gitě vlákno ve formátu blogu: pro každou „úroveň“, která byla alespoň nějak prošlá, byl přidán podrobný komentář v ruštině. Tento článek je ve skutečnosti do značné míry převyprávěním závěru git log.

Můžete to všechno vyzkoušet zde (pozor na provoz).

Co již funguje:

  • Virtuální procesor x86 běží
  • Existuje funkční prototyp generátoru JIT kódu od strojového kódu po JavaScript
  • Existuje šablona pro sestavení dalších 32bitových hostujících architektur: právě teď můžete obdivovat Linux pro zmrazení architektury MIPS v prohlížeči ve fázi načítání

Co jiného můžeš dělat

  • Zrychlete emulaci. Dokonce i v režimu JIT se zdá, že běží pomaleji než Virtual x86 (ale potenciálně existuje celé Qemu se spoustou emulovaného hardwaru a architektur)
  • Vytvořit normální rozhraní - upřímně, nejsem dobrý webový vývojář, takže jsem zatím předělal standardní prostředí Emscripten, jak nejlépe umím
  • Zkuste spustit složitější funkce Qemu – síťování, migrace VM atd.
  • UPD: budete muset odeslat svých několik vývojů a hlášení o chybách do Emscriptenu upstream, jako to dělali předchozí portoři Qemu a dalších projektů. Děkuji jim za to, že mohli implicitně využít jejich příspěvek pro Emscripten jako součást mého úkolu.

Zdroj: www.habr.com

Přidat komentář