QEMU.js: most komoly és WASM-mel

Egyszer csak szórakozásból döntöttem bizonyítja a folyamat visszafordíthatóságát és megtanulja, hogyan lehet JavaScriptet (pontosabban Asm.js-t) generálni gépi kódból. A kísérlethez a QEMU-t választották, és nem sokkal később cikket írtak a Habr-ról. A megjegyzésekben azt tanácsolták, hogy készítsem újra a projektet WebAssembly-ben, és még magam is kilépek majdnem kész Valahogy nem akartam a projektet... A munka folyt, de nagyon lassan, és most, nemrég jelent meg az a cikk megjegyzés a „Hogy ért véget az egész?” témában. Részletes válaszomra azt hallottam, hogy „Ez úgy hangzik, mint egy cikk”. Nos, ha lehet, lesz cikk. Talán valaki hasznosnak találja. Ebből az olvasó megtudhat néhány tényt a QEMU kódgeneráló háttérrendszerek tervezéséről, valamint arról, hogyan írhat Just-in-Time fordítót egy webalkalmazáshoz.

feladatok

Mivel már megtanultam, hogyan kell „valahogy” portolni a QEMU-t JavaScriptre, ezúttal úgy döntöttem, hogy okosan csinálom, és nem ismételjük meg a régi hibákat.

Egyes számú hiba: elágazás a pont kiadásától

Az első hibám az volt, hogy leválasztottam a verziómat az upstream 2.4.1-es verzióról. Akkor jó ötletnek tűnt: ha létezik pontkiadás, akkor az valószínűleg stabilabb, mint az egyszerű 2.4, és még inkább az ág master. És mivel azt terveztem, hogy elég sok saját hibát adok hozzá, egyáltalán nem volt szükségem senki másra. Valószínűleg így alakult. De itt van a helyzet: a QEMU nem áll meg, sőt valamikor bejelentették, hogy 10 százalékkal optimalizálják a generált kódot.” „Igen, most lefagyok” – gondoltam és összetörtem. Itt egy kitérőt kell tennünk: a QEMU.js egyszálas jellege és az a tény miatt, hogy az eredeti QEMU nem jelenti a többszálúság hiányát (vagyis több, egymással nem összefüggő kódút egyidejű működtetésének képességét, ill. nem csak az „összes kernel használata”) kritikus fontosságú számára, a szálak fő funkcióit „ki kellett kapcsolnom”, hogy kívülről hívhassam. Ez természetes problémákat okozott az egyesülés során. Azonban az a tény, hogy néhány változás az ágból master, amellyel a kódomat próbáltam összevonni, szintén cseresznye lettek a pontkiadásban (és ezért az én ágamban) szintén valószínűleg nem lett volna nagyobb a kényelem.

Általánosságban úgy döntöttem, hogy még mindig van értelme kidobni a prototípust, szétszedni alkatrészekre, és a semmiből megépíteni egy új verziót valami frissebb és most master.

Második hiba: TLP módszertan

Lényegében ez nem hiba, általában csak egy projekt létrehozásának sajátossága, amikor teljesen félreértik mind a „hova és hogyan kell költözni?”, mind általában az „odajutunk?” Ilyen körülmények között ügyetlen programozás indokolt lehetőség volt, de természetesen nem akartam feleslegesen megismételni. Ezúttal bölcsen szerettem volna csinálni: atomi commit, tudatos kódmódosítás (és nem „véletlen karakterek összefűzése, amíg le nem fordítja (figyelmeztetésekkel)”, ahogy Linus Torvalds mondta egyszer valakiről a Wikiidézet szerint), stb.

Harmadik hiba: bejutni a vízbe anélkül, hogy ismerné a gázlót

Ettől még mindig nem szabadultam meg teljesen, de most úgy döntöttem, hogy nem a legkisebb ellenállás útját követem, hanem a „felnőtt módjára” teszem, vagyis a nulláról írom meg a TCG backendemet, hogy ne hogy később azt kell mondanom: „Igen, ez persze lassan, de nem tudok mindent irányítani – így van megírva a TCI…” Ráadásul ez kezdetben kézenfekvő megoldásnak tűnt, hiszen Bináris kódot generálok. Ahogy mondják: „Ghent összegyűltу, de nem az”: a kód természetesen bináris, de a vezérlést nem lehet egyszerűen átvinni rá - kifejezetten be kell tolni a böngészőbe fordításhoz, aminek eredményeként a JS világból egy bizonyos objektum jön létre, amelyet még meg kell menteni valahol. A normál RISC architektúrákon azonban, amennyire én értem, tipikus helyzet az, hogy kifejezetten vissza kell állítani az utasítás-gyorsítótárat a regenerált kódhoz - ha nem erre van szükségünk, akkor mindenesetre közel van. Ráadásul a legutóbbi próbálkozásomból megtudtam, hogy a vezérlés a jelek szerint nem kerül át a fordítási blokk közepére, így nem igazán van szükségünk semmilyen eltolásból értelmezett bájtkódra, hanem egyszerűen előállíthatjuk a TB-n lévő függvényből. .

Jöttek és rúgtak

Bár már júliusban elkezdtem átírni a kódot, észrevétlenül bejött egy varázslat: általában a GitHubról érkeznek levelek értesítésként a problémákra és a lehívási kérésekre adott válaszokról, de itt hirtelen megemlíteni a szálban Binaryen qemu háttérként szövegkörnyezetben: "Valami ilyesmit csinált, talán mond valamit." Az Emscripten kapcsolódó könyvtárának használatáról beszéltünk Binaryen WASM JIT létrehozásához. Nos, mondtam, hogy van egy Apache 2.0 licenced, és a QEMU egésze GPLv2 alatt van terjesztve, és nem nagyon kompatibilisek. Hirtelen kiderült, hogy jogosítvány lehet javítsd meg valahogy (Nem tudom: esetleg változtass, esetleg kettős licencelés, esetleg valami más...). Ez persze boldoggá tett, mert addigra már alaposan megnéztem bináris formátum WebAssembly, és valahogy szomorú és érthetetlen voltam. Volt olyan könyvtár is, amely az alapblokkokat felfalta az átmeneti gráffal, előállította a bájtkódot, sőt, ha kellett, magában az interpreterben is lefuttatta.

Aztán volt több is egy levelet a QEMU levelezőlistán, de ez inkább a „Kinek van szüksége rá?” kérdésről szól. És ez hirtelen, kiderült, hogy szükséges. Legalább a következő felhasználási lehetőségeket lehet összekaparni, ha többé-kevésbé gyorsan működik:

  • valami oktatási program elindítása telepítés nélkül
  • virtualizáció iOS-en, ahol a pletykák szerint az egyetlen olyan alkalmazás, amely jogosult a kódgenerálásra menet közben, egy JS motor (igaz ez?)
  • mini-OS bemutatója - egylemezes, beépített, mindenféle firmware stb.

A böngésző futásidejű funkciói

Ahogy már mondtam, a QEMU többszálú megoldáshoz van kötve, de a böngészőben nincs ilyen. Nos, vagyis nem... Eleinte egyáltalán nem létezett, aztán megjelentek a WebWorkerek - ha jól értem, ez üzenettovábbításon alapuló többszálú megosztott változók nélkül. Ez természetesen jelentős problémákat okoz a meglévő kód osztott memória modellen alapuló portolásakor. Aztán lakossági nyomásra ez is néven valósult meg SharedArrayBuffers. Fokozatosan vezették be, különböző böngészőkben ünnepelték az indulását, majd megünnepelték az újévet, majd a Meltdownt... Ez után arra jutottak, hogy durva vagy durva az időmérés, de a közös memória és egy szál növeli a számlálót, ez mindegy elég pontosan fog sikerülni. Így letiltottuk a többszálú megosztott memóriával való átvitelt. Úgy tűnik, hogy később visszakapcsolták, de mint az első kísérletből kiderült, van élet nélküle is, és ha igen, akkor megpróbáljuk a többszálas kezelés nélkül megcsinálni.

A második jellemző az alacsony szintű manipulációk lehetetlensége a veremmel: nem lehet egyszerűen csak átvenni, elmenteni az aktuális kontextust, és átváltani egy újra egy új veremmel. A hívásveremet a JS virtuális gép kezeli. Úgy tűnik, mi a probléma, mivel mégis úgy döntöttünk, hogy a korábbi folyamokat teljesen manuálisan kezeljük? A helyzet az, hogy a QEMU blokk I/O-ja korutinokon keresztül valósul meg, és itt jól jönnek az alacsony szintű veremmanipulációk. Szerencsére az Emscipten már tartalmaz egy mechanizmust az aszinkron műveletekhez, méghozzá kettőt: Aszinkronizálás и Emterpreter. Az első a generált JavaScript-kód jelentős felfúvódásán keresztül működik, és már nem támogatott. A második a jelenlegi "helyes út", és a natív értelmező bájtkód-generálásán keresztül működik. Természetesen lassan működik, de nem duzzasztja fel a kódot. Igaz, ehhez a mechanizmushoz a korutinok támogatását függetlenül kellett hozzájárulni (voltak már az Asyncifyhoz írt korutinok, és az Emterpreterhez is volt egy körülbelül azonos API implementációja, csak csatlakoztatni kellett őket).

Jelenleg még nem sikerült egy WASM-ben lefordított és Emterpreter segítségével értelmezett kódra bontani, így a blokkeszközök még nem működnek (lásd a következő sorozatban, ahogy mondják...). Vagyis a végén valami ehhez hasonló vicces réteges dolgot kellene kapnia:

  • értelmezett blokk I/O. Nos, tényleg natív teljesítményű emulált NVMe-re számított? 🙂
  • statikusan lefordított fő QEMU kód (fordító, egyéb emulált eszközök stb.)
  • dinamikusan lefordított vendégkód WASM-be

A QEMU források jellemzői

Ahogy valószínűleg már sejtette, a vendégarchitektúrák emulálására szolgáló kód és a gazdagép-utasítások generálására szolgáló kód a QEMU-ban elkülönül. Valójában ez még egy kicsit trükkösebb is:

  • vannak vendégarchitektúrák
  • van gyorsítók, nevezetesen KVM hardvervirtualizációhoz Linuxon (egymással kompatibilis vendég- és gazdarendszerekhez), TCG JIT kód generálásához bárhol. A QEMU 2.9-től kezdve megjelent a HAXM hardvervirtualizációs szabvány támogatása a Windows rendszeren (a részleteket)
  • ha TCG-t használunk és nem hardveres virtualizációt, akkor külön kódgenerálási támogatással rendelkezik minden hosztarchitektúrához, valamint az univerzális értelmezőhöz
  • ... és mindezek körül - emulált perifériák, felhasználói felület, migráció, rekord-visszajátszás stb.

Egyébként tudtad: A QEMU nem csak a teljes számítógépet képes emulálni, hanem a processzort is egy külön felhasználói folyamathoz a gazdagép kernelében, amelyet például az AFL fuzzer használ bináris műszerezéshez. Esetleg valaki szeretné ezt a QEMU üzemmódot átvinni JS-re? 😉

A legtöbb régóta működő ingyenes szoftverhez hasonlóan a QEMU is a híváson keresztül épül fel configure и make. Tegyük fel, hogy úgy dönt, hogy hozzáad valamit: TCG-háttérprogramot, szál-megvalósítást vagy valami mást. Ne rohanjon boldognak/iszonyodni (aláhúzni kell) az Autoconffal való kommunikáció lehetőségétől – sőt, configure A QEMU látszólag saját kezűleg íródott, és nem keletkezik semmiből.

WebAssemble

Tehát mi ez a WebAssembly (más néven WASM) nevű dolog? Ez az Asm.js helyettesítője, és már nem adja ki magát érvényes JavaScript-kódnak. Éppen ellenkezőleg, pusztán bináris és optimalizált, és még egyszerűen egész szám beírása sem túl egyszerű: a tömörség érdekében a formátumban tárolódik LEB128.

Talán hallott már az Asm.js újrahurkolási algoritmusáról – ez a „magas szintű” áramlásvezérlő utasítások (vagyis ha-akkor-else, hurkok stb.) visszaállítása, amelyre a JS-motorokat tervezték. az alacsony szintű LLVM IR, közelebb a processzor által végrehajtott gépi kódhoz. Természetesen a QEMU köztes reprezentációja közelebb áll a másodikhoz. Úgy tűnik, itt a bájtkód, vége a gyötrelemnek... És akkor vannak blokkok, ha-akkor-más és ciklusok!..

És ez egy másik ok, amiért a Binaryen hasznos: természetesen képes olyan magas szintű blokkokat fogadni, amelyek közel vannak ahhoz, amit a WASM tárolna. De kódot is tud előállítani az alapvető blokkok és a köztük lévő átmenetek grafikonjából. Nos, már mondtam, hogy a WebAssembly tárolási formátumot a kényelmes C/C++ API mögé rejti.

TCG (Tiny Code Generator)

TCG eredetileg volt A C fordító hátterében láthatóan nem bírta a versenyt a GCC-vel, de végül megtalálta a helyét a QEMU-ban, mint a gazdagép platform kódgeneráló mechanizmusa. Létezik egy TCG háttérprogram is, ami valamilyen absztrakt bájtkódot generál, amit az interpreter azonnal végrehajt, de úgy döntöttem, hogy ezúttal kerülöm a használatát. Az viszont tény, hogy a QEMU-ban már lehetőség van a függvényen keresztül a generált TB-re való áttérés engedélyezésére tcg_qemu_tb_exec, nagyon hasznosnak bizonyult számomra.

Ha új TCG-háttérprogramot szeretne hozzáadni a QEMU-hoz, létre kell hoznia egy alkönyvtárat tcg/<имя архитектуры> (ebben az esetben, tcg/binaryen), és két fájlt tartalmaz: tcg-target.h и tcg-target.inc.c и felírni minden arról szól configure. Más fájlokat is elhelyezhetsz oda, de ahogy e kettő nevéből sejthető, mindkettő szerepelni fog valahol: az egyik normál fejlécfájlként (ez benne van tcg/tcg.h, és ez már a könyvtárak más fájljaiban van tcg, accel és nem csak), a másik - csak kódrészletként tcg/tcg.c, de hozzáfér a statikus funkcióihoz.

Mivel úgy döntöttem, hogy túl sok időt töltök a működésének részletes vizsgálatával, egyszerűen kimásoltam ennek a két fájlnak a „csontvázát” egy másik háttér-implementációból, és ezt őszintén jeleztem a licenc fejlécében.

fájl tcg-target.h főleg az űrlap beállításait tartalmazza #define-s:

  • hány regiszter és milyen szélesség van a célarchitektúrán (annyi van, amennyit akarunk, amennyit akarunk - a kérdés inkább az, hogy a „teljesen célzott” architektúrán mit generál a böngésző hatékonyabb kódba ...)
  • host utasítások igazítása: x86-on, sőt TCI-ben sem igazodnak el az utasítások, de a kódpufferbe egyáltalán nem utasításokat fogok betenni, hanem a Binaryen könyvtári struktúrákra mutató mutatókat, szóval azt mondom: 4 bájtok
  • milyen opcionális utasításokat generálhat a háttérprogram - mindent beleírunk, amit a Binaryenben találunk, a többit hagyja, hogy a gyorsító maga bontsa egyszerűbbekre
  • Mekkora a háttérprogram által kért TLB gyorsítótár hozzávetőleges mérete. A helyzet az, hogy a QEMU-ban minden komoly: bár vannak helper funkciók, amelyek a vendég-MMU figyelembevételével végzik a betöltést/tárolást (hol lennénk most nélküle?), de a fordítási gyorsítótárukat struktúra formájában mentik el, a amelyek feldolgozása kényelmesen közvetlenül beágyazható broadcast blokkokba. A kérdés az, hogy ebben a struktúrában melyik eltolást lehet a leghatékonyabban feldolgozni egy kis és gyors parancssorral?
  • itt módosíthatja egy vagy két lefoglalt regiszter célját, engedélyezheti a TB hívását egy függvényen keresztül, és opcionálisan leírhat néhány apró inline-funkciók, mint flush_icache_range (de ez nem a mi esetünk)

fájl tcg-target.inc.cTermészetesen általában sokkal nagyobb méretű, és számos kötelező funkciót tartalmaz:

  • inicializálás, beleértve a korlátozásokat arra vonatkozóan, hogy mely utasítások mely operandusokon működhetnek. Kirívóan másoltam egy másik háttérprogramból
  • függvény, amely egy belső bájtkód utasítást vesz fel
  • Ide tehetünk segédfunkciókat is, illetve statikus függvényeket is használhatunk innen tcg/tcg.c

Saját magam számára a következő stratégiát választottam: a következő fordítási blokk első szavaiba négy mutatót írtam fel: kezdőjel (egy adott érték a közelben 0xFFFFFFFF, amely meghatározta a TB aktuális állapotát), kontextust, generált modult és varázsszámot a hibakereséshez. Először a jelölést helyezték el 0xFFFFFFFF - nAhol n - egy kis pozitív szám, és minden alkalommal, amikor az értelmezőn keresztül végrehajtották, 1-gyel nőtt. Amikor elérte 0xFFFFFFFE, a fordítás megtörtént, a modult a függvénytáblázatba mentettük, egy kis „indítóba” importáltuk, amibe a végrehajtás ment tcg_qemu_tb_exec, és a modult eltávolították a QEMU memóriából.

A klasszikusokat átfogalmazva: „Mankó, mennyi minden összefonódik ebben a hangzásban a proger szívéhez...”. Az emlék azonban valahol kiszivárgott. Ráadásul a QEMU által kezelt memória volt! Volt egy kódom, ami a következő utasítás (na jó, azaz mutató) írásakor törölte azt, aminek linkje korábban ezen a helyen volt, de ez nem segített. Valójában a legegyszerűbb esetben a QEMU indításkor lefoglalja a memóriát, és oda írja a generált kódot. Amikor a puffer kifogy, a kód kidobásra kerül, és a következőt kezdik a helyére írni.

A kód tanulmányozása után rájöttem, hogy a bűvös számmal való trükk lehetővé tette, hogy ne bukjak el a kupac megsemmisítésénél azáltal, hogy az inicializálatlan pufferen az első lépésben kiszabadítottam valamit. De ki írja át a puffert, hogy később megkerülje a funkciómat? Ahogy az Emscripten fejlesztői tanácsolják, amikor problémába ütköztem, a kapott kódot visszaportoltam a natív alkalmazásba, beállítottam rá a Mozilla Record-Replay-t... Általánosságban elmondható, hogy végül egy egyszerű dologra jöttem rá: minden blokknál a struct TranslationBlock leírásával. Találd ki, hol... Így van, közvetlenül a blokk előtt a pufferben. Ezt felismerve úgy döntöttem, hogy abbahagyom a mankók használatát (legalábbis néhányat), egyszerűen kidobtam a varázsszámot, és átvittem a maradék szavakat struct TranslationBlock, létrehoz egy egyedi hivatkozású listát, amely gyorsan bejárható a fordítási gyorsítótár alaphelyzetbe állításakor, és memóriát szabadít fel.

Néhány mankó megmarad: például megjelölt mutatók a kódpufferben – némelyikük egyszerűen BinaryenExpressionRef, vagyis azt nézik, hogy milyen kifejezéseket kell lineárisan beletenni a generált alapblokkba, egy rész a BB-k közötti átmenet feltétele, rész az, hogy merre kell menni. Nos, a Relooper számára már vannak előkészített blokkok, amelyeket a feltételeknek megfelelően csatlakoztatni kell. Megkülönböztetésükre azt a feltételezést használjuk, hogy mindegyik legalább négy bájttal igazodik, így nyugodtan használhatjuk a legkisebb jelentőségű két bitet a címkéhez, csak ne felejtsük el, hogy szükség esetén eltávolítsuk. Egyébként a QEMU-ban már használnak ilyen címkéket a TCG hurokból való kilépés okának jelzésére.

A Binaryen használata

A WebAssembly moduljai függvényeket tartalmaznak, amelyek mindegyike tartalmaz egy törzset, amely kifejezés. A kifejezések unáris és bináris műveletek, egyéb kifejezések listáiból álló blokkok, vezérlőfolyamat stb. Ahogy már mondtam, a vezérlési folyamat itt pontosan magas szintű elágazások, hurkok, függvényhívások stb. A függvények argumentumait nem a veremben adják át, hanem kifejezetten, akárcsak a JS-ben. Vannak globális változók is, de nem használtam őket, ezért nem mesélek róluk.

A függvényeknek vannak nullától kezdődően számozott helyi változói is, amelyek típusa: int32 / int64 / float / double. Ebben az esetben az első n helyi változó a függvénynek átadott argumentum. Kérjük, vegye figyelembe, hogy bár itt minden nem teljesen alacsony szintű a vezérlési folyamat szempontjából, az egész számok még mindig nem hordozzák az „signed/unsigned” attribútumot: a szám viselkedése a műveleti kódtól függ.

Általánosságban elmondható, hogy a Binaryen biztosítja egyszerű C-API: létrehoz egy modult, benne kifejezések létrehozása - unáris, bináris, blokkok más kifejezésekből, vezérlési folyamat stb. Ezután létrehoz egy függvényt, amelynek törzse egy kifejezés. Ha Önnek, mint nekem, alacsony szintű átmeneti gráfja van, a relooper komponens segít. Ha jól értem, lehetséges a végrehajtási folyamat magas szintű vezérlése egy blokkban, mindaddig, amíg az nem lépi túl a blokk határait - azaz lehetséges belső gyorsútra / lassúra tenni útvonal elágazása a beépített TLB gyorsítótár feldolgozó kódjában, de nem zavarja a „külső” vezérlési folyamatot. Amikor felszabadítasz egy reloopert, a blokkjai felszabadulnak; amikor felszabadítasz egy modult, eltűnnek a hozzá rendelt kifejezések, függvények stb. aréna.

Ha azonban a kódot menet közben szeretné értelmezni anélkül, hogy szükségtelenül létrehozna és törölne egy interpreter példányt, akkor érdemes lehet ezt a logikát egy C++ fájlba helyezni, és onnan közvetlenül kezelni a könyvtár teljes C++ API-ját, megkerülve a kész csomagolóanyagokat készítettek.

Tehát a szükséges kód generálásához

// настроить глобальные параметры (можно поменять потом)
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);

... ha valamit elfelejtettem, elnézést, ez csak a skála ábrázolása, és a részletek a dokumentációban találhatók.

És most kezdődik a crack-fex-pex, valami ilyesmi:

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);

Annak érdekében, hogy a QEMU és a JS világát valahogy összekapcsoljuk, és egyúttal a lefordított függvényeket gyorsan elérjük, egy tömböt hoztak létre (a launcherbe importálható függvénytáblázat), és oda helyeztük el a generált függvényeket. Az index gyors kiszámításához kezdetben a nulla szó fordítási blokk indexét használták, de az ezzel a képlettel kiszámított index egyszerűen belefért a mezőbe. struct TranslationBlock.

By the way, demó (jelenleg homályos jogosítvánnyal) csak Firefoxban működik jól. A Chrome fejlesztői voltak valahogy nincs készen arra a tényre, hogy valaki több mint ezer példányt szeretne létrehozni WebAssembly modulokból, ezért egyszerűen egy gigabájt virtuális címteret különített el mindegyikhez...

Ez minden most. Talán lesz még cikk, ha valakit érdekel. Mégpedig legalább marad csak blokkeszközök működését. Az is lehet értelme, hogy a WebAssembly modulok összeállítását a JS világban megszokott módon aszinkronra tegyük, hiszen van még egy interpreter, amely mindezt meg tudja csinálni, amíg a natív modul el nem készül.

Végül egy rejtvény: 32 bites architektúrára fordítottál egy binárist, de a kód a memóriaműveletek révén a Binaryenből mászik fel, valahol a veremben, vagy valahol máshol a 2 bites címtér felső 32 GB-jában. A probléma az, hogy Binaryen szemszögéből ez túl nagy eredő címhez fér hozzá. Hogyan lehet ezt megkerülni?

Admin módjára

Végül nem teszteltem, de az első gondolatom az volt: „Mi lenne, ha telepítenék a 32 bites Linuxot?” Ekkor a címtér felső részét a kernel foglalja el. A kérdés csak az, hogy mennyi lesz elfoglalva: 1 vagy 2 Gb.

Programozói módon (opció gyakorló szakemberek számára)

Fújjunk egy buborékot a címtér tetején. Magam sem értem, miért működik - ott már kell lennie egy veremnek. De "gyakorlók vagyunk: nekünk minden működik, de senki sem tudja, miért..."

// 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));
}

... igaz, hogy nem kompatibilis a Valgrinddal, de szerencsére maga a Valgrind nagyon hatékonyan taszít ki mindenkit onnan :)

Talán valaki jobban elmagyarázza, hogyan működik ez a kódom...

Forrás: will.com

Hozzászólás