QEMU.js: tani seriozisht dhe me WASM

Njëherë, për të qeshur, vendosa të provonte përmbysjen e procesit dhe të mësoja si të gjeneroja JavaScript (në mënyrë më të saktë, Asm.js) nga kodi makinor. Për eksperimentin u zgjodh QEMU, dhe pas pak kohësh u shkrua një artikull në Habr. Në komentet, më këshilluan ta rindërtoja projektin në WebAssembly, por më vetë nuk më përshkonte të heqja dorë nga gati i përfunduar projekti… Puna po vazhdonte, por shumë ngadalë, dhe së fundmi, në atë artikull u shfaq komentar në temën «Dhe si përfundoi gjithçka?». Përgjigjes time të detajuar, dëgjova «Kjo kërkon një artikull». Pra, nëse kërkon, do të ketë artikull. Ndoshta, do t'i shërbejë ndokujt. Nga ai, lexuesi do të mësojë disa fakte rreth ndërtimit të backend-eve të kodigjenerimit të QEMU, si dhe se si të shkruash një kompjilator Just-in-Time për aplikacionin e uebit.

Detyrat

Duke qenë se «si dhe si» kam mësuar ta portoja QEMU në JavaScript, kësaj here vendosa ta bëj me shpirt dhe të mos përsëris gabimet e vjetra.

Gabimi numër një: të devijoja nga versioni point release

Gabimi im i parë ishte të devijoja versionin tim nga versioni upstream 2.4.1. Atëhere më dukej si një ide e mirë: nëse point release ekziston, atëherë ndoshta është më i qëndrueshëm se thjesht 2.4, dhe aq më pak nga dega master. Dhe duke qenë se unë planifikoja të shtoja një numër të konsiderueshëm të gabimeve të mia, atëherë gabimet e të tjerëve nuk ishin aspak të nevojshme. Ashtu, ndoshta, ndodhi. Por problemin e kam: QEMU nuk qëndron vendi, dhe në një moment madje njoftuan optimizimin e kodit të gjeneruar për 10%. "Ah, tani do e realizoj" mendoja, dhe u zhgënjeva. Këtu duhet bërë një ndalesë: për shkak të natyrës njëpërmasa të QEMU.js dhe thatë që QEMU origjinal nuk parashikon mungesën e shumëpërmasave (domethënë, për të është kritike mundësia e punës së disa rrugëve të kodeve të padëshiruara njëkohësisht, jo thjesht "përdor të gjithë bërthamat"), funksionet kryesore të projekteve duhej "të përftoheshin" për mundësinë e thirrjes nga jashtë. Kjo krijoi disa probleme natyrore gjatë bashkimit. Megjithatë, fakti që disa ndryshime nga dega, me të cilën përpiqesha të bashkoja kodin tim, gjithashtu u cherry picked në versionin e pikës (dhe kështu, edhe në degën time) gjithashtu, ndoshta, nuk do të shtonte lehtësi. master, e cila po përpiqesha të bashkoja kodin tim, gjithashtu u cherry picked në versionin e pikës (dhe kështu, edhe në degën time) gjithashtu, ndoshta, nuk do të shtonte lehtësi.

Në përgjithësi, vendosa se prapë prototipi ka kuptim të hidhet për të u ndarë në pjesë dhe të ndërtohet një version i ri nga e para mbi bazën e diçkaje më të re dhe tani vërtetë nga master.

Gabimi i dytë: metodologjia TLP

Për të qenë të saktë, kjo nuk është një gabim, thjesht një veçori e krijimit të projektit në kushte të plota të paqartësive si "ku dhe si të lëvizim?", dhe gjithashtu "a do të arrijmë?". Në këto kushte programim i bëshëm ishte një variant i arsyeshëm, por, natyrisht, nuk doja ta përsërisja pa nevojë. Këtë herë doja të bëja më me kuptim: komitë atomike, ndryshime të vetëdijshme të kodit (jo "të lidhim karaktere rastësore derisa të kompilojë (me paralajmërime)", siç ka thënë dikush njëherë Linus Torvalds, nëse e beson Wikipedia) etj.

Gabimi numër tre: të hysh në ujë pa e ditur se ku është rrjedha

Për këtë nuk jam liruar plotësisht edhe tani, por tani kam vendosur të ec në një drejtim më të arsyeshëm, duke krijuar backend-in tim TCG nga fillimi, për të mos thënë më vonë, "Po, sigurisht, është e ngadalshme, por nuk mund ta kontrolloj gjithçka — TCI është shkruar kështu...". Për më tepër, fillimisht kjo dukej si një zgjidhje e qartë, pasi unë gjeneroj kod binar. Siç thotë një shprehje, "Grumbulluar Gentu, por jo që nuk e kalon: kodi është, sigurisht, binar, por kontrolli mbi të nuk mund të transferohet thjesht - ai duhet të injektohet qartë në shfletues për përpunim, duke rezultuar në një objekt nga bota e JS, i cili gjithashtu duhet të ruhet diku. Megjithatë, në arkitekturën tipike RISC, sa kam kuptuar, një situatë tipike është nevoja për të pastruar ndjeshëm cache-në e instrukcioneve për kodin e ripërgatitur - nëse kjo nuk është pikërisht ajo që na nevojitet, në çdo rast, është e afërt. Për më tepër, nga përpjekja ime e kaluar kam mësuar se kontrolli në mes të bllokut të përkthimit duket se nuk kalon, prandaj bytecode, i interpretuar nga çdo offset, nuk na nevojitet dhe mund të gjenerojmë thjesht sipas funksionit në TB.

E dërguan dhe e goditën

Megjithëse fillova të rishkruaj kodin që në korrik, por goditja magjike erdhi papritur: zakonisht, e-mailet nga GitHub vijnë si njoftime për përgjigjet në Issues dhe Pull requests, por këtu, papritur përmendje në thredhë Binaryen si një backend qemu në kontekstin, «Ja, ai bënte diçka të ngjashme, ndoshta do të thotë diçka». Flitej për përdorimin e një biblioteke të ngjashme me Emscripten Binaryen për krijimin e WASM JIT. E thashë që keni licencën Apache 2.0, ndërsa QEMU shpërndahet si një person i vetëm nën GPLv2, dhe ato nuk janë shumë të përputhshme. Papritur u zbulua se mund të ndryshohet ndonjëherë (nuk e di: ndoshta, të ndërrohet, ndoshta, licencim të dyfishtë, ndoshta, diçka tjetër…). Kjo, natyrisht, më gëzoi, sepse deri atëherë e kisha vënë re disa herë formatin binar WebAssembly, dhe më dukej disi e trishtueshme dhe e paqartë. Këtu kishte një bibliotekë që do të përpunonte dhe blloqet bazë me diagramin e kalimeve, do të jepte kodin e bajtëve dhe madje do ta ekzekutonte në interpretuesin, nëse ishte e nevojshme.

Pastaj kishte një letër në listën e shpërndarjes QEMU, por kjo tashmë ishte më shumë në pyetje: "Po për kë do të duhej përfundimisht?". Dhe u zbuluar se papritur, kishte ndonjë interes. Të paktën, mund të gjejmë mundësi të tilla për ta përdorur, nëse do të funksiononte me atë që janë;

  • ekzekutimi i diçkaje mësimore pa ndonjë instalim
  • virtualizimi në iOS, ku për të dëgjuar thashetheme, aplikacioni i vetëm që ka të drejtë të krijojë kod në fluks është motorri JS (a është vërtet kështu?)
  • demostrimi i mini-OS — disqe një disku, të ndërmjetme, firmware të ndryshme, etj…

Karakteristikat e ambientit të ekzekutimit në shfletues

Siç e thashë më parë, QEMU është i lidhur me multithreading, por në shfletues nuk ka. Pra, në fakt, si nuk ka... Fillimisht nuk kishte fare, pastaj u shfaqën WebWorkers — sa kam kuptuar, është multithreading i bazuar në komunikimin e mesazheve pa variabla të përbashkët. Natyrisht, kjo krijon probleme të konsiderueshme kurportoni kodin ekzistues të bazuar në modelin e memories së përbashkët. Më pas, nën presionin e opinionit publik, u implementua dhe ajo me emrin SharedArrayBuffers. Ajo u fut gradualisht, e festuam lansimin e saj në shfletues të ndryshëm, pastaj festuam vitin e ri, dhe më pas Meltdown... Pas kësaj, arritëm në përfundimin se, pavarësisht se si e mat kohën, me ndihmën e memories së përbashkët dhe një rrjedhe që inkrementon numëruesin, prapë do të arrijmë mjaft saktë. Kështu që e çaktivizuam multithreading me memorinë e përbashkët. Duket se më vonë e filluan përsëri, por siç kuptuam nga eksperimentin e parë, edhe pa të, jeta vazhdon, dhe nëse është kështu, do të përpiqemi të bëjmë pa mbështetje për multithreading.

Karakteristika e dytë është se manovrimi i nivelit të ulët me stack-un është i pamundur: nuk mund të marrësh thjesht kontekstin aktual, ta ruash dhe të kalosh në një të ri me një stack të ri. Stack-u i thirrjeve menaxhohet nga makina virtuale JS. Duke e parë kështu, çfarë problemi ka, që ne tashmë kemi vendosur të menaxhojmë plotësisht me rrjedhat e mëparshme manualisht? Problemi është se I/O bllokues në QEMU realizohet përmes korutinave, dhe këtu do na duhej manovrimi i nivelit të ulët me stack-un. Fatmirësisht, Emscripten tashmë përmban një mekanizëm për operacione asinkrone, madje dy: Asyncify dhe Emterpreter. E para funksionon përmes një fryrjeje të konsiderueshme të kodit JavaScript të gjeneruar dhe nuk mbështetet më. E dyta është mënyra e tanishme ‘e saktë’ dhe funksionon përmes gjenerimit të kodit të bytes për interpretorin e vet. Funksionon, sigurisht, ngadalë, por nuk fryn kodin. E vërteta është se mbështetja për korutina për këtë mekanizëm duhej të kontribuonte vetë (ato ishin tashmë korutina të shkruara për Asyncify dhe kishte një implementim të ngjashëm me API-në për Emterpreter, thjesht duhej t'i lidhim ato).

Aktualisht, nuk kam arritur ende të ndaj kodin në atë që mund të kompilojë në WASM dhe atë që interpretohet me Emterpreter, prandaj pajisjet bllok janë ende jo funksionale (shihni në episodet e ardhshme, siç thuhet…). Pra, përfundimisht, duhet të rezultojë një gjë e tillë e çuditshme e shtresuar:

  • input-output bllokesh të interpretuar. Pra, ju vërtet prisnit një NVMe të emuluar me performancë natyrale? 🙂
  • kodin kryesor QEMU të kompiluar statikisht (translatori, pajisjet e tjera të emuluara etj.)
  • kodin e mysafirëve të kompiluar dinamika në WASM

Veçoritë e burimeve QEMU

Siç mund ta keni kuptuar, kodi i emulimit të arkitekturave të mysafirëve dhe kodi i gjenerimit të instrukcioneve të makinës për mikpritësit është ndarë në QEMU. Në fakt, ka një shtesë tjetër interesante:

  • ka arkitektura mysafirësh
  • ka akceleratorë, konkretisht, KVM për virtualizim harduerik në Linux (për sisteme të mysafirëve dhe mikpritësve që janë të përshtatshme me njëra-tjetrën), TCG për gjenerimin e kodi JIT kudo. Duke filluar nga QEMU 2.9, u shtua mbështetje për standardin e virtualizimit harduerik HAXM në Windows (detaje)
  • nëse përdoret TCG dhe jo virtualizim hardueri, atëherë ka mbështetje të veçantë për gjenerimin e kodit për secilën arkitekturë hosti, si dhe për interpretorin universal
  • … dhe rreth gjithë kësaj—për periferikët që emulohen, ndërfaqen e përdoruesit, migrimin, regjistrimin-riprodhimin, etj.

Për shembull, a e dini: QEMU mund të emulojë jo vetëm një kompjuter tërësisht, por edhe një procesor për një proces të veçantë përdoruesi në bërthamën e hostit, gjë që përdoret, për shembull, nga fuzzers si AFL për instrumentimin e binarëve. Ndoshta dikush do të dëshirojë ta portojë këtë mënyrë funksionimi të QEMU në JS? 😉

Si shumica e programeve të lira që eksistojnë për një kohë të gjatë, QEMU ndërlidhet përmes thirrjes konfiguro dhe make. Supozoni se keni vendosur të shtoni diçka: backend TCG, implementimin e fijeve, diçka tjetër. Mos u nxito të gëzohesh/zhgënjehesh (theksoni të nevojshmen) nga perspektiva e komunikimit me Autoconf — në të vërtetë, konfiguro QEMU, gjithsesi, ka dukshëm kod të shkruar vetë dhe asgjë nuk ndodhet në gjenerim.

WebAssembly

Çfarë është pra kjo gjë — WebAssembly (ose WASM)? Ai është zëvendësimi i Asm.js, tani nuk pretendon më të jetë kod valid JavaScript. Në të vërtetë, ai është krejtësisht binar dhe i optimizuar, dhe madje, thjesht shkruar një numër të plotë në të nuk është ashtu edhe e thjeshtë: ai ruhet në një format LEB128.

Ndoshta keni dëgjuar për algoritmin relooping për Asm.js — kjo është rikuperimi i instruksioneve të kontrollit të rrjedhës së ekzekutimit "të nivelit të lartë" (pra if-then-else, ciklet etj.), për të cilat motorët JS janë të optimizuar, nga LLVM IR më të ulët, më afër kodit të makinës që ekzekutohet nga procesori. Natyrisht, përfaqësimi ndërmjetës QEMU është më afër të dytit. Duke dukur, ja ai, bytecode, fundi i mundimeve... Dhe pastaj blloqet, if-then-else dhe ciklet!..

Dhe kjo është një tjetër arsye pse Binaryen është i dobishëm: ai natyrisht mund të pranojë blloqe të nivelit të lartë, të ngjashme me atë që do të ruhet në WASM. Por gjithashtu ai mund të japë kod nga grafi i blloqeve bazë dhe kalimeve midis tyre. E po, për atë që fsheh pas një API të rehatshëm C/C++ formatin e ruajtjes së WebAssembly, e thashë tashmë.

TCG (Tiny Code Generator)

TCG fillimisht ishte backend për kompilatorin C. Më pas, duket se nuk e përballoi dot konkurrencën me GCC, por përfundimisht gjeti vendin e tij si pjesë e QEMU si mekanizmi i gjenerimit të kodit për platformën pritëse. Po ashtu ekziston një backend TCG që gjeneron një kod të abstraktë bajtësh, i cili menjëherë ekzekutohet nga interpreti, por për herën e parë vendosa të largohem nga përdorimi i tij. Megjithatë, fakti që në QEMU është tashmë mundësia për të përfshirë kalimin në TB të gjeneruar përmes funksionit tcg_qemu_tb_exec, më erdhi shumë në dorë.

Për të shtuar një backend të ri TCG në QEMU, duhet të krijosh një nënfoldër tcg/ (në këtë rast, tcg/binaryen), dhe brenda saj dy skedarë: tcg-target.h dhe tcg-target.inc.c dhe duhet të shkruash të gjitha këto në konfiguro. Mund ta vendosësh edhe skedarë të tjerë aty, por, siç mund ta kuptosh nga emrat e këtyre dy skedarëve, të dyja do të përfshihen diku: njëri si një skedar i zakonshëm kokë (ai përfshihet në tcg/tcg.h, dhe ai tashmë në skedarë të tjerë në direktoriumet tcg, accel dhe jo vetëm), tjetri — vetëm si kod skica në tcg/tcg.c, por ai ka akses në funksionet e tij statike.

Duke që kam vendosur se do të harxhoj shumë kohë për të kuptuar në detaje si funksionin, thjesht kam kopjuar "skeletet" e këtyre dy skedarëve nga një implementim tjetër të backend-it, duke e theksuar këtë në titullin e licencës.

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

  • sa regjistra dhe cilën gjerësi kanë në arkitekturën e synuar (në rastin tonë — sa të duam, aq edhe kemi — çështja është më shumë se çfarë do të gjenerohet në një kod më efikas nga shfletuesi në arkitekturën "përmendore")
  • këndvështrimi i udhëzimeve host: në x86, dhe në TCI, udhëzimet nuk janë fare të alignuara, unë planifikoj të vendos në bufferin e kodit dhe jo udhëzimet fare, por tregues për strukturat e bibliotekës Binaryen, prandaj do të them: 4 byte
  • cilat udhëzime opsionale mund të gjenerojë backend — përfshijmë gjithçka që gjejmë në Binaryen, të tjerat le t'i thyejë accelerator-i në më të thjeshtat vetë
  • cili është përafërsisht madhësia e caches TLB që backend-i kërkon. Çështja është se në QEMU gjithçka është serioze: megjithëse ka funksione ndihmëse që realizojnë ngarkimin/ruajtjen duke marrë parasysh MMU-në e mysafirit (nuk mund të kalosh pa të tani?), cache-i i vet i përkthimit ruhet në formën e një strukture, përpunimi i së cilës përshtatet lehtësisht në bloket e përkthimit. Pyetja është se cili ofset në këtë strukturë përpunohen më efektivisht nga një seri urdhërash të vogla dhe të shpejta.
  • këtu gjithashtu mund të rregullohet caktimi i një ose dy regjistrave të rezervuar, të aktivizohet thirrja e TB-së përmes funksionit dhe opcionalisht të përshkruhet një çift i vogël. inline-funksione si flush_icache_range (por kjo nuk është rasti ynë)

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

  • kryesimi, që tregon përfshirë kufizimet mbi atë që instruksioni me operandet e tij mund të punojë. Është kopjuar furishëm nga një backend tjetër.
  • funksioni që pranon një instruksion të kodit të brendshëm
  • këtu gjithashtu mund të vendosen funksione ndihmëse, si dhe këtu mund të përdoren funksionet statike nga tcg/tcg.c

Unë kam zgjedhur strategjinë time të mëposhtme: në fjalët e para të çdo blloku të transmetimit, regjistrova katër tregues: shenjën e fillimit (një vlerë në rrethinë 0xFFFFFFFF, me të cilin përcaktohej gjendja aktuale e TB), konteksti, moduli i gjeneruar dhe numri magjik për debug. Fillimisht, shenja vendosej në 0xFFFFFFFF - n, ku n — një numër të vogël pozitiv, dhe me çdo ekzekutim përmes interpretuesit, rritej me 1. Kur arrinte në 0xFFFFFFFE, ndodhte kompilimi, moduli ruhej në tabelën e funksioneve, e importuar në një "startues" të vogël, në të cilin e drejtonte ekzekutimi nga tcg_qemu_tb_exec, dhe moduli hiqej nga memoria QEMU.

Për ta parafrazuar klasiken, "Kostili, sa shumë në këtë zë për zemrën e programuesit..." Megjithatë, kujtesa ishte duke u humbur diku. Dhe kjo ishte një kujtesë e menaxhuar nga QEMU! Kisha një kod që, teksa shkruante një instruktion tjetër ( pra, treguesin), fshinte atë që kishte qenë më parë në atë vend, por kjo nuk ndihmonte. Në të vërtetë, në rastin më të thjeshtë, QEMU rezervon kujtesën në fillim dhe shkruan kodin e gjeneruar aty. Kur buffer-i përfundon, kodi hidhet, dhe në vendin e tij fillon të shkruhet i ardhshmi.

Pas studimit të kodit, kuptova se kostili me numra magjikë e lejonte të mos bien shumë në shembjen e grumbullit, duke çliruar diçka jo të duhur në një buffer të pa inicializuar gjatë kalimit të parë. Por kush e ripërshkruan buffer-in oborr në mënyrë anashkalueshëm? Siç rekomandojnë zhvilluesit e Emscripten, përballë një problemi, kam portuar kodin e krijuar prapa në një aplikacion natyror, e kam drejtuar atë ndaj Mozilla Record-Replay… Për fund, arrita të kuptoj një gjë të thjeshtë: për çdo bllok rezervon struct TranslationBlock me detajet. Sa më mirë, përpara bllokut direkt në bferin. Pas kësaj, vendosa të ndaloj me ndihmësit (të paktën disa) dhe thjesht e hodha numrin magjik, duke ishkruar fjalët e mbetura në struct TranslationBlock, duke krijuar një listë të lidhur njëanëshe, përmes së cilës mund të kalojmë shpejt gjatë pastrimit të caches së përkthimit dhe të lehtësojmë kujtesën.

Disa ndihmës mbetën: për shembull, të shënuar tregues në bferin e kodit — disa prej tyre janë BinaryenExpressionRef, pra ata shikojnë në shprehjet që duhet të vendosen në bllokun bazë të gjeneruar, disa janë kushtet e kalimit midis blloqeve, disa — ku të kalojnë. Po ashtu, ekzistojnë blloqet e përgatitura për Relooper, të cilat duhet të lidhen sipas kushteve. Për t’i dalluar, përdoret supozimi që të gjithë janë të rregulluar të paktën në katër byte, kështu që mund të përdoren lirshëm dy bitët e fundit si etiketë, thjesht mos e harro ta heqësh atë kur është e nevojshme. Për më tepër, këto etiketat përdoren tashmë në QEMU për të shënuar arsyen e daljes nga cikli TCG.

Përdorimi i Binaryen

Modul në WebAssembly përmban funksione, secila prej të cilave përmban një trup që përbën një shprehje. Shprehjet janë operacione unar dhe binar, blloqe që përbëhen nga lista shprehjesh të tjera, rrjedhje kontrolli etj. Siç e kam thënë më parë, rrjedha e kontrollit këtu organizohet si ndarje dhe cikle në nivel të lartë, thirrje funksionesh etj. Argumentet për funksionet kalohen jo në stack, por shprehimisht, ashtu si në JS. Ka edhe variabla globale, por unë nuk i kam përdorur, kështu që nuk do të flas për to.

Gjithashtu, funksionet kanë variabla lokalë të numëruar nga zero, të cilët kanë tipin: int32 / int64 / float / double. Këtu, variabla të parë n lokalë janë argumentet e kaluara për funksionin. Vini re se, megjithëse gjithçka këtu nuk është krejtësisht nivel i ulët në termin e rrjedhjes së kontrollit, numrat e tërë gjithsesi nuk përmbajnë asnjë shenjë "të nënshkruar/të pa nënshkruar": si do të sillet numri varet nga kodi i operacionit.

Në përgjithësi, Binaryen ofron një C-API të thjeshtë: krijoni një modul, në të krijoni shprehje — unike, binarë, blloqe nga shprehje të tjera, kontrollin e rrjedhës etj. Pastaj krijoni një funksion, ku si trup duhet të tregoni shprehjen. Nëse keni, si unë, një grafik të nivelit të ulët të kalimeve — komponenti relooper do t'ju ndihmojë. Sa kuptoj, mund të përdorni menaxhimin e nivelit të lartë të rrjedhës në bllok, përderisa nuk del jashtë bllokut — domethënë, mund të bëni dekurbime të brendshme fast path / slow path brenda kodit të integruar për TLB-cache, por nuk mund të ndërhyni në rrjedhën e jashtme të kontrollit. Kur liroheni nga relooper, lirohen blloqet e tij, kur liroheni nga moduli — zhduken shprehjet, funksionet etj., të dedikuara në të. arena.

Megjithatë, nëse dëshironi të interpretoni kodin në mënyrë dinamike pa krijime dhe heqje të panevojshme të instancave të interpretuesit, mund të ketë kuptim ta nxjerrni këtë logjikë në një skedar në C++, dhe nga aty të menaxhoni tërë API-në C++ të bibliotekës, duke anashkaluar mbështjellësit e gatshëm.

Në këtë mënyrë, për të gjeneruar kod, 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 diçka — më falni, kjo është thjesht për të parashikuar shkallën, dhe hollësitë — ato janë në dokumentacion.

Tani fillon kreks-feks-peks, afërsisht kështu:

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 instance!
}, buf, sz);

Për të lidhur ndonjëherë botën QEMU dhe JS dhe për të hyrë shpejt në funksionet e kompiluar, u krijua një array (tabela e funksioneve për import në lançues), dhe aty u vendosën funksionet e gjeneruara. Për të llogaritur shpejt indeksin, fillimisht u përdor indeksi i fjalës zero të bllokut të përkthimit, por më pas, indeksi i llogaritur sipas kësaj formule filloi thjesht të regjistrohej në fushën struct TranslationBlock.

Me thënë të vërtetën, demo (për momentin me një licencë të paqartë) punon normalisht vetëm në Firefox. Zhvilluesit e Chrome ishin në njëfarë forme të papërgatitur për faktin që dikush do të dëshiron të krijojë më shumë se një mijë instanca të moduleve WebAssembly, prandaj thjesht ndanin nga një gigabajt hapësirë adresimi virtuale për çdo…

Për momentin, kjo është gjithçka. Ndoshta do të ketë një artikull tjetër, nëse ndokujt i intereson. Konkretisht, mbetet të paktën vetëm të bëjmë që pajisjet bllokuese të funksionojnë. Ndoshta ka kuptim gjithashtu të bëjmë kompaktimin e moduleve WebAssembly asinkron, siç është praktikë në botën e JS, tani që për më tepër kemi një interpretues që mund ta ekzekutojë këtë, derisa moduli natyror të jetë gati.

Përfundimisht, një enigmë: ju keni ndërtuar një binar në arkitekturën 32-bit, por kodi përmes operacioneve me memorie del nga Binaryen, diku në stek ose ndonjë vend tjetër në 2 GB e sipërme të hapësirës adresimi 32-bit. Problemi është se, nga perspectiva e Binaryen, kjo referencë është në një adresë rezultuese shumë të madhe. Si mund ta kaloni këtë?

Në stilin administrativ

Nuk e kam testuar këtë përfundimisht, por mendimi i parë ishte «Çfarë do të ndodhte nëse vendosim një Linux 32-bit?» Atëherë, pjesa e sipërme e hapësirës adresuese do të ishte e zënë nga bërthama. Pyetja është vetëm sa do të jetë e zënë: 1 apo 2 Gb.

Në stilin programues (variant për praktikë)

Ne do të frymëzojmë një flluskë në pjesën e sipërme të hapësirës adresuese. Nuk e kuptoj përse funksionon - është aty janë duhet të ketë një stek. Por «ne jemi praktik, gjithçka funksionon, 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, megjithatë, nuk është e pajtueshme, por, fatmirësisht, Valgrind vetë e ndihmon shumë efektivisht për t'i nxjerrë të gjithë jashtë 🙂

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

Burimi: habr.com

Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS 🔥 Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS | ProHoster