QEMU.js: ngayon ay seryoso at may WASM

Noong unang panahon, nagpasya akong magsaya patunayan ang reversibility ng proseso at matutunan kung paano bumuo ng JavaScript (mas tiyak, Asm.js) mula sa machine code. Napili ang QEMU para sa eksperimento, at pagkaraan ng ilang panahon, isang artikulo ang isinulat sa Habr. Sa mga komento ay pinayuhan akong gawing muli ang proyekto sa WebAssembly, at kahit na huminto sa aking sarili halos tapos na Kahit papaano ay hindi ko gusto ang proyekto... Ang trabaho ay nangyayari, ngunit napakabagal, at ngayon, kamakailan sa artikulong iyon ay lumitaw komentaryo sa paksang "Kaya paano natapos ang lahat?" Bilang tugon sa aking detalyadong sagot, narinig ko ang "Ito ay parang isang artikulo." Well, kung maaari, magkakaroon ng isang artikulo. Baka may makakita nito na kapaki-pakinabang. Mula dito matututunan ng mambabasa ang ilang katotohanan tungkol sa disenyo ng mga backend ng pagbuo ng QEMU code, pati na rin kung paano magsulat ng Just-in-Time compiler para sa isang web application.

gawain

Dahil natutunan ko na kung paano "paano" i-port ang QEMU sa JavaScript, sa pagkakataong ito ay napagpasyahan na gawin ito nang matalino at hindi ulitin ang mga lumang pagkakamali.

Error number one: sangay mula sa paglabas ng punto

Ang una kong pagkakamali ay ang pag-fork ng aking bersyon mula sa upstream na bersyon 2.4.1. Pagkatapos ay tila isang magandang ideya sa akin: kung umiiral ang paglabas ng punto, malamang na mas matatag ito kaysa sa simpleng 2.4, at higit pa sa sangay master. At dahil nagplano akong magdagdag ng isang patas na dami ng sarili kong mga bug, hindi ko na kailangan ng iba. Ganun siguro ang nangyari. Ngunit narito ang bagay: Ang QEMU ay hindi tumitigil, at sa isang punto ay inanunsyo pa nila ang pag-optimize ng nabuong code ng 10 porsyento. "Oo, ngayon ay mag-freeze ako," naisip ko at nasira. Dito kailangan nating gumawa ng digression: dahil sa katangian ng single-threaded ng QEMU.js at ang katotohanan na ang orihinal na QEMU ay hindi nagpapahiwatig ng kawalan ng multi-threading (iyon ay, ang kakayahang sabay na magpatakbo ng ilang hindi nauugnay na mga path ng code, at hindi lang "gamitin ang lahat ng kernels") ay kritikal para dito, ang mga pangunahing pag-andar ng mga thread na kailangan kong "i-out" upang makatawag mula sa labas. Lumikha ito ng ilang natural na problema sa panahon ng pagsasanib. Gayunpaman, ang katotohanan na ang ilan sa mga pagbabago mula sa sangay master, na kung saan sinubukan kong pagsamahin ang aking code, ay pinili din ng seresa sa paglabas ng punto (at samakatuwid sa aking sangay) ay malamang na hindi rin magdagdag ng kaginhawahan.

Sa pangkalahatan, napagpasyahan ko na makatuwiran pa rin na itapon ang prototype, i-disassemble ito para sa mga bahagi at bumuo ng isang bagong bersyon mula sa simula batay sa isang bagay na mas bago at ngayon mula sa master.

Pangalawang pagkakamali: pamamaraan ng TLP

Sa esensya, hindi ito isang pagkakamali, sa pangkalahatan, ito ay isang tampok lamang ng paglikha ng isang proyekto sa mga kondisyon ng kumpletong hindi pagkakaunawaan ng parehong "saan at paano lumipat?" at sa pangkalahatan "pupunta tayo doon?" Sa ganitong mga kondisyon clumsy programming ay isang makatwirang opsyon, ngunit, natural, hindi ko nais na ulitin ito nang hindi kinakailangan. Sa pagkakataong ito, gusto kong gawin ito nang matalino: atomic commits, conscious code changes (at hindi "pagsasama-sama ng mga random na character hanggang sa ito ay mag-compile (na may mga babala)", gaya ng sinabi minsan ni Linus Torvalds tungkol sa isang tao, ayon sa Wikiquote), atbp.

Pangatlong pagkakamali: ang pagpasok sa tubig nang hindi nalalaman ang ford

Hindi ko pa rin ito ganap na naaalis, ngunit ngayon ay nagpasya akong huwag sundin ang landas ng hindi bababa sa paglaban, at gawin ito sa "matandang paraan", ibig sabihin, isulat ang aking TCG backend mula sa simula, upang hindi na sabihin sa ibang pagkakataon, "Oo, ito ay siyempre, dahan-dahan, ngunit hindi ko makontrol ang lahat - kung paano nakasulat ang TCI..." Bukod dito, ito sa una ay tila isang malinaw na solusyon, dahil Bumubuo ako ng binary code. Sabi nga nila, β€œGhent gatheredΡƒ, ngunit hindi iyon": ang code ay, siyempre, binary, ngunit ang kontrol ay hindi maaaring ilipat lamang dito - dapat itong tahasang itulak sa browser para sa pagsasama-sama, na nagreresulta sa isang tiyak na bagay mula sa mundo ng JS, na kailangan pa ring mailigtas sa isang lugar. Gayunpaman, sa mga normal na arkitektura ng RISC, sa pagkakaintindi ko, ang karaniwang sitwasyon ay ang pangangailangang tahasang i-reset ang cache ng pagtuturo para sa regenerated na code - kung hindi ito ang kailangan natin, kung gayon, sa anumang kaso, malapit na ito. Bilang karagdagan, mula sa aking huling pagtatangka, nalaman ko na ang kontrol ay tila hindi inililipat sa gitna ng bloke ng pagsasalin, kaya hindi talaga namin kailangan ang bytecode na binibigyang-kahulugan mula sa anumang offset, at maaari lang namin itong buuin mula sa function sa TB .

Dumating sila at sumipa

Bagama't sinimulan kong muling isulat ang code noong Hulyo, hindi napansin ang isang magic kick: kadalasang dumarating ang mga titik mula sa GitHub bilang mga notification tungkol sa mga tugon sa mga kahilingan sa Mga Isyu at Pull, ngunit narito, lahat ng biglaan mention sa thread Binaryen bilang isang qemu backend sa konteksto, "Ginawa niya ang isang bagay na ganoon, baka may sasabihin siya." Pinag-uusapan namin ang tungkol sa paggamit ng kaugnay na aklatan ng Emscripten Binaryen upang lumikha ng WASM JIT. Well, sinabi ko na mayroon kang isang Apache 2.0 na lisensya doon, at ang QEMU sa kabuuan ay ipinamamahagi sa ilalim ng GPLv2, at hindi sila masyadong tugma. Biglang lumabas na ang isang lisensya ay maaaring ayusin mo naman kahit papaano (Hindi ko alam: baka palitan, baka dual licensing, baka iba pa...). Ito, siyempre, ang nagpasaya sa akin, dahil sa oras na iyon ay tiningnan ko nang mabuti binary na format WebAssembly, at kahit papaano ay nalungkot ako at hindi maintindihan. Mayroon ding isang library na lalamunin ang mga pangunahing bloke na may transition graph, gagawa ng bytecode, at kahit na patakbuhin ito sa mismong interpreter, kung kinakailangan.

Tapos meron pa isang liham sa QEMU mailing list, ngunit ito ay higit pa tungkol sa tanong na, β€œSino pa rin ang nangangailangan nito?” At ito ay lahat ng biglaan, kailangan pala. Sa pinakamababa, maaari mong pagsama-samahin ang mga sumusunod na posibilidad ng paggamit, kung ito ay gumagana nang mas mabilis o mas mabilis:

  • paglulunsad ng isang bagay na pang-edukasyon nang walang anumang pag-install
  • virtualization sa iOS, kung saan, ayon sa mga alingawngaw, ang tanging application na may karapatan sa pagbuo ng code sa mabilisang ay isang JS engine (totoo ba ito?)
  • pagpapakita ng mini-OS - single-floppy, built-in, lahat ng uri ng firmware, atbp...

Mga Tampok ng Browser Runtime

Tulad ng sinabi ko na, ang QEMU ay nakatali sa multithreading, ngunit ang browser ay wala nito. Well, iyon ay, hindi... Sa una ay hindi ito umiiral, pagkatapos ay lumitaw ang WebWorkers - sa pagkakaintindi ko, ito ay multithreading batay sa pagpasa ng mensahe walang nakabahaging mga variable. Naturally, lumilikha ito ng malalaking problema kapag nag-port ng umiiral na code batay sa shared memory model. Pagkatapos, sa ilalim ng presyon ng publiko, ipinatupad din ito sa ilalim ng pangalan SharedArrayBuffers. Unti-unti itong ipinakilala, ipinagdiwang nila ang paglulunsad nito sa iba't ibang mga browser, pagkatapos ay ipinagdiwang nila ang Bagong Taon, at pagkatapos ay Meltdown... Pagkatapos nito ay dumating sila sa konklusyon na magaspang o magaspang ang pagsukat ng oras, ngunit sa tulong ng nakabahaging memorya at isang thread incrementing the counter, pareho lang ito ay gagana nang medyo tumpak. Kaya hindi namin pinagana ang multithreading na may nakabahaging memorya. Tila na sa kalaunan ay ibinalik nila ito, ngunit, dahil naging malinaw ito mula sa unang eksperimento, mayroong buhay na wala ito, at kung gayon, susubukan naming gawin ito nang hindi umaasa sa multithreading.

Ang pangalawang tampok ay ang imposibilidad ng mababang antas ng mga manipulasyon sa stack: hindi mo basta-basta kunin, i-save ang kasalukuyang konteksto at lumipat sa bago na may bagong stack. Ang call stack ay pinamamahalaan ng JS virtual machine. Tila, ano ang problema, dahil nagpasya pa rin kaming pamahalaan ang mga dating daloy nang manu-mano? Ang katotohanan ay ang block I/O sa QEMU ay ipinapatupad sa pamamagitan ng mga coroutine, at ito ay kung saan magagamit ang mababang antas ng stack manipulations. Sa kabutihang palad, ang Emscipten ay naglalaman na ng isang mekanismo para sa mga asynchronous na operasyon, kahit na dalawa: Asyncify ΠΈ Emterpreter. Ang una ay gumagana sa pamamagitan ng makabuluhang bloat sa nabuong JavaScript code at hindi na sinusuportahan. Ang pangalawa ay ang kasalukuyang "tamang paraan" at gumagana sa pamamagitan ng pagbuo ng bytecode para sa katutubong interpreter. Gumagana ito, siyempre, dahan-dahan, ngunit hindi nito pinalaki ang code. Totoo, ang suporta para sa mga coroutine para sa mekanismong ito ay kailangang mag-ambag nang nakapag-iisa (mayroon nang mga coroutine na isinulat para sa Asyncify at nagkaroon ng pagpapatupad ng humigit-kumulang sa parehong API para sa Emterpreter, kailangan mo lang ikonekta ang mga ito).

Sa ngayon, hindi ko pa nagawang hatiin ang code sa isang pinagsama-sama sa WASM at binibigyang kahulugan gamit ang Emterpreter, kaya hindi pa gumagana ang mga block device (tingnan sa susunod na serye, gaya ng sinasabi nila...). Iyon ay, sa huli ay dapat kang makakuha ng isang bagay na tulad nitong nakakatawang layered na bagay:

  • binibigyang kahulugan ang block I/O. Buweno, inaasahan mo ba talagang tinularan ang NVMe na may katutubong pagganap? πŸ™‚
  • statically compiled main QEMU code (translator, iba pang emulated na device, atbp.)
  • dynamic na pinagsama-sama ang guest code sa WASM

Mga tampok ng QEMU source

Tulad ng malamang na nahulaan mo na, ang code para sa pagtulad sa mga arkitektura ng bisita at ang code para sa pagbuo ng mga tagubilin sa host machine ay pinaghihiwalay sa QEMU. Sa katunayan, ito ay medyo nakakalito:

  • may mga guest architecture
  • mayroon mga accelerators, ibig sabihin, KVM para sa virtualization ng hardware sa Linux (para sa mga guest at host system na magkatugma sa isa't isa), TCG para sa pagbuo ng JIT code kahit saan. Simula sa QEMU 2.9, lumitaw ang suporta para sa HAXM hardware virtualization standard sa Windows (ang mga detalye)
  • kung TCG ang ginagamit at hindi virtualization ng hardware, mayroon itong hiwalay na suporta sa pagbuo ng code para sa bawat arkitektura ng host, gayundin para sa unibersal na interpreter
  • ... at sa paligid ng lahat ng ito - emulated peripheral, user interface, migration, record-replay, atbp.

By the way, alam mo ba: Maaaring tularan ng QEMU hindi lamang ang buong computer, kundi pati na rin ang processor para sa isang hiwalay na proseso ng user sa host kernel, na ginagamit, halimbawa, ng AFL fuzzer para sa binary instrumentation. Marahil ay may gustong i-port ang mode ng pagpapatakbo ng QEMU sa JS? πŸ˜‰

Tulad ng karamihan sa matagal nang libreng software, ang QEMU ay binuo sa pamamagitan ng tawag configure ΠΈ make. Sabihin nating nagpasya kang magdagdag ng isang bagay: isang backend ng TCG, pagpapatupad ng thread, iba pa. Huwag magmadali na maging masaya/kinatatakutan (salungguhitan kung naaangkop) sa posibilidad na makipag-ugnayan sa Autoconf - sa katunayan, configure Ang QEMU ay tila isinulat sa sarili at hindi nabuo mula sa anumang bagay.

WebAss Assembly

Kaya ano ang bagay na ito na tinatawag na WebAssembly (aka WASM)? Ito ay isang kapalit para sa Asm.js, hindi na nagpapanggap na wastong JavaScript code. Sa kabaligtaran, ito ay purong binary at na-optimize, at kahit na simpleng pagsulat ng isang integer dito ay hindi masyadong simple: para sa pagiging compact, ito ay naka-imbak sa format LEB128.

Maaaring narinig mo na ang tungkol sa relooping algorithm para sa Asm.js - ito ang pagpapanumbalik ng "mataas na antas" na mga tagubilin sa pagkontrol sa daloy (iyon ay, kung-kung gayon, mga loop, atbp.), kung saan ang mga JS engine ay idinisenyo, mula sa ang mababang antas ng LLVM IR, mas malapit sa machine code na pinaandar ng processor. Naturally, ang intermediate na representasyon ng QEMU ay mas malapit sa pangalawa. Mukhang narito na, bytecode, ang katapusan ng pagdurusa... At pagkatapos ay may mga bloke, kung-kung gayon-iba at mga loop!..

At ito ay isa pang dahilan kung bakit kapaki-pakinabang ang Binaryen: natural itong tumanggap ng mga bloke na may mataas na antas na malapit sa kung ano ang maiimbak sa WASM. Ngunit maaari rin itong gumawa ng code mula sa isang graph ng mga pangunahing bloke at mga transition sa pagitan ng mga ito. Buweno, nasabi ko na na itinatago nito ang format ng imbakan ng WebAssembly sa likod ng maginhawang C/C++ API.

TCG (Tiny Code Generator)

GCT ay orihinal backend para sa C compiler. Pagkatapos, tila, hindi nito makayanan ang kumpetisyon sa GCC, ngunit sa huli ay natagpuan nito ang lugar nito sa QEMU bilang mekanismo ng pagbuo ng code para sa platform ng host. Mayroon ding backend ng TCG na bumubuo ng ilang abstract bytecode, na agad na isinasagawa ng interpreter, ngunit nagpasya akong iwasan ang paggamit nito sa oras na ito. Gayunpaman, ang katotohanan na sa QEMU posible nang paganahin ang paglipat sa nabuong TB sa pamamagitan ng function tcg_qemu_tb_exec, ito ay naging lubhang kapaki-pakinabang para sa akin.

Para magdagdag ng bagong backend ng TCG sa QEMU, kailangan mong gumawa ng subdirectory tcg/<имя Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Ρ‹> (sa kasong ito, tcg/binaryen), at naglalaman ito ng dalawang file: tcg-target.h ΠΈ tcg-target.inc.c ΠΈ magreseta ito ay tungkol sa lahat configure. Maaari kang maglagay ng iba pang mga file doon, ngunit, tulad ng maaari mong hulaan mula sa mga pangalan ng dalawang ito, pareho silang isasama sa isang lugar: isa bilang isang regular na file ng header (ito ay kasama sa tcg/tcg.h, at ang isang iyon ay nasa iba pang mga file sa mga direktoryo tcg, accel at hindi lamang), ang isa pa - bilang snippet ng code lamang sa tcg/tcg.c, ngunit mayroon itong access sa mga static na function nito.

Sa pagpapasya na gugugol ako ng masyadong maraming oras sa mga detalyadong pagsisiyasat kung paano ito gumagana, kinopya ko lang ang "mga balangkas" ng dalawang file na ito mula sa isa pang pagpapatupad ng backend, na matapat na nagpapahiwatig nito sa header ng lisensya.

talaksan tcg-target.h naglalaman ng pangunahing mga setting sa form #define-s:

  • kung gaano karaming mga rehistro at kung anong lapad ang naroroon sa target na arkitektura (mayroon kaming kasing dami hangga't gusto namin, hangga't gusto namin - ang tanong ay higit pa tungkol sa kung ano ang bubuo sa mas mahusay na code ng browser sa "ganap na target" na arkitektura ...)
  • pagkakahanay ng mga tagubilin sa host: sa x86, at kahit sa TCI, ang mga tagubilin ay hindi nakahanay sa lahat, ngunit ilalagay ko sa buffer ng code hindi mga tagubilin sa lahat, ngunit mga payo sa mga istruktura ng library ng Binaryen, kaya sasabihin ko: 4 byte
  • anong mga opsyonal na tagubilin ang maaaring mabuo ng backend - isinama namin ang lahat ng makikita namin sa Binaryen, hayaan ang accelerator na hatiin ang iba sa mas simple mismo
  • Ano ang tinatayang laki ng TLB cache na hiniling ng backend. Ang katotohanan ay sa QEMU lahat ng bagay ay seryoso: kahit na may mga helper function na gumaganap ng load/store na isinasaalang-alang ang guest MMU (saan tayo kung wala ito ngayon?), I-save nila ang kanilang translation cache sa anyo ng isang istraktura, ang pagpoproseso ng kung saan ay maginhawa upang i-embed nang direkta sa mga bloke ng broadcast. Ang tanong ay kung ano ang offset sa istrukturang ito ang pinaka mahusay na naproseso ng isang maliit at mabilis na pagkakasunud-sunod ng mga utos?
  • dito maaari mong i-tweak ang layunin ng isa o dalawang nakareserbang rehistro, paganahin ang pagtawag sa TB sa pamamagitan ng isang function at opsyonal na ilarawan ang ilang maliliit na inline- mga function tulad ng flush_icache_range (ngunit hindi ito ang aming kaso)

talaksan tcg-target.inc.c, siyempre, ay karaniwang mas malaki ang laki at naglalaman ng ilang mga mandatoryong function:

  • pagsisimula, kabilang ang mga paghihigpit sa kung aling mga tagubilin ang maaaring gumana sa kung aling mga operand. Tahasan na kinopya ko mula sa isa pang backend
  • function na tumatagal ng isang panloob na pagtuturo ng bytecode
  • Maaari ka ring maglagay ng mga auxiliary function dito, at maaari ka ring gumamit ng mga static na function mula sa tcg/tcg.c

Para sa aking sarili, pinili ko ang sumusunod na diskarte: sa mga unang salita ng susunod na bloke ng pagsasalin, isinulat ko ang apat na puntos: isang panimulang marka (isang tiyak na halaga sa paligid 0xFFFFFFFF, na nagtukoy sa kasalukuyang estado ng TB), konteksto, nabuong module, at magic number para sa pag-debug. Sa una ay inilagay ang marka 0xFFFFFFFF - nSaan n - isang maliit na positibong numero, at sa bawat oras na ito ay naisakatuparan sa pamamagitan ng interpreter tumaas ito ng 1. Nang umabot ito 0xFFFFFFFE, naganap ang compilation, na-save ang module sa function table, na-import sa isang maliit na "launcher", kung saan napunta ang execution tcg_qemu_tb_exec, at ang module ay inalis sa QEMU memory.

Upang i-paraphrase ang mga classic, "Crutch, magkano ang intertwined sa tunog na ito para sa puso ng proger...". Gayunpaman, ang alaala ay tumutulo sa kung saan. Bukod dito, ito ay memory na pinamamahalaan ng QEMU! Mayroon akong isang code na, kapag isinusulat ang susunod na pagtuturo (mabuti, iyon ay, isang pointer), tinanggal ang isa na ang link ay nasa lugar na ito nang mas maaga, ngunit hindi ito nakatulong. Sa totoo lang, sa pinakasimpleng kaso, ang QEMU ay naglalaan ng memorya sa pagsisimula at nagsusulat ng nabuong code doon. Kapag naubos ang buffer, itatapon ang code at ang susunod ay magsisimulang isulat sa lugar nito.

Matapos pag-aralan ang code, napagtanto ko na ang trick na may magic number ay nagpapahintulot sa akin na hindi mabigo sa heap destruction sa pamamagitan ng pagpapalaya ng isang bagay na mali sa isang uninitialized buffer sa unang pass. Ngunit sino ang muling sumulat ng buffer upang i-bypass ang aking function sa ibang pagkakataon? Tulad ng ipinapayo ng mga developer ng Emscripten, nang magkaroon ako ng problema, inilipat ko ang resultang code pabalik sa native na application, itinakda dito ang Mozilla Record-Replay... Sa pangkalahatan, sa huli ay napagtanto ko ang isang simpleng bagay: para sa bawat bloke, a struct TranslationBlock kasama ang paglalarawan nito. Hulaan kung saan... Tama, bago ang block sa buffer. Napagtanto ko ito, nagpasya akong huminto sa paggamit ng mga saklay (kahit ilan), at itinapon na lang ang magic number, at inilipat ang natitirang mga salita sa struct TranslationBlock, na lumilikha ng isang solong naka-link na listahan na maaaring mabilis na madaanan kapag ang cache ng pagsasalin ay na-reset, at nagbakante ng memorya.

Nananatili ang ilang saklay: halimbawa, may markang mga pointer sa buffer ng code - ang ilan sa mga ito ay simple BinaryenExpressionRef, iyon ay, tinitingnan nila ang mga expression na kailangang linearly na ilagay sa nabuong pangunahing bloke, ang bahagi ay ang kondisyon para sa paglipat sa pagitan ng mga BB, ang bahagi ay kung saan pupunta. Well, mayroon nang nakahanda na mga bloke para sa Relooper na kailangang konektado ayon sa mga kondisyon. Upang makilala ang mga ito, ginagamit ang pagpapalagay na lahat sila ay nakahanay ng hindi bababa sa apat na byte, upang ligtas mong magamit ang hindi bababa sa makabuluhang dalawang bit para sa label, kailangan mo lamang tandaan na alisin ito kung kinakailangan. Sa pamamagitan ng paraan, ang mga naturang label ay ginagamit na sa QEMU upang ipahiwatig ang dahilan ng pag-alis sa TCG loop.

Gamit ang Binaryen

Ang mga module sa WebAssembly ay naglalaman ng mga function, na ang bawat isa ay naglalaman ng katawan, na isang expression. Ang mga expression ay unary at binary na operasyon, mga bloke na binubuo ng mga listahan ng iba pang mga expression, control flow, atbp. Tulad ng nasabi ko na, ang daloy ng kontrol dito ay nakaayos nang tumpak bilang mga mataas na antas na sangay, mga loop, mga tawag sa pag-andar, atbp. Ang mga argumento sa mga function ay hindi ipinapasa sa stack, ngunit tahasan, tulad ng sa JS. Mayroon ding mga pandaigdigang variable, ngunit hindi ko pa ginagamit ang mga ito, kaya hindi ko sasabihin sa iyo ang tungkol sa mga ito.

Ang mga function ay mayroon ding mga lokal na variable, na may bilang mula sa zero, ng uri: int32 / int64 / float / double. Sa kasong ito, ang unang n lokal na variable ay ang mga argumento na ipinasa sa function. Pakitandaan na bagama't ang lahat dito ay hindi ganap na mababa ang antas sa mga tuntunin ng daloy ng kontrol, ang mga integer ay hindi pa rin nagtataglay ng katangiang "signed/unsigned": depende sa operation code kung paano kumikilos ang numero.

Sa pangkalahatan, nagbibigay ang Binaryen simpleng C-API: gumawa ka ng module, Sa kanya lumikha ng mga expression - unary, binary, mga bloke mula sa iba pang mga expression, control flow, atbp. Pagkatapos ay lumikha ka ng isang function na may isang expression bilang katawan nito. Kung ikaw, tulad ko, ay may mababang antas ng transition graph, tutulungan ka ng relooper component. Sa pagkakaintindi ko, posible na gumamit ng mataas na antas ng kontrol ng daloy ng pagpapatupad sa isang bloke, hangga't hindi ito lalampas sa mga hangganan ng bloke - iyon ay, posible na gumawa ng panloob na mabilis na landas / mabagal sumasanga ng landas sa loob ng built-in na TLB cache processing code, ngunit hindi upang makagambala sa "panlabas" na daloy ng kontrol . Kapag pinalaya mo ang isang relooper, mapapalaya ang mga bloke nito; kapag nabakante mo ang isang module, mawawala ang mga expression, function, atbp. na nakalaan dito arena.

Gayunpaman, kung gusto mong bigyang-kahulugan ang code nang mabilisan nang walang hindi kinakailangang paglikha at pagtanggal ng isang halimbawa ng interpreter, maaaring makatuwirang ilagay ang lohika na ito sa isang C++ file, at mula doon ay direktang pamahalaan ang buong C++ API ng library, na lumalampas sa handa- ginawang mga balot.

Kaya para makabuo ng code na kailangan mo

// Π½Π°ΡΡ‚Ρ€ΠΎΠΈΡ‚ΡŒ Π³Π»ΠΎΠ±Π°Π»ΡŒΠ½Ρ‹Π΅ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ (ΠΌΠΎΠΆΠ½ΠΎ ΠΏΠΎΠΌΠ΅Π½ΡΡ‚ΡŒ ΠΏΠΎΡ‚ΠΎΠΌ)
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);

... kung may nakalimutan ako, paumanhin, ito ay para lamang kumatawan sa sukat, at ang mga detalye ay nasa dokumentasyon.

At ngayon magsisimula na ang crack-fex-pex, parang ganito:

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

Upang kahit papaano ay maikonekta ang mga mundo ng QEMU at JS at kasabay nito ay mabilis na ma-access ang mga pinagsama-samang function, isang array ang ginawa (isang talahanayan ng mga function para sa pag-import sa launcher), at ang mga nabuong function ay inilagay doon. Upang mabilis na kalkulahin ang index, ang index ng zero word translation block ay unang ginamit bilang ito, ngunit pagkatapos ay ang index na kinakalkula gamit ang formula na ito ay nagsimulang magkasya lamang sa field sa struct TranslationBlock.

Sa pamamagitan ng paraan, demo (kasalukuyang may madilim na lisensya) gumagana lang ng maayos sa Firefox. Mga developer ng Chrome noon kahit papaano hindi pa handa sa katotohanang may gustong lumikha ng higit sa isang libong mga pagkakataon ng mga module ng WebAssembly, kaya naglaan lang sila ng isang gigabyte ng virtual address space para sa bawat...

Yun lang muna. Marahil ay magkakaroon ng isa pang artikulo kung sinuman ang interesado. Namely, may nananatiling hindi bababa sa lamang gawing gumagana ang mga block device. Maaaring magkaroon din ng kahulugan na gawing asynchronous ang compilation ng mga WebAssembly modules, gaya ng nakaugalian sa JS world, dahil mayroon pa ring interpreter na kayang gawin ang lahat ng ito hanggang sa maging handa ang native module.

Sa wakas isang bugtong: nag-compile ka ng binary sa isang 32-bit na arkitektura, ngunit ang code, sa pamamagitan ng mga pagpapatakbo ng memorya, ay umakyat mula sa Binaryen, sa isang lugar sa stack, o sa ibang lugar sa itaas na 2 GB ng 32-bit address space. Ang problema ay mula sa pananaw ni Binaryen ito ay nag-a-access ng masyadong malaki sa isang resultang address. Paano makalibot dito?

Sa paraan ng admin

Hindi ko natapos na subukan ito, ngunit ang una kong naisip ay "Paano kung nag-install ako ng 32-bit na Linux?" Pagkatapos ang itaas na bahagi ng puwang ng address ay sasakupin ng kernel. Ang tanong lang ay kung magkano ang sasakupin: 1 o 2 Gb.

Sa paraang programmer (opsyon para sa mga practitioner)

Pumutok tayo ng bubble sa tuktok ng address space. Ako mismo ay hindi maintindihan kung bakit ito gumagana - doon na dapat may salansan. Ngunit "kami ay mga practitioner: lahat ay gumagana para sa amin, ngunit walang nakakaalam kung bakit..."

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

... totoo na hindi ito tugma sa Valgrind, ngunit, sa kabutihang palad, ang Valgrind mismo ay napaka-epektibong nagtutulak sa lahat palabas doon :)

Marahil ay may magbibigay ng mas mahusay na paliwanag kung paano gumagana itong code ko...

Pinagmulan: www.habr.com

Magdagdag ng komento