QEMU.js: dabar rimtas ir su WASM

Kažkada nusprendžiau pasilinksminti įrodyti proceso grįžtamumą ir išmokti generuoti JavaScript (tiksliau, Asm.js) iš mašinos kodo. Eksperimentui buvo pasirinktas QEMU, o po kurio laiko buvo parašytas straipsnis apie Habr. Komentaruose man buvo patarta perdaryti projektą „WebAssembly“ ir netgi pasitraukti iš savęs beveik baigta Kažkaip nenorėjau projekto... Darbas vyko, bet labai lėtai, o dabar, neseniai tame straipsnyje pasirodė komentaras tema "Tai kaip viskas baigėsi?" Atsakydamas į mano išsamų atsakymą, išgirdau: „Tai skamba kaip straipsnis“. Na, jei gali, bus straipsnis. Gal kam nors tai bus naudinga. Iš jo skaitytojas sužinos keletą faktų apie QEMU kodų generavimo užpakalinių sistemų dizainą, taip pat kaip parašyti žiniatinklio programai skirtą „Just-in-Time“ kompiliatorių.

užduotys

Kadangi jau buvau išmokęs „kažkaip“ pervesti QEMU į JavaScript, šį kartą buvo nuspręsta tai daryti išmintingai ir nekartoti senų klaidų.

Klaida numeris vienas: šaka iš taško išleidimo

Pirmoji mano klaida buvo atjungti savo versiją iš ankstesnės 2.4.1 versijos. Tada man atrodė gera mintis: jei taškinis leidimas egzistuoja, tai jis tikriausiai yra stabilesnis nei paprastas 2.4, o juo labiau atšaka master. Ir kadangi planavau pridėti nemažą kiekį savo klaidų, man visiškai nereikėjo kitų. Turbūt taip ir išėjo. Bet štai kas: QEMU nestovi vietoje, o kažkuriuo momentu netgi paskelbė apie sugeneruoto kodo optimizavimą 10 procentų.„Taip, dabar aš sustingsiu“, – pagalvojau ir palūžau. Čia turime padaryti nukrypimą: dėl vienos gijos QEMU.js pobūdžio ir to, kad originalus QEMU nereiškia kelių gijų nebuvimo (ty galimybės vienu metu valdyti kelis nesusijusius kodo kelius, ir ne tik "naudoti visus branduolius") yra labai svarbus, o pagrindinės gijų funkcijos, kurias turėjau "išjungti", kad galėčiau iškviesti iš išorės. Dėl to susijungimo metu atsirado natūralių problemų. Tačiau tai, kad kai kurie pakeitimai iš filialo master, su kuriais bandžiau sujungti savo kodą, taip pat buvo pasirinktos taško leidime (taigi ir mano šakoje), taip pat tikriausiai nebūtų pridėję patogumo.

Apskritai nusprendžiau, kad vis tiek prasminga išmesti prototipą, išardyti jį dalimis ir sukurti naują versiją nuo nulio, remiantis kažkuo naujesne ir dabar nuo. master.

Klaida numeris antra: TLP metodika

Iš esmės tai nėra klaida, apskritai tai tik projekto kūrimo bruožas visiškai nesupratus tiek „kur ir kaip persikelti?“, tiek apskritai „ar mes ten pateksime? Tokiomis sąlygomis gremėzdiškas programavimas buvo pagrįstas variantas, bet, žinoma, nenorėjau to be reikalo kartoti. Šį kartą norėjau tai padaryti išmintingai: atominiai įsipareigojimai, sąmoningi kodo keitimai (o ne „atsitiktinių simbolių sujungimas, kol susikompiliuoja (su įspėjimais)“, kaip kažkada apie ką nors pasakė Linusas Torvaldsas, pagal Wikiquote) ir t.t.

Klaida numeris trys: patekimas į vandenį nežinant brastos

Vis dar iki galo to neatsikračiau, bet dabar nusprendžiau neiti mažiausio pasipriešinimo keliu, o tai daryti „suaugusiųjų būdu“, ty parašyti savo TCG backendą nuo nulio, kad nereikėtų. vėliau turėsiu pasakyti: „Taip, tai, žinoma, lėtai, bet aš negaliu visko suvaldyti – taip parašyta TCI...“ Be to, tai iš pradžių atrodė akivaizdus sprendimas, nes Aš generuoju dvejetainį kodą. Kaip sakoma: „Susirinko Gentasу, bet ne tas“: kodas, be abejo, yra dvejetainis, tačiau valdymo į jį tiesiog perkelti negalima – jis turi būti aiškiai įstumtas į naršyklę kompiliavimui, todėl atsiranda tam tikras objektas iš JS pasaulio, kurį vis tiek reikia būti kažkur išgelbėtam. Tačiau įprastose RISC architektūrose, kiek suprantu, tipiška situacija yra būtinybė aiškiai iš naujo nustatyti regeneruoto kodo instrukcijų talpyklą - jei to mums nereikia, bet kuriuo atveju tai yra artima. Be to, iš paskutinio bandymo sužinojau, kad valdymas neatrodo perkeltas į vertimo bloko vidurį, todėl mums tikrai nereikia baitinio kodo, interpretuoto iš bet kokio poslinkio, ir galime jį tiesiog sugeneruoti iš TB funkcijos. .

Jie atėjo ir spardė

Nors kodą pradėjau perrašinėti dar liepos mėnesį, nepastebimai užklupo magija: dažniausiai laiškai iš „GitHub“ gaunami kaip pranešimai apie atsakymus į „Issues“ ir „Pull“ užklausas, bet čia staiga paminėti temoje Binaryen kaip qemu backend kontekste: „Jis padarė kažką panašaus, gal ką nors pasakys“. Kalbėjome apie su Emscripten susijusios bibliotekos naudojimą Binaryen sukurti WASM JIT. Na, aš sakiau, kad ten turite Apache 2.0 licenciją, o QEMU kaip visuma yra platinama pagal GPLv2, ir jie nėra labai suderinami. Staiga paaiškėjo, kad licencija gali būti kažkaip pataisyti (Nežinau: gal pakeisti, gal dviguba licencija, gal dar kažkas...). Tai, žinoma, mane nudžiugino, nes tuo metu jau buvau įdėmiai apžiūrėjęs dvejetainis formatas WebAssembly, ir man buvo kažkaip liūdna ir nesuprantama. Taip pat buvo biblioteka, kuri suryja pagrindinius blokus su perėjimo grafiku, pagamino baitinį kodą ir, jei reikia, netgi paleisdavo jį pačiame interpretatoriuje.

Tada buvo daugiau laiškas QEMU adresų sąraše, bet tai daugiau apie klausimą „Kam to vis tiek reikia? Ir tai yra staiga, paaiškėjo, kad to reikia. Jei tai veikia daugiau ar mažiau greitai, galite surinkti bent šias naudojimo galimybes:

  • paleisti ką nors mokomojo visiškai be jokios instaliacijos
  • virtualizavimas iOS sistemoje, kur, pasak gandų, vienintelė programa, kuri turi teisę generuoti kodą skrydžio metu, yra JS variklis (ar tai tiesa?)
  • mini OS demonstravimas - vienas diskelis, įmontuotas, visų rūšių programinė įranga ir kt.

Naršyklės vykdymo funkcijos

Kaip jau sakiau, QEMU yra susieta su daugiasriegiu ryšiu, tačiau naršyklė to neturi. Na, tai yra, ne... Iš pradžių jo iš viso nebuvo, tada atsirado WebWorkers - kiek suprantu, tai daugiagija, pagrįsta žinučių perdavimu be bendrinamų kintamųjų. Žinoma, tai sukelia didelių problemų perkeliant esamą kodą pagal bendrosios atminties modelį. Tada, spaudžiant visuomenei, jis taip pat buvo įgyvendintas pavadinimu SharedArrayBuffers. Jis buvo įvestas palaipsniui, švęsdavo jo paleidimą įvairiose naršyklėse, tada švęsdavo Naujuosius metus, o paskui Meltdowną... Po to priėjo prie išvados, kad laiko matavimas grubus ar grubus, bet pasitelkiant bendrą atmintį ir sriegis didinant skaitiklį, tai viskas tas pats tai pavyks gana tiksliai. Taigi išjungėme kelių gijų su bendra atmintimi. Atrodo, kad vėliau jį vėl įjungė, bet, kaip paaiškėjo iš pirmo eksperimento, yra gyvenimas ir be jo, o jei taip, tai pasistengsime tai padaryti nepasikliaunant daugiagija.

Antrasis bruožas yra žemo lygio manipuliacijų su krūva neįmanoma: jūs negalite tiesiog paimti, išsaugoti esamą kontekstą ir pereiti prie naujo su nauja krūva. Skambučių krūvą valdo JS virtuali mašina. Atrodytų, kame problema, nes vis tiek nusprendėme buvusius srautus valdyti visiškai rankiniu būdu? Faktas yra tas, kad QEMU blokų įvestis / išvestis yra įgyvendinama naudojant korutinas, ir čia praverstų žemo lygio krūvos manipuliacijos. Laimei, Emscipten jau turi asinchroninių operacijų mechanizmą, net du: Asinchronizuoti и Vertėjas. Pirmasis veikia dėl didelio sugeneruoto „JavaScript“ kodo išpūtimo ir nebepalaikomas. Antrasis yra dabartinis „teisingas būdas“ ir veikia per baito kodo generavimą vietiniam vertėjui. Žinoma, tai veikia lėtai, tačiau kodo neišpučia. Tiesa, šio mechanizmo korutinų palaikymas turėjo būti prisidėtas savarankiškai (jau buvo korutinos parašytos Asyncify ir buvo įdiegta maždaug tokia pati API Emterpreter, tik reikėjo jas prijungti).

Šiuo metu dar nespėjau suskaidyti kodo į vieną sukompiliuotą WASM ir interpretuotą naudojant Emterpreter, todėl blokuoti įrenginiai dar neveikia (žr. kitoje serijoje, kaip sakoma...). Tai yra, galų gale turėtumėte gauti kažką panašaus į šį juokingą sluoksniuotą dalyką:

  • interpretuojamas blokas I/O. Na, ar tikrai tikėjotės emuliuotos NVMe su vietiniu našumu? 🙂
  • statiškai sukompiliuotas pagrindinis QEMU kodas (vertėjas, kiti emuliuojami įrenginiai ir kt.)
  • dinamiškai sukompiliuotas svečio kodas į WASM

QEMU šaltinių ypatybės

Kaip tikriausiai jau atspėjote, svečių architektūrų emuliavimo kodas ir pagrindinio kompiuterio instrukcijų generavimo kodas yra atskirti QEMU. Tiesą sakant, tai netgi šiek tiek sudėtingesnė:

  • yra svečių architektūros
  • yra greitintuvai, ty KVM aparatinės įrangos virtualizavimui Linux sistemoje (suderinamoms svečių ir pagrindinio kompiuterio sistemoms), TCG JIT kodo generavimui bet kur. Pradedant nuo QEMU 2.9, Windows sistemoje atsirado aparatinės įrangos virtualizacijos standarto HAXM palaikymas (Informacija)
  • jei naudojamas TCG, o ne aparatinės įrangos virtualizavimas, tada jis turi atskirą kodo generavimo palaikymą kiekvienai pagrindinio kompiuterio architektūrai, taip pat universaliam interpretatoriui
  • ... ir aplink visa tai - emuliuoti periferiniai įrenginiai, vartotojo sąsaja, perkėlimas, įrašų atkūrimas ir kt.

Beje, ar žinojote: QEMU gali emuliuoti ne tik visą kompiuterį, bet ir procesorių atskiram vartotojo procesui pagrindinio kompiuterio branduolyje, kurį, pavyzdžiui, naudoja AFL fuzzer dvejetainiams instrumentams. Galbūt kas nors norėtų šį QEMU veikimo režimą perkelti į JS? 😉

Kaip ir dauguma seniai veikiančios nemokamos programinės įrangos, QEMU sukurta skambinant configure и make. Tarkime, kad nusprendėte ką nors pridėti: TCG užpakalinę programą, gijos diegimą ar dar ką nors. Neskubėkite džiaugtis / pasibaisėti (pabraukite, jei reikia) dėl galimybės bendrauti su Autoconf – iš tikrųjų, configure QEMU's, matyt, yra parašytas savarankiškai ir nėra sukurtas iš nieko.

WebAssembly

Taigi, kas yra šis dalykas, vadinamas WebAssembly (dar žinomas kaip WASM)? Tai yra Asm.js pakaitalas, nebepretenduojantis į galiojantį „JavaScript“ kodą. Priešingai, jis yra grynai dvejetainis ir optimizuotas, ir net tiesiog į jį įrašyti sveikąjį skaičių nėra labai paprasta: kompaktiškumo dėlei jis saugomas formatu LEB128.

Galbūt esate girdėję apie Asm.js pakartotinio kilpos keitimo algoritmą – tai „aukšto lygio“ srauto valdymo instrukcijų (tai yra, jei-tai-kitu, kilpų ir t. t.), kurioms sukurti JS varikliai, atkūrimas. žemo lygio LLVM IR, arčiau procesoriaus vykdomo mašinos kodo. Natūralu, kad tarpinis QEMU atstovavimas yra artimesnis antrajam. Atrodytų, štai, baitkodas, kankinimo pabaiga... O dar blokai, jei-tai-kitaip ir kilpos!..

Ir tai yra dar viena priežastis, kodėl „Binaryen“ yra naudinga: ji natūraliai gali priimti aukšto lygio blokus, artimus tam, kas būtų saugoma WASM. Tačiau jis taip pat gali sukurti kodą iš pagrindinių blokų ir perėjimų tarp jų grafiko. Na, aš jau sakiau, kad jis slepia WebAssembly saugojimo formatą už patogios C/C++ API.

TCG (Tiny Code Generator)

TCG iš pradžių buvo C kompiliatoriaus backend. Tada, matyt, jis neatlaikė konkurencijos su GCC, bet galiausiai rado savo vietą QEMU kaip kodo generavimo mechanizmas pagrindinei platformai. Taip pat yra TCG backend, kuris generuoja tam tikrą abstraktų baitinį kodą, kurį iškart paleidžia interpretatorius, bet nusprendžiau šį kartą jo nenaudoti. Tačiau faktas, kad QEMU jau galima per funkciją įjungti perėjimą prie sugeneruotos TB tcg_qemu_tb_exec, man tai pasirodė labai naudinga.

Norėdami pridėti naują TCG užpakalinę programą prie QEMU, turite sukurti pakatalogį tcg/<имя архитектуры> (tokiu atveju, tcg/binaryen) ir jame yra du failai: tcg-target.h и tcg-target.inc.c и paskirti viskas apie configure. Ten galite įdėti kitus failus, bet, kaip galite atspėti iš šių dviejų pavadinimų, jie abu bus kažkur įtraukti: vienas kaip įprastas antraštės failas (jis yra įtrauktas į tcg/tcg.h, o tas jau yra kituose katalogų failuose tcg, accel ir ne tik), kitas - tik kaip kodo fragmentas tcg/tcg.c, bet jis turi prieigą prie savo statinių funkcijų.

Nusprendęs, kad skirsiu per daug laiko detaliems tyrimams, kaip tai veikia, tiesiog nukopijavau šių dviejų failų „skeletus“ iš kitos užpakalinės programos diegimo, sąžiningai tai nurodydamas licencijos antraštėje.

byla tcg-target.h formoje daugiausia yra nustatymų #define-s:

  • kiek registrų ir kokio pločio yra tikslinėje architektūroje (turime kiek norime, kiek norime - klausimas labiau apie tai, ką naršyklė sugeneruos į efektyvesnį kodą „visiškai tikslinėje“ architektūroje ...)
  • pagrindinio kompiuterio instrukcijų derinimas: x86 ir net TCI instrukcijos visai nesulyginamos, bet į kodo buferį dėsiu visai ne instrukcijas, o rodykles į Binaryen bibliotekos struktūras, todėl pasakysiu: 4 baitų
  • kokias pasirinktines instrukcijas gali generuoti backend - įtraukiame viską, ką randame Binaryen, tegul greitintuvas pats suskaido likusias į paprastesnes
  • Koks yra apytikslis TLB talpyklos dydis, kurio prašo užpakalinės programos. Faktas yra tas, kad QEMU viskas rimta: nors yra pagalbinių funkcijų, kurios atlieka įkėlimą / saugojimą atsižvelgdami į svečio MMU (kur mes dabar būtume be jo?), jie išsaugo savo vertimo talpyklą struktūros pavidalu, kurių apdorojimą patogu įterpti tiesiai į transliacijos blokus. Kyla klausimas, kokį poslinkį šioje struktūroje efektyviausiai apdoroja nedidelė ir greita komandų seka?
  • čia galite pakoreguoti vieno ar dviejų rezervuotų registrų paskirtį, įgalinti iškviesti TB per funkciją ir pasirinktinai apibūdinti keletą mažų inline- veikia kaip flush_icache_range (bet tai ne mūsų atvejis)

byla tcg-target.inc.c, žinoma, paprastai yra daug didesnio dydžio ir turi keletą privalomų funkcijų:

  • inicijavimas, įskaitant apribojimus, kurios instrukcijos gali veikti su kokiais operandais. Akivaizdžiai nukopijavau iš kitos užpakalinės programos
  • funkcija, kuri paima vieną vidinį baito kodo nurodymą
  • Čia taip pat galite įdėti pagalbines funkcijas, taip pat galite naudoti statines funkcijas iš tcg/tcg.c

Sau pasirinkau tokią strategiją: pirmuose kito vertimo bloko žodžiuose užrašiau keturias nuorodas: pradžios ženklą (tam tikra reikšmė šalia 0xFFFFFFFF, kuris nustatė dabartinę TB būseną), kontekstą, sugeneruotą modulį ir magišką skaičių derinimui. Iš pradžių buvo įdėtas ženklas 0xFFFFFFFF - nKur n - mažas teigiamas skaičius, ir kiekvieną kartą, kai jis buvo vykdomas per vertėją, jis padidėjo 1. Kai pasiekė 0xFFFFFFFE, įvyko kompiliavimas, modulis buvo įrašytas į funkcijų lentelę, importuotas į mažą "paleidimo priemonę", į kurią buvo vykdomas tcg_qemu_tb_exec, o modulis buvo pašalintas iš QEMU atminties.

Perfrazuojant klasiką: „Ramentai, kiek šiame skambesyje susipynė progerio širdžiai...“. Tačiau atmintis kažkur nutekėjo. Be to, tai buvo QEMU valdoma atmintis! Turėjau kodą, kuris, rašydamas kitą instrukciją (na, tai yra, rodyklę), ištrynė tą, kurios nuoroda buvo šioje vietoje anksčiau, bet tai nepadėjo. Tiesą sakant, paprasčiausiu atveju QEMU paleidžiant paskirsto atmintį ir ten įrašo sugeneruotą kodą. Kai baigiasi buferis, kodas išmetamas, o jo vietoje pradedamas rašyti kitas.

Išstudijavus kodą, supratau, kad triukas su magišku skaičiumi leido man nesužlugdyti krūvos sunaikinimo, atlaisvinant kažką negero neinicijuotame buferyje pirmą kartą. Bet kas perrašo buferį, kad vėliau apeitų mano funkciją? Kaip pataria „Emscripten“ kūrėjai, iškilus problemai, perkėliau gautą kodą atgal į savąją programą, nustačiau joje „Mozilla Record-Replay“... Apskritai, galiausiai supratau paprastą dalyką: kiekvienam blokui, a struct TranslationBlock su jo aprašymu. Atspėk, kur... Teisingai, prieš pat bloką tiesiai buferyje. Supratęs tai, nusprendžiau nebenaudoti ramentų (bent jau kai kurių), tiesiog išmečiau stebuklingą skaičių, o likusius žodžius perkėliau į struct TranslationBlock, sukuriant atskirai susietą sąrašą, kurį galima greitai pereiti, kai iš naujo nustatoma vertimo talpykla, ir atlaisvinti atminties.

Kai kurie ramentai lieka: pavyzdžiui, pažymėtos rodyklės kodo buferyje - kai kurios iš jų yra tiesiog BinaryenExpressionRef, tai yra, jie žiūri į išraiškas, kurias reikia tiesiškai įdėti į sugeneruotą pagrindinį bloką, dalis yra perėjimo tarp BB sąlyga, dalis yra kur eiti. Na jau yra paruošti blokeliai Relooper, kuriuos reikia jungti pagal sąlygas. Norint juos atskirti, daroma prielaida, kad jie visi yra sulygiuoti mažiausiai keturiais baitais, todėl etiketei galite saugiai naudoti mažiausiai reikšmingus du bitus, tik reikia nepamiršti jį pašalinti, jei reikia. Beje, tokios etiketės jau naudojamos QEMU, nurodant išėjimo iš TCG kilpos priežastį.

Naudojant Binaryen

„WebAssembly“ moduliuose yra funkcijų, kurių kiekvienoje yra kūnas, kuris yra išraiška. Išraiškos yra vienanarės ir dvejetainės operacijos, blokai, susidedantys iš kitų išraiškų sąrašų, valdymo srauto ir kt. Kaip jau sakiau, valdymo srautas čia organizuojamas kaip aukšto lygio šakos, kilpos, funkcijų iškvietimai ir pan. Argumentai funkcijoms perduodami ne ant krūvos, o aiškiai, kaip ir JS. Taip pat yra globalių kintamųjų, bet aš jų nenaudojau, todėl apie juos nepasakosiu.

Funkcijos taip pat turi vietinius kintamuosius, sunumeruotus nuo nulio, tipo: int32 / int64 / float / double. Šiuo atveju pirmieji n vietinių kintamųjų yra funkcijai perduoti argumentai. Atkreipkite dėmesį, kad nors viskas čia nėra visiškai žemo lygio valdymo srauto požiūriu, sveikieji skaičiai vis tiek neturi atributo „pasirašytas / nepasirašytas“: kaip elgiasi skaičius, priklauso nuo operacijos kodo.

Paprastai kalbant, Binaryen numato paprastas C-API: sukuriate modulį, jame kurti išraiškas – unarinę, dvejetainę, blokus iš kitų išraiškų, valdyti srautą ir kt. Tada sukuriate funkciją, kurios turinys yra išraiška. Jei jūs, kaip ir aš, turite žemo lygio perėjimo grafiką, jums padės relooper komponentas. Kiek suprantu, bloke galima naudoti aukšto lygio vykdymo srauto valdymą, jei tik jis neperžengia bloko ribų - tai yra, galima padaryti vidinį greitą kelią / lėtą kelias šakojasi įtaisytosios TLB talpyklos apdorojimo kode, bet netrukdo „išoriniam“ valdymo srautui. Atlaisvinus relooper, atlaisvinami jo blokai, atlaisvinus modulį išnyksta jam skirtos išraiškos, funkcijos ir pan. arena.

Tačiau jei norite interpretuoti kodą skrydžio metu be nereikalingo vertėjo egzemplioriaus kūrimo ir trynimo, gali būti prasminga šią logiką įdėti į C++ failą ir iš ten tiesiogiai valdyti visą bibliotekos C++ API, apeinant paruoštą pagaminti įvyniojimai.

Taigi, norėdami sugeneruoti reikalingą kodą

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

... jei ką nors pamiršau, atsiprašau, tai tik skalei pavaizduoti, o išsami informacija pateikta dokumentacijoje.

Ir dabar prasideda crack-fex-pex, maždaug taip:

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

Norint kažkaip sujungti QEMU ir JS pasaulius ir tuo pačiu greitai pasiekti sukompiliuotas funkcijas, buvo sukurtas masyvas (funkcijų lentelė importavimui į paleidimo programą), o sugeneruotos funkcijos buvo patalpintos ten. Norint greitai apskaičiuoti indeksą, iš pradžių buvo naudojamas nulinio žodžio vertimo bloko indeksas, bet tada pagal šią formulę apskaičiuotas indeksas pradėjo tiesiog tilpti į lauką struct TranslationBlock.

Beje, Demo (šiuo metu su miglota licencija) gerai veikia tik Firefox. „Chrome“ kūrėjai buvo kažkaip nepasiruošęs į tai, kad kažkas norėtų sukurti daugiau nei tūkstantį WebAssembly modulių egzempliorių, todėl kiekvienam tiesiog skyrė po gigabaitą virtualios adresų erdvės...

Tai kol kas viskas. Galbūt bus dar vienas straipsnis, jei kam bus įdomu. Būtent, lieka bent tik kad blokiniai įrenginiai veiktų. Taip pat gali būti prasminga WebAssembly modulių kompiliavimą padaryti asinchroniniu, kaip įprasta JS pasaulyje, nes vis dar yra vertėjas, galintis visa tai padaryti, kol bus paruoštas vietinis modulis.

Pagaliau mįslė: sukompiliavote dvejetainį failą 32 bitų architektūroje, bet kodas, atliekant atminties operacijas, pakyla iš Binaryen, kažkur ant krūvos arba kažkur kitur viršutiniuose 2 GB 32 bitų adresų erdvės. Problema ta, kad Binaryeno požiūriu tai pasiekia per didelį gaunamą adresą. Kaip tai apeiti?

Administratoriaus būdu

Aš to neišbandžiau, bet pirmoji mintis buvo „O kas, jei įdiegčiau 32 bitų Linux? Tada viršutinę adreso erdvės dalį užims branduolys. Tik klausimas, kiek bus užimta: 1 ar 2 Gb.

Programuotojo būdu (pasirinkimas praktikams)

Išpūskime burbulą adreso erdvės viršuje. Aš pats nesuprantu, kodėl tai veikia - ten jau turi būti kaminas. Bet „mes esame praktikai: viskas mums tinka, bet niekas nežino kodėl...“

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

... tiesa, kad jis nesuderinamas su Valgrindu, bet, laimei, pats Valgrindas labai efektyviai visus iš ten išstumia :)

Galbūt kas nors geriau paaiškins, kaip veikia šis mano kodas...

Šaltinis: www.habr.com

Добавить комментарий