QEMU.js: tani serioze dhe me WASM

Njëherë e një kohë vendosa për qejf vërtetojnë kthyeshmërinë e procesit dhe mësoni se si të gjeneroni JavaScript (më saktë, Asm.js) nga kodi i makinës. QEMU u zgjodh për eksperiment, dhe disa kohë më vonë u shkrua një artikull në Habr. Në komentet më këshilluan të ribëja projektin në WebAssembly dhe madje të largohesha vetë pothuajse mbarova Disi nuk e desha projektin... Puna po vazhdonte, por shumë ngadalë, dhe tani, së fundi në atë artikull u shfaq комментарий me temën "Pra, si përfundoi gjithçka?" Në përgjigje të përgjigjes sime të hollësishme, dëgjova "Kjo tingëllon si një artikull". Epo, nëse mundeni, do të ketë një artikull. Ndoshta dikush do ta ketë të dobishme. Prej tij lexuesi do të mësojë disa fakte rreth dizajnit të backend-eve të gjenerimit të kodit QEMU, si dhe se si të shkruhet një përpilues Just-in-Time për një aplikacion ueb.

detyrat

Meqenëse kisha mësuar tashmë se si të "portoja disi" QEMU në JavaScript, këtë herë u vendos ta bëja me mençuri dhe të mos përsërisja gabimet e vjetra.

Gabimi numër një: degëzim nga pika e lëshimit

Gabimi im i parë ishte të shkëputa versionin tim nga versioni 2.4.1 në rrjedhën e sipërme. Atëherë më dukej një ide e mirë: nëse ekziston lirimi i pikës, atëherë ndoshta është më i qëndrueshëm se 2.4 i thjeshtë, dhe aq më tepër dega master. Dhe meqenëse kam planifikuar të shtoj një sasi të mjaftueshme të defekteve të mia, nuk më duheshin fare të askujt tjetër. Ndoshta kështu doli. Por këtu është gjëja: QEMU nuk qëndron ende, dhe në një moment ata madje njoftuan optimizimin e kodit të gjeneruar me 10 për qind. "Po, tani do të ngrij," mendova dhe u prisha. Këtu duhet të bëjmë një digresion: për shkak të natyrës me një fije të vetme të QEMU.js dhe faktit që QEMU origjinale nuk nënkupton mungesën e multi-threading (d.m.th., aftësinë për të operuar në të njëjtën kohë disa shtigje kodi të palidhura, dhe jo vetëm "përdorni të gjitha kernelët") është kritike për të, funksionet kryesore të thread-eve më duhej t'i "zbuloja" për të qenë në gjendje të thërrisja nga jashtë. Kjo krijoi disa probleme natyrore gjatë bashkimit. Megjithatë, fakti që disa nga ndryshimet nga dega master, me të cilin u përpoqa të bashkoja kodin tim, u zgjodhën gjithashtu në lëshimin e pikës (dhe për rrjedhojë në degën time) gjithashtu ndoshta nuk do të kishte shtuar lehtësi.

Në përgjithësi, vendosa që ka ende kuptim të hedh prototipin, ta çmontoj për pjesë dhe të ndërtoj një version të ri nga e para bazuar në diçka më të freskët dhe tani nga master.

Gabimi numër dy: Metodologjia e TLP

Në thelb, ky nuk është një gabim, në përgjithësi, është vetëm një veçori e krijimit të një projekti në kushtet e keqkuptimit të plotë si "ku dhe si të lëvizim?" dhe në përgjithësi "a do të arrijmë atje?" Në këto kushte programim i ngathët ishte një opsion i justifikuar, por, natyrisht, nuk doja ta përsërisja pa nevojë. Këtë herë doja ta bëja me mençuri: angazhimet atomike, ndryshimet e kodit të vetëdijshëm (dhe jo "lidhja e karaktereve të rastësishme së bashku derisa të përpilohet (me paralajmërime)", siç tha një herë Linus Torvalds për dikë, sipas Wikiquote), etj.

Gabimi numër tre: futja në ujë pa e ditur Ford-in

Unë ende nuk e kam hequr qafe plotësisht këtë, por tani kam vendosur të mos ndjek fare rrugën e rezistencës më të vogël dhe ta bëj atë "si i rritur", domethënë, të shkruaj nga e para fundin tim të TCG, në mënyrë që të mos për të thënë më vonë, "Po, kjo është sigurisht, ngadalë, por unë nuk mund të kontrolloj gjithçka - kështu është shkruar TCI..." Për më tepër, kjo fillimisht dukej si një zgjidhje e qartë, pasi Unë gjeneroj kodin binar. Siç thonë ata, “Gent u mblodhу, por jo ai”: kodi është, natyrisht, binar, por kontrolli nuk mund të transferohet thjesht tek ai - ai duhet të futet në mënyrë eksplicite në shfletues për përpilim, duke rezultuar në një objekt të caktuar nga bota JS, i cili ende duhet të të ruhen diku. Sidoqoftë, në arkitekturat normale RISC, me sa kuptoj unë, një situatë tipike është nevoja për të rivendosur në mënyrë eksplicite cache-in e udhëzimeve për kodin e rigjeneruar - nëse kjo nuk është ajo që na nevojitet, atëherë, në çdo rast, është afër. Përveç kësaj, nga përpjekja ime e fundit, mësova se kontrolli nuk duket se është transferuar në mes të bllokut të përkthimit, kështu që ne nuk kemi nevojë realisht për bajtkod të interpretuar nga ndonjë kompensim, dhe ne thjesht mund ta gjenerojmë atë nga funksioni në TB .

Erdhën dhe shkelmuan

Edhe pse fillova të rishkruaj kodin në korrik, një goditje magjike doli pa u vënë re: zakonisht letrat nga GitHub mbërrijnë si njoftime për përgjigjet ndaj çështjeve dhe kërkesave të tërheqjes, por këtu, papritur përmend në temë Binaryen si një prapambetje qemu në kontekst, "Ai bëri diçka të tillë, ndoshta ai do të thotë diçka." Ne po flisnim për përdorimin e bibliotekës përkatëse të Emscripten Binarjen për të krijuar WASM JIT. Epo, thashë që ju keni një licencë Apache 2.0 atje, dhe QEMU në tërësi shpërndahet nën GPLv2, dhe ato nuk janë shumë të përputhshme. Papritur doli se një licencë mund të jetë rregulloje disi (Nuk e di: ndoshta ta ndryshoj, ndoshta licencim të dyfishtë, ndoshta diçka tjetër...). Kjo, natyrisht, më bëri të lumtur, sepse në atë kohë tashmë e kisha parë nga afër format binar WebAssembly, dhe isha disi e trishtuar dhe e pakuptueshme. Kishte gjithashtu një bibliotekë që do të gllabëronte blloqet bazë me grafikun e tranzicionit, do të prodhonte bajtkodin dhe madje do ta ekzekutonte në vetë interpretuesin, nëse ishte e nevojshme.

Pastaj kishte më shumë një letër në listën e postimeve të QEMU, por kjo ka të bëjë më shumë me pyetjen, "Kujt i duhet gjithsesi?" Dhe eshte papritur, doli se ishte e nevojshme. Në minimum, ju mund të gërvishtni së bashku mundësi të tilla përdorimi nëse funksionon pak a shumë shpejt:

  • lançimi i diçkaje edukative pa asnjë instalim fare
  • virtualizimi në iOS, ku, sipas thashethemeve, i vetmi aplikacion që ka të drejtën e gjenerimit të kodit në fluturim është një motor JS (a është e vërtetë kjo?)
  • demonstrim i mini-OS - një floppy, i integruar, të gjitha llojet e firmware, etj...

Karakteristikat e kohës së funksionimit të shfletuesit

Siç thashë tashmë, QEMU është i lidhur me multithreading, por shfletuesi nuk e ka atë. Epo, domethënë jo... Në fillim nuk ekzistonte fare, më pas u shfaqën WebWorkers - me sa kuptoj unë, kjo është multithreading bazuar në transmetimin e mesazhit pa variabla të përbashkët. Natyrisht, kjo krijon probleme të rëndësishme kur transferon kodin ekzistues bazuar në modelin e kujtesës së përbashkët. Më pas, nën presionin e publikut, u zbatua edhe me emër SharedArrayBuffers. U prezantua gradualisht, ata festuan fillimin e saj në shfletues të ndryshëm, më pas festuan Vitin e Ri dhe më pas Meltdown... Pas së cilës arritën në përfundimin se matja e kohës është e trashë ose e trashë, por me ndihmën e kujtesës së përbashkët dhe një fije duke rritur numëruesin, është e gjitha njësoj do të funksionojë mjaft saktë. Kështu që ne çaktivizuam multithreading me memorie të përbashkët. Duket se ata më vonë e kthyen atë përsëri, por, siç u bë e qartë nga eksperimenti i parë, ka jetë pa të, dhe nëse po, ne do të përpiqemi ta bëjmë atë pa u mbështetur në multithreading.

Karakteristika e dytë është pamundësia e manipulimeve të nivelit të ulët me pirgun: thjesht nuk mund të marrësh, të ruash kontekstin aktual dhe të kalosh në një të ri me një pirg të ri. Stacki i thirrjeve menaxhohet nga makina virtuale JS. Do të duket, cili është problemi, pasi ne ende vendosëm të menaxhojmë rrjedhat e mëparshme plotësisht manualisht? Fakti është se blloku I/O në QEMU zbatohet përmes korutinave, dhe këtu do të ishin të dobishëm manipulimet e stakut të nivelit të ulët. Për fat të mirë, Emscipten tashmë përmban një mekanizëm për operacione asinkrone, madje dy: Asincifikoj и Emterpretues. E para funksionon përmes fryrjes së konsiderueshme në kodin e gjeneruar të JavaScript dhe nuk mbështetet më. E dyta është "mënyra e saktë" aktuale dhe funksionon përmes gjenerimit të bytekodit për interpretuesin vendas. Ajo funksionon, natyrisht, ngadalë, por nuk e fryn kodin. Vërtetë, mbështetja për korutinat për këtë mekanizëm duhej të kontribuohej në mënyrë të pavarur (tashmë kishte korutina të shkruara për Asyncify dhe kishte një zbatim afërsisht të njëjtë API për Emterpreter, thjesht duhej t'i lidhje ato).

Për momentin, nuk kam arritur ende të ndaj kodin në një të përpiluar në WASM dhe të interpretuar duke përdorur Emterpreter, kështu që pajisjet e bllokut nuk funksionojnë ende (shiko në serinë tjetër, siç thonë ata ...). Kjo do të thotë, në fund duhet të merrni diçka si kjo gjë qesharake me shtresa:

  • blloku i interpretuar I/O. Epo, a e prisnit vërtet NVMe të emuluar me performancë vendase? 🙂
  • Kodi kryesor QEMU i përpiluar në mënyrë statike (përkthyesi, pajisje të tjera të emuluara, etj.)
  • kodi i vizitorit i përpiluar në mënyrë dinamike në WASM

Karakteristikat e burimeve QEMU

Siç ndoshta e keni menduar tashmë, kodi për emulimin e arkitekturave të mysafirëve dhe kodi për gjenerimin e udhëzimeve të makinës pritëse janë të ndara në QEMU. Në fakt, është edhe pak më e ndërlikuar:

  • ka arkitektura të ftuar
  • ka përshpejtuesit, domethënë, KVM për virtualizimin e harduerit në Linux (për sistemet e vizitorëve dhe pritës të pajtueshëm me njëri-tjetrin), TCG për gjenerimin e kodit JIT kudo. Duke filluar me QEMU 2.9, u shfaq mbështetja për standardin e virtualizimit të harduerit HAXM në Windows (Detalet)
  • nëse përdoret TCG dhe jo virtualizimi i harduerit, atëherë ai ka mbështetje të veçantë të gjenerimit të kodit për secilën arkitekturë të hostit, si dhe për interpretuesin universal
  • ... dhe rreth gjithë kësaj - periferikë të emuluar, ndërfaqja e përdoruesit, migrimi, riprodhimi i regjistrimit, etj.

Meqë ra fjala, a e dinit: QEMU mund të imitojë jo vetëm të gjithë kompjuterin, por edhe procesorin për një proces të veçantë përdoruesi në kernelin pritës, i cili përdoret, për shembull, nga fuzzeri AFL për instrumente binar. Ndoshta dikush do të donte ta transferonte këtë mënyrë funksionimi të QEMU në JS? 😉

Ashtu si shumica e softuerëve të lirë afatgjatë, QEMU ndërtohet përmes thirrjes configure и make. Le të themi se keni vendosur të shtoni diçka: një backend TCG, zbatimi i fijeve, diçka tjetër. Mos nxitoni të jeni të lumtur/të tmerruar (nënvizoni sipas rastit) në mundësinë e komunikimit me Autoconf - në fakt, configure QEMU është me sa duket i shkruar vetë dhe nuk është krijuar nga asgjë.

WebAssembly

Pra, çfarë quhet kjo gjë që quhet WebAssembly (aka WASM)? Ky është një zëvendësim për Asm.js, duke mos pretenduar më të jetë kod i vlefshëm JavaScript. Përkundrazi, është thjesht binar dhe i optimizuar, dhe madje edhe thjesht shkrimi i një numri të plotë në të nuk është shumë i thjeshtë: për kompaktësi, ai ruhet në format LEB128.

Ju mund të keni dëgjuar për algoritmin e riciklimit për Asm.js - ky është rivendosja e udhëzimeve të kontrollit të rrjedhës "të nivelit të lartë" (d.m.th., nëse-atëherë-tjetër, sythe, etj.), për të cilat motorët JS janë projektuar, nga LLVM IR e nivelit të ulët, më afër kodit të makinës të ekzekutuar nga procesori. Natyrisht, përfaqësimi i ndërmjetëm i QEMU është më afër të dytës. Duket se këtu është, bytekodi, fundi i mundimit... Dhe pastaj ka blloqe, nëse-atëherë-tjetër dhe sythe!..

Dhe kjo është një arsye tjetër pse Binaryen është i dobishëm: ai mund të pranojë natyrshëm blloqe të nivelit të lartë afër asaj që do të ruhet në WASM. Por gjithashtu mund të prodhojë kod nga një grafik i blloqeve bazë dhe kalimet midis tyre. Epo, unë kam thënë tashmë se ai fsheh formatin e ruajtjes WebAssembly pas API-së së përshtatshme C/C++.

TCG (Gjenerator i kodit të vogël)

GTC ishte fillimisht backend për kompajlerin C. Më pas, me sa duket, nuk mundi t'i rezistonte konkurrencës me GCC, por në fund gjeti vendin e tij në QEMU si një mekanizëm gjenerimi i kodit për platformën pritës. Ekziston gjithashtu një backend TCG që gjeneron disa bytekod abstrakt, i cili ekzekutohet menjëherë nga përkthyesi, por vendosa të shmang përdorimin e tij këtë herë. Megjithatë, fakti që në QEMU tashmë është e mundur të mundësohet kalimi në TB të gjeneruar përmes funksionit tcg_qemu_tb_exec, doli të ishte shumë e dobishme për mua.

Për të shtuar një backend të ri TCG në QEMU, duhet të krijoni një nëndrejtori tcg/<имя архитектуры> (në këtë rast, tcg/binaryen), dhe përmban dy skedarë: tcg-target.h и tcg-target.inc.c и përshkruajnë eshte e gjitha per configure. Ju mund të vendosni skedarë të tjerë atje, por, siç mund ta merrni me mend nga emrat e këtyre dyve, ata të dy do të përfshihen diku: njëri si skedar i zakonshëm i kokës (është i përfshirë në tcg/tcg.h, dhe ai është tashmë në skedarë të tjerë në drejtori tcg, accel dhe jo vetëm), tjetra - vetëm si një copë kodi në tcg/tcg.c, por ka akses në funksionet e tij statike.

Duke vendosur që do të shpenzoja shumë kohë në hetime të hollësishme se si funksionon, thjesht kopjova "skeletet" e këtyre dy skedarëve nga një zbatim tjetër i backend-it, duke e treguar sinqerisht këtë në kokën e licencës.

skedar tcg-target.h përmban kryesisht cilësime në formë #define-s:

  • sa regjistra dhe çfarë gjerësi ka në arkitekturën e synuar (ne kemi aq sa duam, aq sa duam - pyetja është më shumë rreth asaj se çfarë do të gjenerohet në kod më efikas nga shfletuesi në arkitekturën "plotësisht objektiv" ...)
  • rreshtimi i udhëzimeve të hostit: në x86, dhe madje edhe në TCI, udhëzimet nuk janë fare të rreshtuara, por unë do të vendos në buferin e kodit jo fare udhëzime, por tregues për strukturat e bibliotekës Binaryen, kështu që do të them: 4 byte
  • çfarë udhëzimesh opsionale mund të gjenerojë backend - ne përfshijmë gjithçka që gjejmë në Binaryen, lëreni përshpejtuesin të ndajë pjesën tjetër në më të thjeshta vetë
  • Cila është madhësia e përafërt e cache TLB e kërkuar nga backend. Fakti është se në QEMU gjithçka është serioze: megjithëse ka funksione ndihmëse që kryejnë ngarkim/magazinim duke marrë parasysh MMU-në e ftuar (ku do të ishim pa të tani?), ata ruajnë cache-in e tyre të përkthimit në formën e një strukture, përpunimi i të cilave është i përshtatshëm për t'u futur drejtpërdrejt në blloqet e transmetimit. Pyetja është se çfarë kompensimi në këtë strukturë përpunohet në mënyrë më efikase nga një sekuencë e vogël dhe e shpejtë komandash?
  • këtu mund të ndryshoni qëllimin e një ose dy regjistrave të rezervuar, të mundësoni thirrjen e TB përmes një funksioni dhe opsionalisht të përshkruani disa të vogla inline-funksionon si flush_icache_range (por ky nuk është rasti ynë)

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

  • inicializimi, duke përfshirë kufizimet se cilat instruksione mund të funksionojnë në cilët operandë. E kopjuar në mënyrë flagrante nga unë nga një fund tjetër
  • funksion që merr një instruksion të brendshëm të bajtkodit
  • Ju gjithashtu mund të vendosni funksione ndihmëse këtu, dhe gjithashtu mund të përdorni funksione statike nga tcg/tcg.c

Për veten time, zgjodha strategjinë e mëposhtme: në fjalët e para të bllokut të ardhshëm të përkthimit, shënova katër tregues: një shenjë fillimi (një vlerë e caktuar në afërsi 0xFFFFFFFF, i cili përcaktoi gjendjen aktuale të TB), kontekstin, modulin e krijuar dhe numrin magjik për korrigjimin e gabimeve. Në fillim u vendos shenja 0xFFFFFFFF - nKu n - një numër i vogël pozitiv dhe sa herë që ekzekutohej përmes përkthyesit rritej me 1. Kur arrinte 0xFFFFFFFE, u bë përpilimi, moduli u ruajt në tabelën e funksioneve, u importua në një "nisues" të vogël, në të cilin shkoi ekzekutimi nga tcg_qemu_tb_exec, dhe moduli u hoq nga memoria QEMU.

Për të perifrazuar klasikët, "Paterica, sa është e ndërthurur në këtë tingull për zemrën e progerit...". Megjithatë, kujtesa diku po rridhte. Për më tepër, memoria menaxhohej nga QEMU! Unë kisha një kod që, kur shkruaja udhëzimin tjetër (mirë, domethënë një tregues), fshiva atë, lidhja e të cilit ishte në këtë vend më herët, por kjo nuk ndihmoi. Në fakt, në rastin më të thjeshtë, QEMU shpërndan memorie në fillim dhe shkruan kodin e krijuar atje. Kur buferi mbaron, kodi hidhet jashtë dhe në vend të tij fillon të shkruhet tjetri.

Pasi studiova kodin, kuptova se mashtrimi me numrin magjik më lejoi të mos dështoja në shkatërrimin e grumbullit duke çliruar diçka të gabuar në një tampon të painitializuar në kalimin e parë. Por kush e rishkruan bufferin për të anashkaluar funksionin tim më vonë? Siç këshillojnë zhvilluesit e Emscripten, kur hasa në një problem, e transferova kodin që rezulton përsëri në aplikacionin vendas, vendosa Mozilla Record-Replay në të... Në përgjithësi, në fund kuptova një gjë të thjeshtë: për çdo bllok, a struct TranslationBlock me përshkrimin e tij. Merreni me mend se ku... Kjo është e drejtë, pak para bllokut pikërisht në tampon. Duke e kuptuar këtë, vendosa të heq dorë nga përdorimi i patericave (të paktën disa), dhe thjesht hodha numrin magjik dhe transferova fjalët e mbetura në struct TranslationBlock, duke krijuar një listë të vetme të lidhur që mund të përshkohet shpejt kur cache e përkthimit rivendoset dhe të lirohet memoria.

Disa paterica mbeten: për shembull, tregues të shënuar në buferin e kodit - disa prej tyre janë thjesht BinaryenExpressionRef, domethënë, ata shikojnë shprehjet që duhet të vendosen në mënyrë lineare në bllokun bazë të gjeneruar, një pjesë është kushti për kalimin midis BB-ve, pjesa është ku duhet shkuar. Epo, tashmë ka blloqe të përgatitura për Relooper që duhet të lidhen sipas kushteve. Për t'i dalluar ato, përdoret supozimi se të gjithë janë të lidhur me të paktën katër bajtë, kështu që mund të përdorni me siguri dy bitet më pak të rëndësishme për etiketën, thjesht duhet të mbani mend ta hiqni atë nëse është e nevojshme. Nga rruga, etiketa të tilla përdoren tashmë në QEMU për të treguar arsyen e daljes nga laku TCG.

Duke përdorur Binaryen

Modulet në WebAssembly përmbajnë funksione, secila prej të cilave përmban një trup, i cili është një shprehje. Shprehjet janë operacione unare dhe binare, blloqe që përbëhen nga lista të shprehjeve të tjera, rrjedha e kontrollit, etj. Siç thashë tashmë, fluksi i kontrollit këtu është i organizuar pikërisht si degë të nivelit të lartë, unaza, thirrje funksionesh, etj. Argumentet e funksioneve nuk kalohen në stack, por në mënyrë eksplicite, ashtu si në JS. Ka edhe variabla globale, por unë nuk i kam përdorur ato, kështu që nuk do t'ju tregoj për to.

Funksionet kanë gjithashtu variabla lokale, të numëruara nga zero, të tipit: int32 / int64 / float / double. Në këtë rast, n variablat e parë lokale janë argumentet që i kalohen funksionit. Ju lutemi vini re se megjithëse gjithçka këtu nuk është plotësisht e nivelit të ulët për sa i përket fluksit të kontrollit, numrat e plotë ende nuk mbajnë atributin "i nënshkruar/i panënshkruar": mënyra se si sillet numri varet nga kodi i funksionimit.

Në përgjithësi, Binaryen ofron C-API e thjeshtë: ju krijoni një modul, në të krijoni shprehje - unare, binare, blloqe nga shprehjet e tjera, kontrolloni rrjedhën, etj. Pastaj krijoni një funksion me një shprehje si trup të tij. Nëse ju, si unë, keni një grafik tranzicioni të nivelit të ulët, komponenti relooper do t'ju ndihmojë. Me sa kuptoj unë, është e mundur të përdoret kontrolli i nivelit të lartë të rrjedhës së ekzekutimit në një bllok, për sa kohë që nuk shkon përtej kufijve të bllokut - domethënë, është e mundur të bëhet një rrugë e brendshme e shpejtë / e ngadaltë degëzimi i rrugës brenda kodit të përpunimit të memories së integruar TLB, por jo për të ndërhyrë në rrjedhën e kontrollit "të jashtëm". Kur lironi një relooper, blloqet e tij lirohen; kur lironi një modul, shprehjet, funksionet, etj. që i janë caktuar atij zhduken. arenën.

Sidoqoftë, nëse doni të interpretoni kodin në fluturim pa krijimin dhe fshirjen e panevojshme të një shembulli përkthyes, mund të ketë kuptim ta vendosni këtë logjikë në një skedar C++ dhe prej andej të menaxhoni drejtpërdrejt të gjithë API-në C++ të bibliotekës, duke anashkaluar gati- bërë mbështjellës.

Pra, për të gjeneruar kodin që ju nevojitet

// настроить глобальные параметры (можно поменять потом)
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 kam harruar ndonjë gjë, më falni, kjo është vetëm për të paraqitur shkallën, dhe detajet janë në dokumentacion.

Dhe tani fillon crack-fex-pex, diçka si kjo:

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

Për të lidhur disi botët e QEMU dhe JS dhe në të njëjtën kohë për të hyrë shpejt në funksionet e përpiluara, u krijua një grup (një tabelë funksionesh për import në lëshues) dhe funksionet e gjeneruara u vendosën atje. Për të llogaritur shpejt indeksin, indeksi i bllokut të përkthimit të fjalëve zero u përdor fillimisht si ai, por më pas indeksi i llogaritur duke përdorur këtë formulë filloi thjesht të përshtatet në fushën në struct TranslationBlock.

Rastësisht, demonstrim (aktualisht me një licencë të errët) funksionon mirë vetëm në Firefox. Zhvilluesit e Chrome ishin disi jo gati për faktin se dikush do të dëshironte të krijonte më shumë se një mijë shembuj të moduleve WebAssembly, kështu që ata thjesht ndanë një gigabajt hapësirë ​​​​për adresa virtuale për secilin...

Kjo është e gjitha për tani. Ndoshta do të ketë një artikull tjetër nëse dikush është i interesuar. Gjegjësisht, ka mbetur të paktën vetëm bëjnë që pajisjet e bllokut të funksionojnë. Mund të ketë gjithashtu kuptim që përpilimi i moduleve WebAssembly të bëhet asinkron, siç është zakon në botën JS, pasi ekziston ende një përkthyes që mund t'i bëjë të gjitha këto derisa moduli vendas të jetë gati.

Më në fund një gjëegjëzë: ju keni përpiluar një binar në një arkitekturë 32-bitësh, por kodi, nëpërmjet operacioneve të memories, ngjitet nga Binaryen, diku në stack, ose diku tjetër në 2 GB të sipërme të hapësirës së adresave 32-bit. Problemi është se nga këndvështrimi i Binaryen, kjo është aksesi në një adresë shumë të madhe rezultuese. Si ta kapërceni këtë?

Në mënyrën e administratorit

Nuk përfundova duke e testuar këtë, por mendimi im i parë ishte "Po sikur të instaloja Linux 32-bit?" Pastaj pjesa e sipërme e hapësirës së adresave do të zëhet nga kerneli. Pyetja e vetme është se sa do të zënë: 1 ose 2 Gb.

Në mënyrën e një programuesi (opsion për praktikuesit)

Le të fryjmë një flluskë në krye të hapësirës së adresave. Unë vetë nuk e kuptoj pse funksionon - atje tashmë duhet të ketë një pirg. Por "ne jemi praktikues: 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));
}

... është e vërtetë që nuk është në përputhje me Valgrind, por, për fat të mirë, vetë Valgrind i shtyn në mënyrë shumë efektive të gjithë nga atje :)

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

Burimi: www.habr.com

Shto një koment