QEMU.js: teraz vážne a s WASM

Kedysi dávno som sa rozhodol pre zábavu dokázať reverzibilitu procesu a naučte sa generovať JavaScript (presnejšie Asm.js) zo strojového kódu. Pre experiment bol vybraný QEMU a o niečo neskôr bol napísaný článok o Habrovi. V komentároch mi bolo odporučené, aby som prerobil projekt vo WebAssembly a dokonca som skončil skoro dokončené Projekt som akosi nechcel... Práca prebiehala, ale veľmi pomaly a teraz, nedávno, sa v tom článku objavil komentár na tému "Tak ako to celé skončilo?" Ako odpoveď na moju podrobnú odpoveď som počul: „Toto znie ako článok.“ No ak môžete, bude aj článok. Možno sa to niekomu bude hodiť. Z nej sa čitateľ dozvie niektoré fakty o dizajne backendov na generovanie kódu QEMU, ako aj o tom, ako napísať Just-in-Time kompilátor pre webovú aplikáciu.

úlohy

Keďže som sa už naučil, ako „nejako“ preniesť QEMU do JavaScriptu, tentoraz bolo rozhodnuté urobiť to múdro a neopakovať staré chyby.

Chyba číslo jedna: vetva z uvoľnenia bodu

Mojou prvou chybou bolo forkovať moju verziu z upstream verzie 2.4.1. Potom sa mi to zdalo ako dobrý nápad: ak existuje bodové uvoľnenie, potom je pravdepodobne stabilnejšie ako jednoduchý 2.4 a ešte viac vetva master. A keďže som plánoval pridať značné množstvo vlastných chýb, vôbec som nepotreboval nikoho iného. Asi to tak dopadlo. Ale tu je vec: QEMU nestojí na mieste a v určitom bode dokonca oznámili optimalizáciu generovaného kódu o 10 percent. „Áno, teraz zamrznem,“ pomyslel som si a zlomil som sa. Tu musíme urobiť odbočku: kvôli jednovláknovej povahe QEMU.js a skutočnosti, že pôvodný QEMU neznamená absenciu multivlákna (t. j. možnosť súčasne prevádzkovať niekoľko nesúvisiacich ciest kódu a nie len „použiť všetky jadrá“) je pre to kritické, hlavné funkcie vlákien som musel „vypnúť“, aby som mohol volať zvonku. To spôsobilo niekoľko prirodzených problémov počas fúzie. Avšak skutočnosť, že niektoré zmeny od pobočky master, s ktorým som sa pokúšal zlúčiť svoj kód, boli tiež vybrané v point release (a teda v mojej pobočke) by tiež pravdepodobne nepriniesli väčšie pohodlie.

Vo všeobecnosti som sa rozhodol, že stále má zmysel vyhodiť prototyp, rozobrať ho na súčiastky a postaviť novú verziu od začiatku založenú na niečom novšom a teraz od master.

Chyba číslo dva: metodika TLP

V podstate to nie je chyba, vo všeobecnosti je to len vlastnosť vytvárania projektu v podmienkach úplného nepochopenia „kam a ako sa pohybovať?“ a vo všeobecnosti „dostaneme sa tam?“ V týchto podmienkach nemotorné programovanie bola oprávnená možnosť, ale, prirodzene, nechcel som to zbytočne opakovať. Tentoraz som to chcel urobiť múdro: atómové príkazy, vedomé zmeny kódu (a nie „spájanie náhodných znakov, kým sa to neskompiluje (s varovaním)“, ako raz o niekom povedal Linus Torvalds, podľa Wikicitátu) atď.

Chyba číslo tri: dostať sa do vody bez toho, aby ste poznali brod

Stále som sa toho úplne nezbavil, ale teraz som sa rozhodol, že vôbec nepôjdem cestou najmenšieho odporu a urobím to „ako dospelý“, teda napíšem svoj TCG backend od nuly, aby som musím neskôr povedať: „Áno, je to, samozrejme, pomaly, ale nemôžem ovládať všetko – tak sa píše TCI...“ Navyše sa to spočiatku zdalo ako samozrejmé riešenie, keďže Generujem binárny kód. Ako sa hovorí: „Gent sa zhromaždilу, ale nie ten“: kód je, samozrejme, binárny, ale nemožno doňho jednoducho preniesť ovládanie – treba ho vyslovene natlačiť do prehliadača na kompiláciu, výsledkom čoho je určitý objekt zo sveta JS, ktorý ešte potrebuje byť niekde zachránený. Avšak na normálnych architektúrach RISC, pokiaľ som pochopil, je typickou situáciou potreba explicitne resetovať vyrovnávaciu pamäť inštrukcií pre regenerovaný kód - ak to nie je to, čo potrebujeme, potom je to v každom prípade blízko. Navyše z môjho posledného pokusu som sa dozvedel, že ovládanie sa nezdá byť prenesené do stredu prekladového bloku, takže vlastne nepotrebujeme bajtkód interpretovaný z akéhokoľvek offsetu a môžeme ho jednoducho vygenerovať z funkcie na TB .

Prišli a kopali

Hoci som kód začal prepisovať už v júli, nepozorovane sa vkradlo magické nakopnutie: zvyčajne listy z GitHubu prichádzajú ako upozornenia o odpovediach na problémy a žiadosti o stiahnutie, ale tu, zrazu spomenúť vo vlákne Binaryen ako backend qemu v kontexte: "Urobil niečo také, možno niečo povie." Hovorili sme o použití súvisiacej knižnice Emscripten Binaryen na vytvorenie WASM JIT. No povedal som, že tam máš licenciu Apache 2.0 a QEMU ako celok je distribuovaný pod GPLv2 a nie sú veľmi kompatibilné. Zrazu sa ukázalo, že licencia môže byť nejako to opraviť (Neviem: možno to zmeniť, možno duálne licencovanie, možno niečo iné...). To ma, samozrejme, potešilo, pretože v tom čase som si to už poriadne prezrel binárny formát WebAssembly, a bolo mi nejako smutno a nechápavo. Existovala aj knižnica, ktorá by zhltla základné bloky s grafom prechodu, vytvorila bytekód a v prípade potreby ho aj spustila v samotnom interpretači.

Potom toho bolo viac list na zoznam adries QEMU, ale toto je skôr o otázke: „Kto to vlastne potrebuje?“ A to je zrazu, ukázalo sa, že je to potrebné. Minimálne môžete zoškrabať takéto možnosti použitia, ak to funguje viac či menej rýchlo:

  • spustenie niečoho vzdelávacieho bez akejkoľvek inštalácie
  • virtualizácia na iOS, kde je podľa povestí jedinou aplikáciou, ktorá má právo na generovanie kódu za chodu, JS engine (je to pravda?)
  • ukážka mini-OS - jednodisketový, vstavaný, všetky druhy firmvéru atď...

Funkcie prehliadača Runtime

Ako som už povedal, QEMU je viazané na multithreading, ale prehliadač ho nemá. Teda nie... Najprv to vôbec neexistovalo, potom sa objavili WebWorkers - pokiaľ som pochopil, ide o multithreading založený na odovzdávaní správ bez zdieľaných premenných. Prirodzene to spôsobuje značné problémy pri portovaní existujúceho kódu založeného na modeli zdieľanej pamäte. Potom sa pod tlakom verejnosti realizovalo pod názvom SharedArrayBuffers. Postupne sa zavádzalo, oslavovali jeho spustenie v rôznych prehliadačoch, potom oslavovali Nový rok a potom Meltdown... Potom prišli na to, že meranie času je hrubé alebo hrubé, ale pomocou zdieľanej pamäte a závit inkrementácia počítadla, je to všetko rovnaké vyjde to celkom presne. Takže sme zakázali multithreading so zdieľanou pamäťou. Zdá sa, že to neskôr znova zapli, ale ako bolo zrejmé z prvého experimentu, existuje život bez toho, a ak áno, pokúsime sa to urobiť bez spoliehania sa na multithreading.

Druhou vlastnosťou je nemožnosť nízkoúrovňových manipulácií so zásobníkom: nemôžete jednoducho vziať, uložiť aktuálny kontext a prepnúť na nový s novým zásobníkom. Zásobník hovorov spravuje virtuálny stroj JS. Zdalo by sa, v čom je problém, keďže sme sa stále rozhodli riadiť bývalé toky úplne manuálne? Faktom je, že blokové I/O v QEMU sa implementujú prostredníctvom korutín, a práve tu by sa hodili manipulácie so zásobníkom na nízkej úrovni. Našťastie Emscipten už obsahuje mechanizmus pre asynchrónne operácie, dokonca dva: Asynchronizovať и Emterpreter. Prvý funguje prostredníctvom výrazného nadúvania vo vygenerovanom kóde JavaScript a už nie je podporovaný. Druhý je súčasný „správny spôsob“ a funguje prostredníctvom generovania bajtkódu pre natívny interpret. Funguje to, samozrejme, pomaly, ale nenafukuje kód. Pravda, podpora coroutines pre tento mechanizmus musela byť prispievaná nezávisle (pre Asyncify už boli coroutines napísané a pre Emterpreter existovala implementácia približne rovnakého API, len ich bolo potrebné prepojiť).

Momentálne sa mi ešte nepodarilo rozdeliť kód na jeden skompilovaný vo WASM a interpretovaný pomocou Emterpretera, takže blokové zariadenia zatiaľ nefungujú (viď v ďalšej sérii, ako sa hovorí...). To znamená, že nakoniec by ste mali dostať niečo ako túto zábavnú vrstvenú vec:

  • interpretovaný blok I/O. Naozaj ste očakávali emulované NVMe s natívnym výkonom? 🙂
  • staticky skompilovaný hlavný kód QEMU (prekladač, iné emulované zariadenia atď.)
  • dynamicky skompilovaný kód hosťa do WASM

Vlastnosti zdrojov QEMU

Ako ste už pravdepodobne uhádli, kód na emuláciu hosťujúcej architektúry a kód na generovanie inštrukcií hostiteľského počítača sú v QEMU oddelené. V skutočnosti je to ešte trochu zložitejšie:

  • existujú hosťujúce architektúry
  • je urýchľovače, konkrétne KVM pre hardvérovú virtualizáciu v systéme Linux (pre hosťujúce a hostiteľské systémy navzájom kompatibilné), TCG pre generovanie kódu JIT kdekoľvek. Počnúc QEMU 2.9 sa objavila podpora hardvérového virtualizačného štandardu HAXM v systéme Windows (podrobnosti)
  • ak sa používa TCG a nie hardvérová virtualizácia, potom má samostatnú podporu generovania kódu pre každú hostiteľskú architektúru, ako aj pre univerzálny interpret
  • ... a okolo toho všetkého - emulované periférie, používateľské rozhranie, migrácia, prehrávanie záznamu atď.

Mimochodom, vedeli ste: QEMU dokáže emulovať nielen celý počítač, ale aj procesor pre samostatný užívateľský proces v hostiteľskom jadre, čo využíva napríklad AFL fuzzer na binárnu inštrumentáciu. Možno by niekto chcel preniesť tento spôsob fungovania QEMU na JS? 😉

Ako väčšina dlhoročného slobodného softvéru, QEMU je vytvorený prostredníctvom hovoru configure и make. Povedzme, že sa rozhodnete niečo pridať: backend TCG, implementácia vlákna, niečo iné. Neponáhľajte sa, aby ste boli šťastní/zhrození (podčiarknite, ako je to vhodné) pri vyhliadke na komunikáciu s Autoconf – v skutočnosti, configure QEMU je zjavne napísaná sama a nie je vytvorená z ničoho.

WebAssembly

Ako sa teda táto vec nazýva WebAssembly (aka WASM)? Toto je náhrada za Asm.js, ktorá sa už nevydáva za platný kód JavaScript. Naopak, je čisto binárny a optimalizovaný a ani jednoduché zapísanie celého čísla do neho nie je veľmi jednoduché: kvôli kompaktnosti je uložené vo formáte LEB128.

Možno ste už počuli o algoritme reloopingu pre Asm.js – ide o obnovenie inštrukcií riadenia toku na „vysokej úrovni“ (t. j. if-then-else, slučiek atď.), pre ktoré sú motory JS navrhnuté, od nízkoúrovňový LLVM IR, bližšie k strojovému kódu vykonávanému procesorom. Prirodzene, stredné zastúpenie QEMU je bližšie k druhému. Zdalo by sa, že je to tu, bytecode, koniec trápenia... A potom sú tu bloky, ak-tak-tak a slučky!..

A to je ďalší dôvod, prečo je Binaryen užitočný: môže prirodzene akceptovať bloky na vysokej úrovni blízke tomu, čo by bolo uložené vo WASM. Dokáže však vytvoriť kód aj z grafu základných blokov a prechodov medzi nimi. No, už som povedal, že skrýva formát úložiska WebAssembly za pohodlným C/C++ API.

TCG (Tiny Code Generator)

TCG bol pôvodne backend pre kompilátor C. Potom zrejme nemohol obstáť v konkurencii s GCC, ale nakoniec si našiel svoje miesto v QEMU ako mechanizmus generovania kódu pre hostiteľskú platformu. Existuje aj backend TCG, ktorý generuje nejaký abstraktný bajtkód, ktorý interpret okamžite spustí, ale tentoraz som sa rozhodol vyhnúť sa jeho použitiu. No fakt, že v QEMU je už možné cez funkciu umožniť prechod na vygenerovaný TB tcg_qemu_tb_exec, ukázalo sa, že je to pre mňa veľmi užitočné.

Ak chcete pridať nový backend TCG do QEMU, musíte vytvoriť podadresár tcg/<имя архитектуры> (v tomto prípade, tcg/binaryen) a obsahuje dva súbory: tcg-target.h и tcg-target.inc.c и predpisovať je to všetko o configure. Môžete tam umiestniť ďalšie súbory, ale ako môžete uhádnuť z názvov týchto dvoch, oba budú niekde zahrnuté: jeden ako bežný hlavičkový súbor (je súčasťou tcg/tcg.ha ten je už v iných súboroch v adresároch tcg, accel a nielen), druhý - iba ako útržok kódu v tcg/tcg.c, ale má prístup k svojim statickým funkciám.

Rozhodol som sa, že strávim príliš veľa času podrobným skúmaním toho, ako to funguje, jednoducho som skopíroval „kostry“ týchto dvoch súborov z inej implementácie backendu, čo som úprimne uviedol v hlavičke licencie.

súbor tcg-target.h obsahuje hlavne nastavenia vo formulári #define-s:

  • koľko registrov a aká šírka je na cieľovej architektúre (máme koľko chceme, koľko chceme - otázka je skôr o tom, čo vygeneruje do efektívnejšieho kódu prehliadač na „úplne cieľovej“ architektúre ...)
  • zarovnanie inštrukcií hostiteľa: na x86 a dokonca ani v TCI nie sú inštrukcie vôbec zarovnané, ale do vyrovnávacej pamäte kódu vložím nie inštrukcie, ale ukazovatele na štruktúry knižnice Binaryen, takže poviem: 4 bajtov
  • aké voliteľné inštrukcie môže backend vygenerovať – zahrnieme všetko, čo nájdeme v Binaryene, ostatné nech si urýchľovač rozbije na jednoduchšie sám
  • Aká je približná veľkosť vyrovnávacej pamäte TLB požadovaná backendom. Faktom je, že v QEMU je všetko vážne: hoci existujú pomocné funkcie, ktoré vykonávajú načítanie/ukladanie s prihliadnutím na hosťujúcu MMU (kde by sme teraz bez nej boli?), ukladajú svoju prekladovú vyrovnávaciu pamäť vo forme štruktúry, ktorých spracovanie je vhodné vložiť priamo do vysielacích blokov. Otázkou je, aký offset v tejto štruktúre je najefektívnejšie spracovaný malou a rýchlou sekvenciou príkazov?
  • tu môžete vyladiť účel jedného alebo dvoch rezervovaných registrov, povoliť volanie TB prostredníctvom funkcie a voliteľne opísať niekoľko malých inline- funkcie ako flush_icache_range (ale toto nie je náš prípad)

súbor tcg-target.inc.c, samozrejme, má zvyčajne oveľa väčšiu veľkosť a obsahuje niekoľko povinných funkcií:

  • inicializácia, vrátane obmedzení toho, ktoré inštrukcie môžu fungovať na ktorých operandoch. Nehanebne skopírované mnou z iného backendu
  • funkcia, ktorá preberá jednu internú inštrukciu bajtového kódu
  • Môžete sem umiestniť aj pomocné funkcie a môžete použiť aj statické funkcie z tcg/tcg.c

Pre seba som zvolil nasledujúcu stratégiu: v prvých slovách nasledujúceho prekladového bloku som si zapísal štyri ukazovatele: počiatočnú značku (určitú hodnotu v blízkosti 0xFFFFFFFF, ktorý určil aktuálny stav TB), kontext, vygenerovaný modul a magické číslo pre ladenie. Najprv bola značka umiestnená v 0xFFFFFFFF - nKde n - malé kladné číslo a pri každom vykonaní prostredníctvom tlmočníka sa zvýšilo o 1. Keď dosiahol 0xFFFFFFFE, prebehla kompilácia, modul sa uložil do tabuľky funkcií, naimportoval sa do malého „spúšťača“, do ktorého spustenie išlo z tcg_qemu_tb_execa modul bol odstránený z pamäte QEMU.

Aby som parafrázoval klasiku: „Certch, koľko je v tomto zvuku prepletené pre progerovo srdce...“. Pamäť však niekde unikala. Navyše to bola pamäť riadená QEMU! Mal som kód, ktorý pri písaní ďalšej inštrukcie (dobre, teda ukazovateľ) vymazal ten, ktorého odkaz bol na tomto mieste skôr, ale nepomohlo to. V skutočnosti v najjednoduchšom prípade QEMU pri spustení pridelí pamäť a zapíše tam vygenerovaný kód. Po vyčerpaní vyrovnávacej pamäte sa kód vyhodí a na jeho miesto sa začne písať ďalší.

Po preštudovaní kódu som si uvedomil, že trik s magickým číslom mi umožnil nezlyhať pri zničení haldy uvoľnením niečoho zlého na neinicializovanej vyrovnávacej pamäti pri prvom prechode. Ale kto prepíše vyrovnávaciu pamäť, aby neskôr obišiel moju funkciu? Ako radia vývojári Emscriptenu, keď som narazil na problém, preportoval som výsledný kód späť do natívnej aplikácie, nastavil som na ňom Mozilla Record-Replay... Vo všeobecnosti som si nakoniec uvedomil jednoduchú vec: pre každý blok, a struct TranslationBlock s jeho popisom. Hádajte, kde... Presne tak, tesne pred blokom priamo vo vyrovnávacej pamäti. Keď som si to uvedomil, rozhodol som sa prestať používať barle (aspoň niektoré) a jednoducho som vyhodil magické číslo a preniesol som zvyšné slová do struct TranslationBlock, čím sa vytvorí jednotlivo prepojený zoznam, ktorý možno rýchlo prechádzať po vynulovaní vyrovnávacej pamäte prekladu a uvoľniť tak pamäť.

Niektoré barličky zostávajú: napríklad označené ukazovatele vo vyrovnávacej pamäti kódu - niektoré z nich jednoducho sú BinaryenExpressionRef, teda pozerajú sa na výrazy, ktoré treba lineárne vkladať do vygenerovaného základného bloku, časť je podmienkou prechodu medzi BB, časť je kam ísť. No a pre Relooper sú už pripravené bloky, ktoré je potrebné pospájať podľa podmienok. Na ich rozlíšenie sa používa predpoklad, že sú všetky zarovnané aspoň o štyri bajty, takže na označenie môžete pokojne použiť najmenej významné dva bity, len si ho treba v prípade potreby zapamätať. Mimochodom, takéto označenia sa už používajú v QEMU na označenie dôvodu opustenia slučky TCG.

Použitie Binaryen

Moduly vo WebAssembly obsahujú funkcie, z ktorých každá obsahuje telo, čo je výraz. Výrazy sú unárne a binárne operácie, bloky pozostávajúce zo zoznamov iných výrazov, riadiaci tok atď. Ako som už povedal, riadiaci tok je tu organizovaný presne ako vetvy na vysokej úrovni, slučky, volania funkcií atď. Argumenty funkcií sa neprenášajú na zásobník, ale explicitne, rovnako ako v JS. Existujú aj globálne premenné, ale nepoužil som ich, takže vám o nich nepoviem.

Funkcie majú tiež lokálne premenné, číslované od nuly, typu: int32 / int64 / float / double. V tomto prípade je prvých n lokálnych premenných argumenty odovzdané funkcii. Upozorňujeme, že aj keď tu nie je všetko úplne na nízkej úrovni, pokiaľ ide o tok riadenia, celé čísla stále nenesú atribút „signed/unsigned“: ako sa číslo správa, závisí od operačného kódu.

Všeobecne povedané, Binaryen poskytuje jednoduché C-API: vytvoríte modul, v ňom vytvárať výrazy – unárne, binárne, bloky z iných výrazov, riadiť tok atď. Potom vytvoríte funkciu s výrazom ako jej telom. Ak máte ako ja prechodový graf na nízkej úrovni, pomôže vám komponent relooper. Pokiaľ som pochopil, je možné použiť riadenie toku vykonávania na vysokej úrovni v bloku, pokiaľ nepresahuje hranice bloku - to znamená, že je možné urobiť internú rýchlu / pomalú cestu vetvenie cesty vo vnútri vstavaného kódu spracovania vyrovnávacej pamäte TLB, ale aby nezasahovalo do „externého“ riadiaceho toku. Keď uvoľníte relooper, jeho bloky sa uvoľnia; keď uvoľníte modul, zmiznú výrazy, funkcie atď. aréna.

Ak však chcete interpretovať kód za behu bez zbytočného vytvárania a odstraňovania inštancie tlmočníka, môže mať zmysel vložiť túto logiku do súboru C++ a odtiaľ priamo spravovať celé C++ API knižnice, pričom sa obíde ready- vyrobené obaly.

Takže na vygenerovanie kódu, ktorý potrebujete

// настроить глобальные параметры (можно поменять потом)
BinaryenSetAPITracing(0);

BinaryenSetOptimizeLevel(3);
BinaryenSetShrinkLevel(2);

// создать модуль
BinaryenModuleRef MODULE = BinaryenModuleCreate();

// описать типы функций (как создаваемых, так и вызываемых)
helper_type  BinaryenAddFunctionType(MODULE, "helper-func", BinaryenTypeInt32(), int32_helper_args, ARRAY_SIZE(int32_helper_args));
// (int23_helper_args приоб^Wсоздаются отдельно)

// сконструировать супер-мега выражение
// ... ну тут уж вы как-нибудь сами :)

// потом создать функцию
BinaryenAddFunction(MODULE, "tb_fun", tb_func_type, func_locals, FUNC_LOCALS_COUNT, expr);
BinaryenAddFunctionExport(MODULE, "tb_fun", "tb_fun");
...
BinaryenSetMemory(MODULE, (1 << 15) - 1, -1, NULL, NULL, NULL, NULL, NULL, 0, 0);
BinaryenAddMemoryImport(MODULE, NULL, "env", "memory", 0);
BinaryenAddTableImport(MODULE, NULL, "env", "tb_funcs");

// запросить валидацию и оптимизацию при желании
assert (BinaryenModuleValidate(MODULE));
BinaryenModuleOptimize(MODULE);

... ak som na niečo zabudol, ospravedlňujem sa, toto je len na znázornenie váhy a podrobnosti sú v dokumentácii.

A teraz začína crack-fex-pex, niečo takéto:

static char buf[1 << 20];
BinaryenModuleOptimize(MODULE);
BinaryenSetMemory(MODULE, 0, -1, NULL, NULL, NULL, NULL, NULL, 0, 0);
int sz = BinaryenModuleWrite(MODULE, buf, sizeof(buf));
BinaryenModuleDispose(MODULE);
EM_ASM({
  var module = new WebAssembly.Module(new Uint8Array(wasmMemory.buffer, $0, $1));
  var fptr = $2;
  var instance = new WebAssembly.Instance(module, {
      'env': {
          'memory': wasmMemory,
          // ...
      }
  );
  // и вот уже у вас есть instance!
}, buf, sz);

Aby sa svety QEMU a JS nejako prepojili a zároveň sa rýchlo sprístupnili skompilované funkcie, bolo vytvorené pole (tabuľka funkcií na import do launchera) a tam boli umiestnené vygenerované funkcie. Na rýchly výpočet indexu sa pôvodne použil index nulového bloku prekladu slov, ale potom index vypočítaný pomocou tohto vzorca začal jednoducho zapadať do poľa v struct TranslationBlock.

Mimochodom, demonštrácie (momentálne s nejasnou licenciou) funguje dobre iba vo Firefoxe. Vývojári prehliadača Chrome boli akosi nepripravený na to, že by niekto chcel vytvoriť viac ako tisíc inštancií modulov WebAssembly, tak jednoducho pridelil gigabajt virtuálneho adresného priestoru pre každý...

To je zatiaľ všetko. Možno bude ďalší článok, ak by mal niekto záujem. Totiž zostáva minimálne iba aby blokové zariadenia fungovali. Zmysluplné môže byť aj to, aby kompilácia modulov WebAssembly bola asynchrónna, ako je vo svete JS zvykom, keďže stále existuje interpret, ktorý to všetko dokáže, kým nebude pripravený natívny modul.

Na záver hádanka: skompilovali ste binárny súbor na 32-bitovej architektúre, ale kód sa prostredníctvom pamäťových operácií vyšplhá z Binaryen niekde na zásobník alebo niekde inde v horných 2 GB 32-bitového adresného priestoru. Problém je v tom, že z pohľadu Binaryena ide o prístup k príliš veľkej výslednej adrese. Ako to obísť?

Na spôsob admina

Nakoniec som to netestoval, ale moja prvá myšlienka bola „Čo keby som si nainštaloval 32-bitový Linux? Potom bude horná časť adresného priestoru obsadená jadrom. Jedinou otázkou je, koľko bude obsadených: 1 alebo 2 Gb.

Programátorským spôsobom (možnosť pre odborníkov z praxe)

Nafúkneme bublinu v hornej časti adresného priestoru. Sám nerozumiem, prečo to funguje - tam musí tam byť stoh. Ale „sme praktizujúci: všetko nám funguje, ale nikto nevie prečo...“

// 2gbubble.c
// Usage: LD_PRELOAD=2gbubble.so <program>

#include <sys/mman.h>
#include <assert.h>

void __attribute__((constructor)) constr(void)
{
  assert(MAP_FAILED != mmap(1u >> 31, (1u >> 31) - (1u >> 20), PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0));
}

... je pravda, že nie je kompatibilný s Valgrindom, ale našťastie samotný Valgrind veľmi efektívne vytláča každého odtiaľ :)

Možno niekto lepšie vysvetlí, ako tento môj kód funguje...

Zdroj: hab.com

Pridať komentár