QEMU.js: teraz na serio a s WASM

Niekedy dávno som sa v rámci zábavy rozhodol dokázať reverzibilitu procesu a naučiť sa generovať JavaScript (presnejšie, Asm.js) z strojového kódu. Pre experiment bol zvolený QEMU, o chvíľu neskôr bol napísaný článok na Habr. V komentároch mi odporučili prepracovať projekt na WebAssembly, a tiež som sám nemal chuť opúšťať skoro dokončený projekt, takže sa to nezdalo správne… Práca napredovala, ale veľmi pomaly, a nedávno sa objavil v tom článku komentár tému "Takže, ako to všetko dopadlo?" Na moju rozšírenú odpoveď som počul "To je na článok". No, keď je to na článok, tak bude článok. Možno to niekomu poslúži. Z neho sa čitateľ dozvie niektoré fakty o fungovaní backendu generovania 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 "nejako" portovať QEMU na JavaScript, tentoraz sa rozhodlo robiť to poriadne a neopakovať staré chyby.

Chyba číslo jedna: odtrhnúť sa od point release

Moja prvá chyba bola oddeliť svoju verziu od upstream verzie 2.4.1. Vtedy sa mi to zdalo ako dobrý nápad: ak existuje point release, znamená to, že je pravdepodobne stabilnejší ako obyčajná 2.4, a už vôbec nie vetva master. A keďže som plánoval pridať značné množstvo vlastných chýb, tie cudzí mi skutočne neboli potrebné. Asi to tak aj skončilo. Ale tu je problém: QEMU sa neustále vyvíja a v istom momente dokonca ohlásili optimalizáciu generovaného kódu o 10%. "Aha, teraz vpadnem" pomyslel som si a sklamal sa. Tu treba spraviť odbočku: vzhľadom na jednovláknovú povahu QEMU.js a na to, že pôvodný QEMU nepredpokladá absenciu multithreadingu (to znamená, že je kritická možnosť súčasnej práce viacerých nesúvislých code path, a nie len "použiť všetky jadrá"), hlavné funkcie vlákien bolo treba "otočiť" pre možnosť volania zvonka. To spôsobilo niektoré prirodzené problémy pri zlúčení. Avšak fakt, že časť zmien z vetvy master, z ktorej som sa snažil zlúčiť svoj kód, bola tiež cherry picked do point release (a teda aj do mojej vetvy), pravdepodobne tiež nepridalo na pohodlí.

Celkovo som sa rozhodol, že prototyp dáva zmysel rozobrať na súčiastky a postaviť novú verziu od základov na niečom novšom a teraz už z master.

Chyba číslo dva: TLP metodológia

V podstate to nie je ani chyba, skôr len charakteristika vytvárania projektu v podmienkach úplného nepochopenia, ako "kam a ako ísť?" a vôbec "dostaneme sa tam?". V týchto podmienkach programovanie na kolene bolo ospravedlniteľnou voľbou, ale, samozrejme, úplne sa to nechcelo opakovať bez potreby. Tentoraz som chcel robiť veci s rozumom: atomárne commity, vedomé zmeny kódu (a nie "spájať náhodné znaky spolu, až to skompiluje (s varovaniami)," ako o niekom raz povedal Linus Torvalds, ak veríme WikiCitátu) atď.

Chyba číslo tri: nevediac, akú vodu skákať do vody

Od toho som sa ani teraz úplne nezbavil, ale teraz som sa rozhodol ísť nie po ceste úplne najmenšieho odporu a urobiť to "dospelácky", a to konkrétne, napísať svoj TCG backend od nuly, aby som potom nemusel hovoriť, že "Áno, to je síce pomalé, ale ja nemôžem všetko kontrolovať – TCI je tak napísaný…". Okrem toho sa to z pôvodnej perspektívy zdalo byť zjavné riešenie, pretože generujem binárny kód. Ako sa hovorí, "Súhrn Gent,uale nie ten správny": kód je síce binárny, ale prenos naň nie je len tak jednoduchý – treba ho explicitne pushnúť do prehliadača na kompiláciu, pričom získame niečo ako objekt zo sveta JS, ktorý ešte potrebujeme niekam uložiť. Napriek tomu na normálnych RISC architektúrach, pokiaľ chápem, je typickou situáciou potreba explicitne vymazávať cache inštrukcií pre regenerovaný kód – ak to nie je presne to, čo potrebujeme, tak to je v každom prípade blízko. Okrem toho som sa z mojej predchádzajúcej snahy naučil, že prenos na stred bloku prekladu akosi nefunguje, takže bytecode, interpretovaný z akéhokoľvek posunu, nám veľmi nie je potrebný, a môžeme jednoducho generovať po funkcii na TB.

Prišli a kopli

Aj keď som s prepisovaním kódu začal už v júli, kúzelný impulz prišiel nečakane: obyčajne prichádzajú e-maily z GitHubu ako oznámenia odpovedí na otázky a Pull requesty, a tu, nečakane spomenutie v vlákne Binaryen ako backend qemu v kontexte: "Toto robil niečo také, možno povie niečo. " Hovorilo sa o využití príbuznej knižnice Emscripten Binaryen na vytvorenie WASM JIT. Tak som povedal, že máte licenciu Apache 2.0, zatiaľ čo QEMU je ako celok šírené pod GPLv2, a nie sú veľmi kompatibilné. Zrazu sa ukázalo, že licenciu je možné nejako to opraviť (neviem: možno zmeniť, možno dvojité licencovanie, možno niečo iné…). To ma samozrejme potešilo, pretože som sa už niekoľkokrát predtým pozeral na binárny formát WebAssembly, a bolo mi akosi smutno a nebolo mi to jasné. Tu bola knižnica, ktorá zje aj základné bloky s grafom prechodov, vydá bytový kód a dokonca ho aj spustí v interprete, ak to bude potrebné.

Potom bolo ešte list v zozname distribúcie QEMU, ale to už skôr súvisí s otázkou, "A komu to vôbec treba?". A ono, nečakane, ukázalo sa, že treba. Minimálne sa dajú nazbierať také možnosti použitia, ak to bude viac-menej rýchlo fungovať:

  • spustenie niečoho vzdelávacieho úplne bez inštalácie
  • virtualizácia na iOS, kde sa hovorí, že jediné aplikácie, ktoré majú právo na generovanie kódu na let, sú JS-enginy (je to pravda?)
  • demonštrácia mini-OS - jednodoskových, zabudovaných, rôznych firmvérov atď…

Vlastnosti prostredia vykonávania prehliadača

Ako som už povedal, QEMU je viazaný na viacero vlákien, a v prehliadači nie je. No, teda ako nie je… Najprv nebolo vôbec, potom sa objavili WebWorkers - pokiaľ chápem, je to viacvláknovosť založená na prenose správ bez zdieľaných premenných. Samozrejme, to vytvára značné problémy pri portovaní existujúceho kódu založeného na modeli zdieľanej pamäte. Potom pod tlakom verejnosti bola zavedená aj táto pod názvom SharedArrayBuffers. Pomaly ju zavádzali, oslávili jej spustenie v rôznych prehliadačoch, potom oslávili nový rok, a potom Meltdown… Po čom dospeli k záveru, že bez ohľadu na to, aké presné je meranie času, pomocou zdieľanej pamäte a vlákna, ktoré inkrementuje čítač, to bude aj tak dosť presné. Takto vypnuli viacvláknovosť so zdieľanou pamäťou. Zdalo sa, že ju potom znova zapínali, ale ako sa ukázalo z prvého experimentu, aj bez nej je život, a ak je to tak, tak sa pokúsime spraviť, bez usporiadania na viacvláknovosť.

Druhá vlastnosť spočíva v nemožnosti nízkoúrovňových manipulácií so zásobníkom: nemôžete len tak vziať aktuálny kontext, uložiť ho a prepnúť sa na nový so novým zásobníkom. Zásobník volaní spravuje virtuálna strojová JS. Zdalo by sa, aký je problém, keď sme sa rozhodli plne spravovať predchádzajúce vlákna ručne? Problém je v tom, že blokové vstupno-výstupné operácie v QEMU sú realizované prostredníctvom korutín, a tu by nám veľmi pomohli nízkoúrovňové manipulácie so zásobníkom. Našťastie, Emscripten už obsahuje mechanizmus pre asynchrónne operácie, dokonca dva: Asyncify a Emterpreter. Prvý funguje cez značné zväčšenie generovaného JavaScript kódu a už sa nepodporuje. Druhý je súčasný „správny spôsob“ a pracuje prostredníctvom generovania bajtkódu pre vlastný interpretátor. Funguje, samozrejme, pomaly, ale aspoň nezväčšuje kód. Pravda, podporu korutín pre tento mechanizmus museli pridať vlastnoručne (už boli korutíny napísané pre Asyncify a bola tu implementácia približne toho istého rozhrania pre Emterpreter, len ich bolo treba spojiť).

V súčasnosti som ešte nemal čas rozdeliť kód na kompilovateľný do WASM a interpretovateľný pomocou Emterpreter, takže blokové zariadenia zatiaľ nefungujú (pozrite sa na ďalšie epizódy, ako sa hovorí…). Teda, nakoniec by malo vzniknúť niečo takéto zábavné vrstvené:

  • interpretovaný blokový vstup-výstup. Nuž, čo, naozaj ste očakávali emulovaný NVMe s natívnym výkonom? 🙂
  • staticky skompilovaný hlavný kód QEMU (translátor, ostatné emulované zariadenia atď.)
  • dynamicky kompilovaný do WASM hosťový kód

Rysy zdrojových kódov QEMU

Ako ste už pravdepodobne uhádli, kód emulácie hosťujúcich architektúr a kód generovania hosťujúcich strojových inštrukcií sú v QEMU oddelené. V skutočnosti je to tam ešte o niečo zložitejšie:

  • existujú hosťujúce architektúry
  • je akcelerátory, najmä KVM pre hardvérovú virtualizáciu na Linuxe (pre kompatibilné hosťujúce a hostiteľské systémy), TCG pre JIT-kódovú generáciu kdekoľvek. Od verzie QEMU 2.9 sa pridala podpora štandardu hardvérovej virtualizácie HAXM na Windows (podrobnosti)
  • ak sa používa TCG namiesto hardvérovej virtualizácie, má samostatnú podporu generovania kódu pre každú hostiteľskú architektúru, ako aj pre univerzálny interpretátor
  • … a okolo toho je emulovan periférne zariadenia, používateľské rozhranie, migrácia, record-replay, atď.

Mimochodom, vedeli ste, že: QEMU môže emulovať nielen celý počítač, ale aj procesor pre samostatný používateľský proces v hostiteľskom jadre, čo napríklad využíva fuzzer AFL na instrumentáciu binárnych súborov. Možno by niekto chcel preniesť tento režim práce QEMU na JS? 😉

Rovnako ako väčšina dlhodobo existujúcich voľných programov, QEMU sa zostavuje cez volanie configure a make. Predpokladajme, že ste sa rozhodli niečo pridať: TCG backend, implementáciu vlákien, niečo iné. Neponáhľajte sa radovať/jesť (to pravé podčiarknite) perspektívy komunikácie s Autoconf — v skutočnosti je configure u QEMU zjavne vlastnoručne napísaný a ne generovaný z ničoho.

WebAssembly

Čo to teda je — WebAssembly (tiež WASM)? To je náhrada za Asm.js, ktorá sa už nepredstiera, že je platným JavaScriptovým kódom. Naopak, je to čisto binárny a optimalizovaný kód, a dokonca len zapísať celé číslo do neho nie je veľmi jednoduché: pre kompaktnosť sa uchováva vo formáte LEB128.

Možno ste počuli o algoritme relooping pre Asm.js — to je obnova «vysokoúrovňových» inštrukcií riadenia toku vykonávania (t.j. if-then-else, cykly atď.), pre ktoré sú JS enginy prispôsobené, z nízkoúrovňového LLVM IR, bližšieho k strojovému kódu, ktorý vykonáva procesor. Samozrejme, medzičasové zastúpenie QEMU je bližšie k druhému. Zdalo by sa, že tu je byte kód, koniec mučeniu… A tu sú bloky, if-then-else a cykly!

A v tom je ďalší dôvod, prečo je Binaryen užitočný: samozrejme môže prijímať vysokoúrovňové bloky, blízke tomu, čo bude uložené vo WASM. Ale tiež môže generovať kód z grafu základných blokov a prechodov medzi nimi. No a o tom, čo skrýva za pohodlným C/C++ API formát ukladania WebAssembly, som už povedal.

TCG (Tiny Code Generator)

TCG pôvodne bol backend pre kompilátor C. Potom, zjavne, neprežil konkurenciu s GCC, ale nakoniec našiel svoje miesto v QEMU ako mechanizmus kódovania pre hostiteľskú platformu. Existuje aj TCG backend, ktorý generuje akýsi abstraktný bajtkód, ktorý hneď vykonáva interpretátor, ale tentokrát som sa rozhodol od jeho použitia upustiť. Napriek tomu, fakt, že v QEMU už existuje možnosť prechodu na vygenerovaný TB cez funkciu tcg_qemu_tb_exec, mi prišiel veľmi vhod.

Aby sa nový TCG backend pridal do QEMU, je potrebné vytvoriť podadresár tcg/ (v tomto prípade, tcg/binaryen), a v ňom dva súbory: tcg-target.h a tcg-target.inc.c a napísať všetko toto do configure. Môžete tam umiestniť aj ďalšie súbory, ale, ako sa dá uhádnuť z názvov týchto dvoch, obidva budú niekde zahrnuté: jeden ako bežný hlavičkový súbor (zahrňuje sa do tcg/tcg.h, a ten už do iných súborov v adresároch tcg, accel a nielen), druhý — iba ako kódový útržok v tcg/tcg.c, ale má prístup k jeho statickým funkciám.

Keďže som usúdil, že strávim príliš veľa času podrobným skúmaním, ako to funguje, jednoducho som skopíroval „kostru“ týchto dvoch súborov z inej implementácie backendu, čestne to uvádzajúc v hlavičke licencií.

Súbor tcg-target.h obsahuje prevažne nastavenia vo forme #define- ov:

  • koľko registrov a akej šírky je na cieľovej architektúre (u nás — koľko chceme, toľko máme — otázka je skôr o tom, čo sa generuje do efektívnejšieho kódu prehliadačom na „úplne cieľovej“ architektúre…)
  • zarovnanie hostiteľských inštrukcií: na x86, a aj v TCI, inštrukcie sa vôbec nezarovnávajú, ja sa chystám ukladať do bufferu kódu a nie inštrukcie vôbec, ale ukazovatele na štruktúry knižnice Binaryen, preto poviem: 4 bajty.
  • akú voliteľnú inštrukciu môže generovať backend — zapíname všetko, čo nájdeme v Binaryen, ostatné nech akcelerátor rozbíja na jednoduchšie.
  • Aká je približne veľkosť TLB cache, ktorú vyžaduje backend. Ide o to, že v QEMU je všetko vážne: hoci existujú pomocné funkcie vykonávajúce load/store s ohľadom na hostitel'ský MMU (a kde by sme teraz bez neho?), ich vlastná cache transformácie sa uchováva vo forme štruktúry, ktorú je pohodlné integrovať priamo do blokov transformácie. Otázka je, aké posunutie v tejto štruktúre je najefektívnejšie spracovávané malou a rýchlou sekvenciou inštrukcií.
  • Tu je možné tiež upraviť určenie jedného alebo dvoch rezervovaných registrov, spustiť volanie TB cez funkciu a voliteľne opísať pár drobností. inline-funkcií ako flush_icache_range (ale to nie je náš prípad)

Súbor tcg-target.inc.c, samozrejme, zvyčajne oveľa väčší a obsahuje niekoľko povinných funkcií:

  • inicializácia, ktorá uvádza aj obmedzenia na to, aká inštrukcia môže pracovať s akými operandmi. Úplne som to skopíroval z iného backendu.
  • funkcia, ktorá prijíma jednu inštrukciu vnútorného bytecodeu.
  • Sem môžeme vložiť aj pomocné funkcie a navyše tu môžeme používať statické funkcie z tcg/tcg.c

Pre seba som si zvolil nasledujúcu stratégiu: na začiatku každého bloku transformácie som zapisoval štyri ukazovatele: označenie začiatku (nejaké hodnoty v okolí 0xFFFFFFFF, podľa ktorého sa určovalo aktuálne stav TB), kontext, generovaný modul a magické číslo na ladenie. Najprv sa označenie nastavilo na 0xFFFFFFFF - n, kde n — malé kladné číslo, a pri každom vykonaní cez interpretátor sa zvyšovalo o 1. Keď dosiahlo 0xFFFFFFFE, nastala kompilácia, modul sa uložil do tabuľky funkcií, importovanej do malého „spúšťača“, do ktorého sa vykonávanie presunulo z tcg_qemu_tb_exec, a modul bol odstránený z pamäte QEMU.

Preformulujúc klasiku, „Kladivo, ako veľa v tomto zvuku pre srdce programátora sa spojilo…“. Napriek tomu pamäť niekam utekala. A bola to pamäť riadená QEMU! Mal som kód, ktorý pri zápise ďalšej inštrukcie (no, teda, ukazovateľa) odstránil ten, na ktorý sa odkazovalo na tomto mieste predtým, ale to nepomáhalo. V podstate QEMU pri štarte vyčleňuje pamäť a zapisuje tam generovaný kód. Keď sa buffer skončí, kód sa vyhodí a na jeho miesto sa začne zapisovať nasledujúci.

Z výskumu kódu som pochopil, že obídenie pomocou magic number zabránilo pádu pri deštrukcii haldy, pri oslobodení niečoho nesprávneho na neinitializovanom buffere pri prvom prechode. Ale kto prepíše buffer mimo mojej funkcie potom? Ako radia vývojári Emscripten, keď som narazil na problém, preniesol som výsledný kód späť do natívnej aplikácie, nasmeroval som naň Mozilla Record-Replay… Nakoniec som pochopil jednoduchú vec: pre každý blok sa vyčlení struct TranslationBlock s jeho popisom. Uhádnite, kde… Správne, presne pred blokom priamo v bufferi. Pochopením toho som sa rozhodol prestať s obídeniami (aspoň niektorými) a jednoducho som vyhodil magic number, zatiaľ čo zostávajúce slová som presunul do struct TranslationBlock, pričom som vytvoril jednolinkový zoznam, cez ktorý sa dá rýchlo prejsť pri vymazaní cache prekladu a uvoľniť pamäť.

Niektoré obídenia zostali: napríklad označené ukazovatele v kóde buffera – časť z nich sú jednoducho BinaryenExpressionRef, to znamená, že sa pozerajú na výrazy, ktoré je potrebné lineárne uložiť do generovaného základného bloku, časť – podmienka prechodu medzi BB, časť – kde prejsť. A už sú pripravené bloky pre Relooper, ktoré je potrebné spojiť na základe podmienok. Na ich rozlíšenie sa používa predpoklad, že všetky sú vyrovnané aspoň na štyri bajty, takže je možné v pohode použiť dolné dva bity pre označenie, len na to nezabudnúť pri potrebe odstrániť. Mimochodom, takéto označenia už používajú v QEMU na označenie dôvodu opustenia slučky TCG.

Používanie Binaryen

Moduly vo WebAssembly obsahujú funkcie, z ktorých každá obsahuje telo predstavujúce výraz. Výrazy sú jedno- a dvojčinné operácie, bloky, ktoré pozostávajú zo zoznamov ďalších výrazov, control flow atď. Ako som už povedal, control flow je tu organizovaný presne ako vysokoúrovňové rozvetvenia, slučky, volania funkcií atď. Argumenty funkciám sa odovzdávajú nie na zásobníku, ale explicitne, ako v JS. Existujú aj globálne premenne, ale ja som ich nepoužíval, preto o nich nebudem hovoriť.

Takže funkcie majú číslované lokálne premenné začínajúce nulou, ktoré majú typ: int32 / int64 / float / double. Prvé n lokálnych premenných sú argumenty odovzdané funkcii. Upozorňujem, že hoci sem to nie je úplne nízkoúrovňové z hľadiska toku riadenia, celé čísla predsa nemajú indikátor „so znamienkom/bez znamienka“: to, ako sa číslo správa, závisí od kódu operácie.

Všeobecne platí, že Binaryen poskytuje jednoduché C-API: vytvoríte modul, v ňom vytvoríte výrazy — unárne, binárne, bloky z iných výrazov, riadenie toku atď. Potom vytvoríte funkciu, ktorej telo musí určiť výraz. Ak máte, ako ja, nízkoúrovňový graf prechodov — komponent relooper vám pomôže. Pokiaľ viem, používanie vysokúrovňového riadenia toku vykonávania v bloku je možné, pokiaľ nepresahuje hranice bloku — to znamená, že je možné urobiť vnútorné vetvenie fast path / slow path v rámci vstavaného kódu spracovania TLB cache, ale zasahovať do „vonkajšieho“ toku riadenia — nie. Keď uvoľníte relooper, uvoľnia sa jeho bloky, keď uvoľníte modul — zmiznú výrazy, funkcie atď., ktoré sú vyhradené na jeho arénu.

Avšak, ak chcete interpretovať kód naživo bez nadbytočného vytvárania a mazania inštancií interpretátora, môže byť rozumné presunúť túto logiku do súboru na C++ a priamo ovládať celé C++ API knižnice, obchádzajúc predpripravené obaly.

Takže, aby ste vygenerovali kód, musíte

// настроить глобальные параметры (можно поменять потом)
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 — prepáčte, je to len na predstavu o rozsahu, a podrobnosti sú v dokumentácii.

A teraz začína kraks-feks-peks, približne takto:

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,
          // ...
      }
  );
  // a už máte instance!
}, buf, sz);

Aby nejako prepojili svet QEMU a JS a pritom rýchlo pristupovali k skompilovaným funkciám, vytvoril sa zoznam (tabuľka funkcií na import do spúšťacieho modulu), kam sa vkladali generované funkcie. Na rýchle vypočítanie indexu sa pôvodne používal index nulového slova prehodnocovacieho bloku, ale neskôr sa index vypočítaný podľa takejto vzorca jednoducho zapísal do poľa v struct TranslationBlock.

Mimochodom, demo (zatiaľ s nejasnou licenciou) funguje normálne iba vo Firefox. Vývojári Chrome boli nejako nepripravení na to, že niekto by chcel vytvoriť viac ako tisíc inštancií modulov WebAssembly, takže jednoducho pridelili po gigabajte virtuálneho adresného priestoru na každý...

Zatiaľ je na tomto všetko. Možno bude ešte jeden článok, ak to niekoho zaujíma. Konkrétne, zostalo minimálne len prinútiť fungovať blokové zariadenia. Možno by tiež malo zmysel spraviť kompiláciu modulov WebAssembly asynchrónnou, ako je to zvykom vo svete JS, keďže máme interpret, ktorý to všetko môže vykonávať, kým nie je nativný modul pripravený.

Na záver hádanka: zhromaždili ste binárku na 32-bitovej architektúre, ale kód cez operácie s pamäťou sa dostáva z Binaryen, niekam na zásobník alebo niekam do horných 2 GB 32-bitového adresného priestoru. Problém je v tom, že z pohľadu Binaryen je toto volanie na príliš veľkú výslednú adresu. Ako to obísť?

Adminsky

Nakoniec som to netestoval, ale prvá myšlienka bola „A čo ak nainštalujem 32-bitový Linux?“ Potom horná časť adresného priestoru bude obsadená jadrom. Otázka len je, koľko bude obsadené: 1 alebo 2 Gb.

Programátorsky (varianta pre praktikantov)

Vytvoríme bublinu v hornej časti adresného priestoru. Sám neviem, prečo to funguje — predsa tam už musí byť zásobník. Ale „my praktikanti: u nás všetko funguje, ale nikto nevie prečo…“ … s Valgrind-om, pravda, nie je kompatibilné, ale našťastie Valgrind sám veľmi efektívne všetkých odtiaľ vytláča 🙂

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

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

Qemu.js s podporou JIT: mleté mäso sa predsa môže otočiť späť

Zdroj: habr.com

Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster