QEMU.js: tani seriozisht dhe me WASM

Njëherë e një kohë, për të bërë shaka, vendosa të dëmshpërblej procesin dhe të mësoj si të gjeneroj JavaScript (në të vërtetë, Asm.js) nga kodi makinerik. Për eksperimentin u zgjodh QEMU, një kohë më vonë u shkrua një artikull në Habr. Në komentet, dikush më këshilloi ta rimekëm projektin në WebAssembly, por ndihesha pak i paqartë për të hequr dorë nga projekti pothuajse i përfunduar siç është disi e vështirë… Puna po vazhdonte, por shumë ngadalë, dhe ja, së fundi, në atë artikull u shfaq koment një temë ‘Si përfundoi gjithçka?’. Në përgjigjen time të detajuar dëgjova ‘Kjo është diçka për një artikull’. Pra, nëse është kështu, do të jetë një artikull. Mund të ndihmojë dikë. Nga ai, lexuesi do të mësojë disa fakte rreth strukturës së backend-eve të kodit të gjenerimit të QEMU, si dhe si të shkruaj një kompjilator Just-in-Time për një aplikacion web.

Detyrat

Duke patur parasysh se ‘si si’ e kam mësuar të portohem QEMU në JavaScript, këtë herë u vendos që ta bëj me një plan dhe të mos përsëris gabimet e mia të kaluara.

Gabimi numër një: të devijoj nga versioni i lëshuar

Gabimi im i parë ishte të devijoja versionin tim nga versioni upstream 2.4.1. Atëherë më dukej se ishte një ide e mirë: nëse ekziston një version i lëshuar, atëherë ndoshta është më i qëndrueshëm se versioni i thjeshtë 2.4, e sidomos branches master. Dhe pasi planifikova të shtoj një numër të konsiderueshëm të bug-ve të mi, bug-ët e të tjerëve nuk më nevojiteshin aspak. Më duket se ashtu ndodhi. Por problemi ishte se: QEMU nuk qëndron në vend dhe në një moment madje shpallën një optimizim të kodit të gjeneruar për rreth 10%. ‘Ah, po ta bëj’ mendova unë dhe mbeta i befasuar. Këtu duhet bërë një këndvështrim: për shkak të natyrës njëkanale të QEMU.js dhe që origjinali QEMU nuk parashikon mungesë të shumëkanalësh (dmth. për të është kritike mundësia e punës së përbashkët të disa rrugëve të kodit, jo thjesht ‘përdor gjithçka’), funksionet kryesore të thesareve duhej ‘të lakoheshin’ për mundësinë e thirrjes nga jashtë. Kjo krijoi disa probleme natyrore në bashkim. Megjithatë, fakti që disa nga ndërrimet nga branchi master, me të cilin përpiqesha të bashkoja kodin tim, gjithashtu ishin cherry picked në versionin e lëshuar (ndonjëherë, edhe në branchin tim) ndoshta nuk do të ndihmonte shumë.

Në përgjithësi, vendosa që prototipi kishte kuptim të hiqej dhe të ndahej në pjesë për të ndërtuar një version të ri nga e para mbi bazën e diçkaje më të re dhe tani nga master.

Gabimi numër dy: metodologjia TLP

Në thelb, kjo nuk është një gabim, në përgjithësi — thjesht një veçori e krijimit të projektit në kushte të plota të paqartësisë si "ku dhe si të lëvizim?", ashtu si dhe "a do arrijmë?". Në këto kushte programimi i përcjellë ishte një mundësi e justifikuar, por, natyrisht, nuk doja ta përsërisja pa nevojë. Këtë herë doja ta bëja si duhet: komitete atomike, ndryshime të vetëdijshme të kodit (dhe jo "të lidhësh karaktere rastësore deri sa të kompilojë (me paralajmërime)", siç tha dikush ndonjëherë Linus Torvalds, nëse besojmë WikiQuote) etj.

Gabimi numër tre: të mos e dish lumit, të futesh në ujë

Për këtë arsye, dhe tani akoma nuk kam arritur ta eliminoj krejtësisht, por tani vendosa të mos shkoj në rrugën e kundërt të rezistencës, dhe ta bëj "si i rritur", konkretisht, të shkruaj backend-in tim TCG nga fillimi, për të mos thënë më pas, "Po, sigurisht, është ngadalë, por unë nuk mund ta kontrolloj çdo gjë - TCI është shkruar kështu...". Për më tepër, në fillim kjo dukeshin zgjidhje e qartë, për shkak se unë gjeneroj kod binar. Siç thone, "Mblodha Genti, por jo atë": kodi është, sigurisht, binar, por kontrolli mbi të nuk mund të kalojë lehtësisht - ai duhet të vendoset shprehimisht në shfletues për kompilim, duke marrë rezultatin nga një objekt nga bota e JS, që gjithashtu duhet të ruhet diku. Megjithatë, në arkitekturën normale RISC, sa kuptoj, një situatë tipike është nevoja për të hequr shprehimisht cache-in e instruksioneve për kodin e ri-gjeneruar - nëse kjo nuk është ajo që na nevojitet, për të paktën është afër. Për më tepër, nga përpjekja ime e kaluar kam mësuar që kontrolli në mes të bllokut të transformimit duket se nuk kalon, prandaj bytecode-i, i interpretuar nga çdo offset, nuk është veçanërisht i nevojshëm, dhe mund ta gjenerojmë thjesht sipas funksionit në TB.

E ardhëm dhe e goditëm

Megjithëse fillova të rishkruaj kodin që në korrik, por shtysa magjike erdhi papritmas: zakonisht email-et nga GitHub vijnë si njoftime për përgjigje në Issues dhe Pull requests, por këtu, papritur përmendje në thred Binaryen si një backend qemu në kontekstin, "Këtu ai bënte diçka të ngjashme, ndoshta do të thotë diçka". Fjalët ishin për përdorimin e bibliotekës së afërt me Emscripten, Binaryen për krijimin e WASM JIT. Atëherë thashë se licensa juaj është Apache 2.0, ndërsa QEMU si një tërësi shpërndahet nën GPLv2, dhe ato nuk janë shumë të përputhshme. Papritur doli se licensën mund të ndonjëherë ta rregullojmë (nuk e di: ndoshta ta ndryshoj, ndoshta do të ketë licencë të dyfishtë, ndoshta ndonjë gjë tjetër…). Kjo më gëzoi, sepse disa herë deri atëherë kam qenë duke parë formatin binar WebAssembly, dhe ishte disi e trishtueshme dhe e paqartë për mua. Por këtu kishte një bibliotekë, që do të përthithte blloqet bazë me grafikun e kalimeve, do të jepte kodin bajt dhe madje do ta ekzekutonte atë në interpreter, nëse nevojitet.

Më pas kishte një letër në listën e shpërndarjes QEMU, por kjo tashmë ishte për pyetjen, «A i duhet dikujt kjo?». Dhe, papritur, rezultoi se i duhej. Të paktën, mund të gjejmë disa mundësi përdorimi, nëse do të funksionojë mjaft shpejt:

  • ekzekutimi i ndonjë gjëje mësimore pa instaluar asgjë
  • virtualizimi në iOS, ku sipas thashethemeve, aplikacioni i vetëm që ka të drejtë të gjenerojë kod në fluks është motorri JS (a është e vërtetë këtë?)
  • demonstrimi i mini-OS — disqe të vetme, të integruara, të ndryshme firmware, etj…

Karakteristikat e mjedisit të shfletuesit

Siç e thashë, QEMU është i lidhur me multithreading, por në shfletues nuk ka. Domethënë, ashtu siç nuk ka… Në fillim nuk kishte fare, pas kësaj dolën WebWorkers — sa kuptoj, kjo është multithreading e bazuar në dërgimin e mesazheve pa variabël të përbashkëta. Natyrisht, kjo krijon probleme të konsiderueshme kur transferojmë kodin ekzistues që është i bazuar në modelin e memorie të përbashkët. Më pas, nën presionin e publikut, u implementua dhe ajo me emrin SharedArrayBuffers. Ajo u fut gradualisht, festuan nisjen e saj në shfletues të ndryshëm, pastaj festuan vitin e ri, dhe pastaj Meltdown… Pas kësaj erdhën në përfundimin se, pavarësisht se sa e lartë është matja e kohës, përmes memories së përbashkët dhe një fluksi që inkrementon numëruesin, gjithsesi do të arrijë mjaft saktësi. Kështu që e çaktivizuan multithreading me memorie të përbashkët. Duket se e aktivizuan përsëri më vonë, por, siç u kuptua nga eksperimenti i parë, dhe pa të është jeta, dhe kështu, do të provojmë ta bëjmë atë, pa u mbështetur në multithreading.

Veçoria e dytë është se manipulimet e nivelit të ulët me grumbullin janë të pamundura: nuk mund të ndalosh thjesht, të ruash kontekstin aktual dhe të kalosh në një të ri me një grumbull të ri. Grumbulli i thirrjeve menaxhohet nga mašina virtuale e JS. Si duket, çfarë problemi ka, pasi që ne tashmë vendosëm të menaxhojmë me rrjedhat e mëparshme plotësisht manualisht? Problemi është se input-output i bllokut në QEMU është realizuar përmes korutinave, dhe këtu do na duheshin manipulimet e nivelit të ulët me grumbullin. Fatmirësisht, Emscripten tashmë përmban një mekanizëm për operacione asinkrone, madje dy: Asyncify dhe Emterpreter. I pari punon përmes fryrjes së konsiderueshme të kodit JavaScript të gjeneruar dhe nuk mbështetet më. I dyti është ‘ mënyra e duhur’ aktuale dhe punon përmes gjenerimit të kodit bajt për interpretorin e tij. Punon, natyrisht, ngadalë, por nuk e fryn kodin. Megjithatë, mbështetje për korutinat për këtë mekanizëm duhej të kontribuonte vetë (aty ishin tashmë korutina të shkruara për Asyncify dhe kishte një implementim më shumë ose më pak të të njëjtit API për Emterpreter, thjesht duhej t'i lidhim ato).

Derisa, aktualisht nuk kam arritur ende të ndaj kodin në atë që do të kompiloj në WASM dhe atë që do të interpretohet përmes Emterpreter, kështu që pajisjet bllok do të funksionojnë ende (shihni episodet e ardhshme, siç thuhet…). Kështu, në fund, duhet të rezultojë diçka kaq e çuditshme:

  • input-output bllok asinkron. Po, a e prisni vërtet një NVMe të emuluar me performancë native? 🙂
  • Kodi kryesor i QEMU i kompiluar statikisht (përkthyesi, pajisjet e tjera të emuluara, etj.)
  • Kodi nikoqir i kompiluar dinamikisht në WASM

Veçoritë e burimeve QEMU

Siç e keni kuptuar, kodi për emulimin e arkitekturave nikoqire dhe kodi për gjenerimin e instrukcioneve makinerike nikoqire në QEMU janë të ndara. Në të vërtetë, aty është edhe një pak më e komplikuar:

  • ekzistojnë arkitektura nikoqire
  • është akseleratorë, domethënë, KVM për virtualizimin harduerik në Linux (për sisteme nikoqire dhe harduer që janë kompatibile), TCG për gjenerimin e kodit JIT kudo. Duke filluar nga QEMU 2.9 u shfaq mbështetje për standardin e virtualizimit harduerik HAXM në Windows (detaje)
  • nëse përdoret TCG dhe jo virtualizimi harduerik, atëherë ai ka mbështetje të veçantë për gjenerimin e kodit për çdo arkitekturë pritëse, si dhe për interpretuesin univers.
  • … dhe rreth të gjithë kësaj — periferi e emuluar, ndërfaqe përdoruesi, migrim, rekord-ripërsëritje, etj.

Për shembull, a e dini: QEMU mund të emullojë jo vetëm një kompjuter të tërë, por edhe procesorin për një proces të veçantë përdoruesi në bërthamën pritëse, gjë që përdoret, për shembull, nga fuzzers AFL për instrumentimin e binarëve. Ndoshta ndokush do të dëshirojë ta portonte këtë mod QEMU në JS? 😉

Si shumica e programeve të lira që janë ekzistuar prej kohësh, QEMU ndërtoset përmes thirrjes konfiguro. dhe make. Supozoni se keni vendosur të shtoni diçka: backend TCG, një implementim të lëshimeve, diçka tjetër. Mos u ngazëlle dhe mos u trembni (nënvizoni që është e nevojshme) nga perspektiva e komunikimit me Autoconf — në të vërtetë, konfiguro. QEMU, duket se, ka një gjenerim të vet, dhe nuk krijohet nga asgjë.

WebAssembly

Pra, çfarë është ky term — WebAssembly (WASM)? Kjo është një zëvendësim për Asm.js, tani nuk pretendon më të jetë kod i vlefshëm JavaScript. Përkundrazi, është plotësisht binar dhe i optimizuar, madje edhe thjesht të shkruash një numër të plotë atje nuk është aq e lehtë: ai ruhet në format LEB128.

Ndoshta keni dëgjuar për algoritmin e ripërsëritjes për Asm.js — kjo është rikthimi i instrukcioneve "të nivelit të lartë" për kontrollimin e rrjedhës së ekzekutimit (domethënë if-then-else, ciklet etj.), të cilat janë të përshtatura për motorët JS, nga LLVM IR të nivelit të ulët, më afër kodit të makinës që ekzekutohet nga procesori. Natyrisht, përfaqësimi ndërmjet QEMU është më afër të dytit. Duket se, ja, kod i byte, fundi i mundimeve… Dhe pastaj blloqet, if-then-else dhe ciklet!..

Dhe këtu është edhe një arsye pse Binaryen është i dobishëm: ai, natyrisht, mund të pranojë blloqe të nivelit të lartë, afër asaj që do të ruhet në WASM. Por gjithashtu ai mund të japë kod nga grafi i blloqeve themelore dhe kalimeve mes tyre. E për më tepër, se çfarë fsheh ai prapa API-t të rehatshëm C/C++ të formatit të ruajtjes së WebAssembly, e kam thënë tashmë.

TCG (Tiny Code Generator)

TCG fillimisht ishte prapa C. Më pas, duket se nuk e përballoi konkurrencën me GCC, por përfundimisht gjeti vendin e tij si një mekanizëm gjenerimi kodi në QEMU për platformën pritëse. Ka edhe një backend TCG, që gjeneron një kod të caktuar të abstrakt, të cilin e ekzekuton interpreti, por për këtë rast vendosa të shmang përdorimin e tij. Megjithatë, fakti që QEMU tashmë ka mundësinë për të përfshirë kalimin në TB të gjeneruar përmes funksionit tcg_qemu_tb_exec, ishte shumë e dobishme për mua.

Për të shtuar një backend të ri TCG në QEMU, duhet të krijoni një nën目录 tcg/ (në këtë rast, tcg/binaryen), dhe brenda saj dy skedarë: tcg-target.h dhe tcg-target.inc.c dhe duhet të specifikoni të gjithë këto në konfiguro.. Mund të vendosni aty edhe skedare të tjera, por, siç mund të kuptohet nga emrat e këtyre dy, të dyja do të përfshihen diku: një si një skedar i zakonshëm header (ai përfshihet në tcg/tcg.h, dhe ai tashmë në skedare të tjera në direktoriumet tcg, accel dhe jo vetëm), i dyti — vetëm si snippet kodi në tcg/tcg.c, por ai ka akses në funksionet e tij statike.

Duke vendosur se do të bëja shumë kohë për të hetuar hollësisht se si është e organizuar, thjesht kopjova «skeletet» e këtyre dy skedarëve nga një implementim tjetër të backend, duke e shënuar këtë ndershmërisht në titullin e licencës.

Skeda tcg-target.h përmban kryesisht konfigurime në formën e #define-eve:

  • sa regjistra dhe cila gjerësi ka arkitektura e synuar (ne kemi - sa të duam, është më shumë çështje se çfarë do të gjenerohet në një kod më efikas nga shfletuesi në arkitekturën 'plotësisht të synuar'...)
  • riformatimi i instruksioneve pritëse: në x86, dhe në TCI, instruksionet në përgjithësi nuk riformatizohen, unë kam ndërmend të vendos në tamponin e kodit dhe jo instruksione, por tregues të strukturave të bibliotekës Binaryen, prandaj do të them: 4 byte
  • cilat instruksione opsionale mund të gjenerojë backend — përfshijmë gjithçka që gjejmë në Binaryen, pjesa tjetër le të thyer në metoda më të thjeshta nga akseleratori.
  • Cili është përafërsisht madhësia e caches TLB që kërkon backend. Çështja është se në QEMU gjithçka është serioze: ndonëse ka funksione ndihmëse që realizojnë ngarkim/skarkim duke marrë parasysh MMU-në e mysafirit (ku do të shkonin tani pa të?), por caches e tyre të përkthimit ruhet si një strukture, të cilën e është e lehtë ta integrosh drejtpërdrejt në bloket e përkthimit. Pyetja është se cila shpërndarje në këtë strukturë përcaktohet më efikas nga një sekuencë e vogël dhe të shpejtë komandash.
  • Këtu mund të rregullosh caktimin e një ose dy regjistrave rezervuar, të aktivizosh thirrjen TB përmes funksionit dhe opcionalisht të përshkruash disa të vogla. inline-funksione si flush_icache_range (por kjo nuk është rasti ynë)

Skeda tcg-target.inc.c, natyrisht, zakonisht është shumë më e madhe në madhësi dhe përmban disa funksione të detyrueshme:

  • init, që gjithëashtu tregon kufizimet se cila instruktion mund të funksionojë me cilat operanda. E kam kopjuar pa turp nga një backend tjetër.
  • funksioni që pranon një instruktion të kodit të brendshëm
  • këtu gjithashtu mund të vendosësh funksione ndihmëse, si dhe mund të përdorësh funksione statike nga tcg/tcg.c

Për vete kam zgjedhur strategjinë e mëposhtme: në fjalët e para të çdo blloku të ri të përkthimit, shkruaja katër tregues: etiketën e fillimit (një vlerë e caktuar brenda 0xFFFFFFFF, me të cilën përcaktohej gjendja aktuale TB), konteksti, moduli i gjeneruar, dhe numri magic për debugging. Fillimisht, etiketës i caktohej 0xFFFFFFFF - nindex n — një numër i vogël pozitiv, dhe me çdo ekzekutim përmes interpreterit rritej me 1. Kur arrinte në 0xFFFFFFFE, ndodhte kompaktimi, moduli ruhej në tabelën e funksioneve, e cila ishte importuar në një «startues» të vogël, në të cilin ndodhej ekzekutimi nga tcg_qemu_tb_exec, dhe moduli hiqej nga memoria QEMU.

Duke e riformuluar klasiken, "Nikë, sa shumë në këtë tingull për zemrën e programuesit është lidhur...". Megjithatë, memoria po zhdukej diku. Për më tepër, kjo ishte memoria e menaxhuar nga QEMU! Kisha një kod që, kur shkruaja çdo instruktion të ardhshme (po, pra, treguesin), hiqte atë që kishte një referencë në këtë vend më parë, por kjo nuk ndihmonte. Në fakt, në rastin më të thjeshtë, QEMU ndan kujtesë në fillim dhe shkruan atje kodin e gjeneruar. Kur buffer-i përfundon, kodi hidhet, dhe në vendin e tij fillon të shkruhet i ardhshmi.

Duke studiuar kodin, kuptova se zgjidhja me numrin magjik ndihmonte që të mos shkelesh në shkatërrimin e grumbullit, duke lirohesh ndonjë gjë të pavendosur në një tampon të pa inicializuar gjatë kalimit të parë. Por kush e riparon tamponin duke anashkaluar funksionin tim më pas? Siç sugjerojnë zhvilluesit e Emscripten, përballë problemit, e kam portuar kodin e fituar përsëri në aplikacionin native, duke e testuar me Mozilla Record-Replay… Në përgjithësi, në fund kuptova një gjë të thjeshtë: për çdo bllok ndahen strukturë TranslationBlock , me përshkrimin e tij. Ditet njohin se ku… E siç duhet, pikërisht përpara bllokut direkt në tampon. Pasi kuptova këtë, vendosa të heq dorë nga zgjidhjet improvizuese (të paktën disa), dhe thjesht e hodha numrin magjik, duke i transferuar fjalët e mbetura në strukturë TranslationBlock, duke krijuar një listë të lidhjeve të njëanshme, përmes së cilës mund të kalojmë shpejt kur dëshirojmë të çlirojmë cache-in e përkthimit dhe të lironi memorie.

Disa zgjidhje improvisuese mbetën: për shembull, tregues të shënuar në tamponin e kodit — disa nga ato janë thjesht BinaryenExpressionRef, që do të thotë se shikojnë në shprehjet që duhet të vendosen linear në bllokun bazik të gjeneruar, disa — kushti i kalimit midis BB, disa — ku të kalojmë. Po ashtu, ekzistojnë blloqe të përgatitura për Relooper, të cilat duhet të lidhen sipas kushteve. Për t'i dalluar ato, përdoret supozimi se të gjithë janë të rreshtuar të paktën në katër byte, kështu që mund të përdoren qetësisht dy bitët më të ulët për etiketën, vetëm mos e harro të heqësh atë kur është e nevojshme. Për më tepër, këto etiketa përdoren tashmë në QEMU për të shënuar arsyen e daljes nga cikli TCG.

Përdorimi i Binaryen

Modulet në WebAssembly përmbajnë funksione, secila prej të cilave ka një trup që përbën një shprehje. Shprehjet janë operacione unare dhe binare, blloqe të përbërë nga lista të shprehjeve të tjera, rrjedha kontrolli etj. Siç kam thënë, rrjedha e kontrollit këtu organizohet si degëzime dhe cikle me nivel të lartë, thirrje funksionesh, etj. Argumentet kalohen në funksione jo nga staku, por shprehimisht, si në JS. Ka edhe variabla globale, por unë nuk i kam përdorur, prandaj nuk do të flas për to.

Po funksionet ka variabla lokale numëruar nga zero, me tipin: int32 / int64 / float / double. Rëndësi ka se variablat e parë n lokale janë argumentet e kaluara në funksion. Vini re se, megjithëse këtu është pak më e avancuar në aspektin e rrjedhës së kontrollit, numrat e tërë nuk kanë shenjën "nënshkruar / pa nënshkrim": si do të sillet numri varet nga kodi i operacionit.

Në përgjithësi, Binaryen ofron një API të thjeshtë C: krijoni një modulk, në të krijoni shprehje - unare, binare, blloqe nga shprehi të tjera, rrjedhë kontrolli etj. Më pas krijoni një funksion, në trupin e të cilit duhet të jepni shprehjen. Nëse keni një grafik të ulët të kalimeve - një komponent i quajtur relooper ju ndihmon. Sa kuptoj, mund të përdorni menaxhimin e lartë të rrjedhës së ekzekutimit brenda bllokut përderisa nuk del jashtë bllokut - do të thotë se mund të bëni ndarjen e brendshme fast path / slow path brenda kodit të integruar për menaxhimin e ketës TLB, por nuk mund të ndërhyjë në rrjedhën e kontrollit "jashtë". Kur çliroheni nga relooper, çlirohen blloqet e tij, kur çliroheni nga moduli - zhduken shprehjet, funksionet etj., të cilat janë alokuar në arenën e tij..

Megjithatë, nëse dëshironi të interpretoni kodin në lëvizje pa krijime dhe fshirje të panevojshme të instancave të interpretorit, ka kuptim ta nxirrni këtë logjikë në një skedë C++, dhe prej andej të menaxhoni drejtpërdrejt të gjithë API-në C++ të bibliotekës, duke kaluar përmes mbështetjeve të gatshme.

Pra, për të gjeneruar kod, është e nevojshme

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

… nëse ndonjë gjë është harruar - më falni, është thjesht që të paraqisni shkallët, dhe detajet - ato janë në dokumentacion.

Tani fillon kriks-feks-peks, diçka e tillë:

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,
          // ...
      }
  );
  // dhe tani keni instancën tuaj!
}, buf, sz);

Për të lidhur ndonjëherë botën QEMU me JS dhe për të hyrë shpejt në funksionet e kompiluar, u krijua një varg (tabela e funksioneve për import në ekzekutues), dhe aty u vendosën funksionet e gjeneruara. Për të llogaritur shpejt indeksin, si të tillë, u përdor fillimisht indeksi i fjalës zero të bllokut të përkthimit, por më vonë indeksi i llogaritur sipas një formule të tillë u shkruajt thjesht në fushë. strukturë TranslationBlock.

Për më tepër, demo (deri tani me një licencë të paqartë) punon normalisht vetëm në Firefox. Zhvilluesit e Chrome ishin në një farë mënyre të papërgatitur për atë që dikush do të donte të krijonte më shumë se një mijë instance të moduleve WebAssembly, prandaj thjesht ndanë një gigabajt hapësirë adresimi virtuale për çdo…

Deri tani, kjo është e gjitha. Ndoshta do të ketë një artikull tjetër, nëse ndokush është i interesuar. Do të thotë se mbetet të paktën vetëm të bëni që pajisjet bllokuese të funksionojnë. Ndoshta ka kuptim gjithashtu të bëni kompilimin e moduleve WebAssembly asinkron, siç është e zakonshme në botën e JS, që pasi gjithsesi ka një interpretor që mund ta kryejë gjithçka, derisa moduli natyror të jetë gati.

Në fund, një enigmë: keni ndërtuar një binar në një arkitekturë 32-bit, por kodi përmes operacioneve me kujtesë del nga Binaryen, ndonjëherë në stack ose diku tjetër në 2 GB e sipërme të hapësirës adresuese 32-bit. Problemi është se nga këndvështrimi i Binaryen, ky është një akses në një adresë rezultuese shumë të madhe. Si ta anashkaloni këtë?

Administrativisht

Këtë në fund nuk e testova, por mendimi i parë ishte "Çfarë nëse vendos një Linux 32-bit?" Pastaj pjesa e sipërme e hapësirës adresuese do të jetë e zënë nga bërthama. Pyetja është vetëm se sa do të jetë e zënë: 1 apo 2 Gb.

Për programuesit (një version për praktikanët)

Do të frymëzojmë një balonë në pjesën e sipërme të hapësirës adresuese. Vetë nuk e kuptoj pse funksionon — aty duhet të jetë tashmë stack-u. Por “ne jemi praktikanë: gjithçka funksionon për ne, por askush nuk e di pse…”.

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

… me Valgrind-in, për më tepër, nuk është e përputhshme, por, fatmirësisht, Valgrind vetë nxjerr shumë efikasitet nga aty 🙂

Ndoshta dikush do të japë një shpjegim më të mirë se si funksionon ky kodi im…

Burimi: habr.com

Blini hosting të besueshëm për faqe interneti me mbrojtje nga DDoS, serverë VPS VDS 🔥 Blini hosting të besueshëm për faqe interneti me mbrojtje nga DDoS, serverë VPS VDS | ProHoster