Qemu.js na may suporta sa JIT: maaari mo pa ring ibalik ang mince

Ilang taon na ang nakalipas Fabrice Bellard isinulat ni jslinux ay isang PC emulator na nakasulat sa JavaScript. Pagkatapos noon ay nagkaroon ng hindi bababa sa higit pa Virtual x86. Ngunit lahat sila, sa pagkakaalam ko, ay mga interpreter, habang ang Qemu, na isinulat nang mas maaga ng parehong Fabrice Bellard, at, malamang, ang sinumang may paggalang sa sarili na modernong emulator, ay gumagamit ng JIT compilation ng guest code sa host system code. Para sa akin, oras na para ipatupad ang kabaligtaran na gawain na may kaugnayan sa isa na nilulutas ng mga browser: JIT compilation ng machine code sa JavaScript, kung saan tila pinaka-lohikal na i-port ang Qemu. Mukhang, bakit Qemu, may mga mas simple at user-friendly na mga emulator - ang parehong VirtualBox, halimbawa - naka-install at gumagana. Ngunit ang Qemu ay may ilang mga kagiliw-giliw na tampok

  • open source
  • kakayahang magtrabaho nang walang kernel driver
  • kakayahang magtrabaho sa interpreter mode
  • suporta para sa isang malaking bilang ng parehong host at guest architecture

Tungkol sa ikatlong punto, maaari ko na ngayong ipaliwanag na sa katunayan, sa TCI mode, hindi ang mga tagubilin ng guest machine mismo ang binibigyang kahulugan, ngunit ang bytecode na nakuha mula sa kanila, ngunit hindi nito binabago ang kakanyahan - upang makabuo at tumakbo Qemu sa isang bagong arkitektura, kung ikaw ay mapalad, A C compiler ay sapat na - ang pagsulat ng isang code generator ay maaaring ipagpaliban.

At ngayon, pagkatapos ng dalawang taon ng masayang pag-uusap sa Qemu source code sa aking libreng oras, lumitaw ang isang gumaganang prototype, kung saan maaari ka nang tumakbo, halimbawa, Kolibri OS.

Ano ang Emscripten

Sa ngayon, maraming mga compiler ang lumitaw, ang resulta nito ay JavaScript. Ang ilan, tulad ng Type Script, ay orihinal na inilaan upang maging pinakamahusay na paraan upang magsulat para sa web. Kasabay nito, ang Emscripten ay isang paraan upang kunin ang umiiral na C o C++ code at i-compile ito sa isang form na nababasa ng browser. Naka-on ang pahinang ito Nakakolekta kami ng maraming port ng mga kilalang programa: ditoHalimbawa, maaari mong tingnan ang PyPy - sa pamamagitan ng paraan, inaangkin nila na mayroon nang JIT. Sa katunayan, hindi lahat ng programa ay maaaring i-compile at patakbuhin sa isang browser - mayroong isang numero mga tampok, na kailangan mong tiisin, gayunpaman, dahil ang inskripsyon sa parehong pahina ay nagsasabing "Ang Emscripten ay maaaring gamitin upang i-compile ang halos anumang portatil C/C++ code sa JavaScript". Ibig sabihin, mayroong ilang mga operasyon na hindi natukoy na pag-uugali ayon sa pamantayan, ngunit kadalasang gumagana sa x86 - halimbawa, hindi nakahanay na pag-access sa mga variable, na karaniwang ipinagbabawal sa ilang mga arkitektura. Sa pangkalahatan , Ang Qemu ay isang cross-platform na programa at , Gusto kong maniwala, at hindi pa ito naglalaman ng maraming hindi natukoy na pag-uugali - kunin ito at i-compile, pagkatapos ay mag-tinker ng kaunti sa JIT - at tapos ka na! Ngunit hindi iyon ang kaso...

Unang pagsubok

Sa pangkalahatan, hindi ako ang unang taong nakaisip ng ideya ng pag-port ng Qemu sa JavaScript. May tanong sa forum ng ReactOS kung posible ito gamit ang Emscripten. Kahit na mas maaga, may mga alingawngaw na ginawa ito ni Fabrice Bellard nang personal, ngunit pinag-uusapan natin ang tungkol sa jslinux, na, sa pagkakaalam ko, ay isang pagtatangka lamang na manu-manong makamit ang sapat na pagganap sa JS, at isinulat mula sa simula. Nang maglaon, isinulat ang Virtual x86 - nai-post para dito ang mga hindi na-obfuscated na mapagkukunan, at, tulad ng nakasaad, ang mas malaking "realismo" ng emulation ay naging posible na gamitin ang SeaBIOS bilang firmware. Bilang karagdagan, mayroong hindi bababa sa isang pagtatangka na i-port ang Qemu gamit ang Emscripten - sinubukan kong gawin ito socketpair, ngunit ang pag-unlad, sa pagkakaintindi ko, ay nagyelo.

Kaya, tila, narito ang mga mapagkukunan, narito ang Emscripten - kunin ito at i-compile. Ngunit mayroon ding mga aklatan kung saan nakasalalay ang Qemu, at mga aklatan kung saan nakasalalay ang mga aklatang iyon, atbp., at isa sa mga ito ay libffi, kung saan nakasalalay ang glib. May mga alingawngaw sa Internet na mayroong isa sa malaking koleksyon ng mga daungan ng mga aklatan para sa Emscripten, ngunit medyo mahirap paniwalaan: una, hindi ito nilayon na maging isang bagong compiler, pangalawa, ito ay masyadong mababang antas ng library para kunin lang, at i-compile sa JS. At ito ay hindi lamang isang bagay ng pagsingit ng pagpupulong - marahil, kung i-twist mo ito, para sa ilang mga calling convention maaari kang bumuo ng mga kinakailangang argumento sa stack at tawagan ang function nang wala ang mga ito. Ngunit ang Emscripten ay isang nakakalito na bagay: upang gawing pamilyar ang nabuong code sa browser JS engine optimizer, ginagamit ang ilang mga trick. Sa partikular, ang tinatawag na relooping - isang code generator gamit ang natanggap na LLVM IR na may ilang abstract na mga tagubilin sa paglipat ay sumusubok na muling likhain ang mga makatotohanang ifs, loops, atbp. Well, paano ipinasa ang mga argumento sa function? Naturally, bilang mga argumento sa mga function ng JS, iyon ay, kung maaari, hindi sa pamamagitan ng stack.

Sa simula mayroong isang ideya na magsulat lamang ng isang kapalit para sa libffi sa JS at magpatakbo ng mga karaniwang pagsubok, ngunit sa huli ay nalilito ako kung paano gawin ang aking mga file ng header upang gumana sila sa umiiral na code - ano ang magagawa ko, sabi nga nila, "Napakakomplikado ba ng mga gawain "Ganyan ba tayo katanga?" Kinailangan kong i-port ang libffi sa isa pang arkitektura, wika nga - sa kabutihang palad, ang Emscripten ay may parehong mga macro para sa inline na pagpupulong (sa Javascript, oo - mabuti, anuman ang arkitektura, kaya ang assembler), at ang kakayahang magpatakbo ng code na nabuo sa mabilisang. Sa pangkalahatan, pagkatapos mag-tinkering sa mga fragment ng libffi na umaasa sa platform sa loob ng ilang panahon, nakakuha ako ng ilang compilable code at pinatakbo ito sa unang pagsubok na napuntahan ko. Sa aking pagtataka, matagumpay ang pagsusulit. Nagulat sa aking henyo - walang biro, nagtrabaho ito mula sa unang paglulunsad - Ako, hindi pa rin naniniwala sa aking mga mata, ay tumingin muli sa resultang code, upang suriin kung saan susunod na maghukay. Dito ako nabaliw sa pangalawang pagkakataon - ang tanging ginawa ng aking function ay ffi_call - nag-ulat ito ng matagumpay na tawag. Walang mismong tawag. Kaya ipinadala ko ang aking unang kahilingan sa paghila, na nagtama ng isang error sa pagsusulit na malinaw sa sinumang mag-aaral sa Olympiad - ang mga tunay na numero ay hindi dapat ikumpara bilang a == b at kahit paano a - b < EPS - kailangan mo ring tandaan ang module, kung hindi, 0 ay magiging katumbas ng 1/3... Sa pangkalahatan, nakabuo ako ng isang tiyak na port ng libffi, na pumasa sa pinakasimpleng mga pagsubok, at kung saan ang glib ay compiled - Napagpasyahan kong kakailanganin ito, idaragdag ko ito sa ibang pagkakataon. Sa hinaharap, sasabihin ko na, tulad ng nangyari, hindi kasama ng compiler ang function ng libffi sa panghuling code.

Ngunit, tulad ng sinabi ko na, mayroong ilang mga limitasyon, at kabilang sa libreng paggamit ng iba't ibang hindi natukoy na pag-uugali, isang mas hindi kasiya-siyang tampok ang naitago - Ang JavaScript sa pamamagitan ng disenyo ay hindi sumusuporta sa multithreading na may nakabahaging memorya. Sa prinsipyo, ito ay karaniwang matatawag na isang magandang ideya, ngunit hindi para sa pag-port ng code na ang arkitektura ay nakatali sa mga C thread. Sa pangkalahatan, ang Firefox ay nag-eeksperimento sa pagsuporta sa mga nakabahaging manggagawa, at ang Emscripten ay may pthread na pagpapatupad para sa kanila, ngunit hindi ko nais na umasa dito. Kinailangan kong dahan-dahang i-root ang multithreading mula sa Qemu code - iyon ay, alamin kung saan tumatakbo ang mga thread, ilipat ang katawan ng loop na tumatakbo sa thread na ito sa isang hiwalay na function, at tawagan ang mga naturang function nang isa-isa mula sa pangunahing loop.

Ikalawang subukan

Sa ilang mga punto, naging malinaw na ang problema ay nandoon pa rin, at ang basta-basta na pagtulak ng mga saklay sa paligid ng code ay hindi hahantong sa anumang kabutihan. Konklusyon: kailangan nating i-systematize ang proseso ng pagdaragdag ng mga saklay. Samakatuwid, ang bersyon 2.4.1, na sariwa noong panahong iyon, ay kinuha (hindi 2.5.0, dahil, sino ang nakakaalam, magkakaroon ng mga bug sa bagong bersyon na hindi pa nahuhuli, at mayroon akong sapat na sarili kong mga bug. ), at ang unang bagay ay muling isulat ito nang ligtas thread-posix.c. Well, iyon ay, bilang ligtas: kung sinubukan ng isang tao na magsagawa ng isang operasyon na humahantong sa pagharang, ang function ay agad na tinawag abort() - siyempre, hindi nito nalutas ang lahat ng mga problema nang sabay-sabay, ngunit kahit papaano ay mas kaaya-aya ito kaysa sa tahimik na pagtanggap ng hindi pantay na data.

Sa pangkalahatan, ang mga opsyon sa Emscripten ay lubhang nakakatulong sa pag-port ng code sa JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - nahuhuli nila ang ilang uri ng hindi natukoy na pag-uugali, tulad ng mga tawag sa isang hindi nakahanay na address (na hindi talaga pare-pareho sa code para sa mga na-type na array tulad ng HEAP32[addr >> 2] = 1) o pagtawag sa isang function na may maling bilang ng mga argumento.

Sa pamamagitan ng paraan, ang mga error sa pag-align ay isang hiwalay na isyu. Gaya ng nasabi ko na, ang Qemu ay may "degenerate" interpretive backend para sa pagbuo ng code na TCI (maliit na code interpreter), at para bumuo at magpatakbo ng Qemu sa isang bagong arkitektura, kung ikaw ay mapalad, sapat na ang isang C compiler. Mga Keyword "kung sinuswerte ka". Hindi ako pinalad, at lumabas na ang TCI ay gumagamit ng hindi nakahanay na pag-access kapag nag-parse ng bytecode nito. Iyon ay, sa lahat ng uri ng ARM at iba pang mga arkitektura na may kinakailangang leveled na pag-access, ang Qemu ay nag-compile dahil mayroon silang normal na TCG backend na bumubuo ng katutubong code, ngunit kung gagana ang TCI sa kanila ay isa pang tanong. Gayunpaman, tulad ng nangyari, malinaw na ipinahiwatig ng dokumentasyon ng TCI ang isang katulad na bagay. Bilang resulta, ang mga function call para sa hindi nakahanay na pagbabasa ay idinagdag sa code, na natagpuan sa ibang bahagi ng Qemu.

Bunton pagkawasak

Bilang isang resulta, ang hindi nakahanay na pag-access sa TCI ay naitama, isang pangunahing loop ay nilikha na siya namang tinatawag na processor, RCU at ilang iba pang maliliit na bagay. At kaya inilunsad ko ang Qemu na may opsyon -d exec,in_asm,out_asm, na nangangahulugan na kailangan mong sabihin kung aling mga bloke ng code ang isinasagawa, at sa oras din ng broadcast para isulat kung ano ang guest code, kung ano ang naging host code (sa kasong ito, bytecode). Nagsisimula ito, nagsasagawa ng ilang mga bloke ng pagsasalin, nagsusulat ng mensahe sa pag-debug na iniwan ko na magsisimula na ang RCU at... nag-crash abort() sa loob ng isang function free(). Sa pamamagitan ng tinkering sa function free() Nagawa naming malaman na sa header ng heap block, na nasa walong byte na nauuna sa inilalaan na memorya, sa halip na ang laki ng bloke o katulad na bagay, mayroong basura.

Pagkasira ng bunton - gaano kaganda... Sa ganoong kaso, mayroong isang kapaki-pakinabang na lunas - mula sa (kung posible) sa parehong mga mapagkukunan, mag-ipon ng isang katutubong binary at patakbuhin ito sa ilalim ng Valgrind. Pagkaraan ng ilang oras, handa na ang binary. Inilunsad ko ito gamit ang parehong mga pagpipilian - nag-crash ito kahit na sa panahon ng pagsisimula, bago aktwal na maabot ang pagpapatupad. Ito ay hindi kasiya-siya, siyempre - tila, ang mga mapagkukunan ay hindi eksakto ang parehong, na kung saan ay hindi nakakagulat, dahil configure scouted out bahagyang iba't ibang mga pagpipilian, ngunit mayroon akong Valgrind - una ay ayusin ko ang bug na ito, at pagkatapos, kung ako ay mapalad , lalabas ang orihinal. Pinapatakbo ko ang parehong bagay sa ilalim ng Valgrind... Y-y-y, y-y-y, uh-uh, nagsimula ito, dumaan sa initialization nang normal at lumipat sa paglipas ng orihinal na bug nang walang isang babala tungkol sa maling pag-access sa memorya, hindi banggitin ang tungkol sa pagbagsak. Ang buhay, gaya ng sinasabi nila, ay hindi naghanda sa akin para dito - huminto ang pag-crash na programa kapag inilunsad sa ilalim ng Walgrind. Kung ano ito ay isang misteryo. Ang hypothesis ko ay isang beses sa paligid ng kasalukuyang pagtuturo pagkatapos ng pag-crash sa panahon ng pagsisimula, nagpakita ng trabaho ang gdb memset-a na may wastong pointer gamit ang alinman mmx, o xmm mga rehistro, kung gayon marahil ito ay isang uri ng error sa pagkakahanay, bagama't mahirap pa rin itong paniwalaan.

Okay, mukhang hindi makakatulong si Valgrind dito. At dito nagsimula ang pinakakasuklam-suklam na bagay - ang lahat ay tila nagsisimula pa, ngunit nag-crash sa ganap na hindi kilalang mga kadahilanan dahil sa isang kaganapan na maaaring nangyari milyon-milyong mga tagubilin ang nakalipas. Sa mahabang panahon, hindi man lang malinaw kung paano lalapit. Sa huli, kailangan ko pa ring umupo at mag-debug. Ang pag-print kung ano ang muling isinulat ng header ay nagpakita na hindi ito mukhang isang numero, ngunit sa halip ay isang uri ng binary data. At, narito, ang binary string na ito ay natagpuan sa BIOS file - iyon ay, ngayon posible na sabihin nang may makatwirang pagtitiwala na ito ay isang buffer overflow, at malinaw pa na isinulat ito sa buffer na ito. Kaya, kung gayon ang isang bagay tulad nito - sa Emscripten, sa kabutihang-palad, walang randomization ng address space, walang mga butas din dito, kaya maaari kang sumulat sa isang lugar sa gitna ng code upang mag-output ng data sa pamamagitan ng pointer mula sa huling paglulunsad, tingnan ang data, tingnan ang pointer, at , kung hindi ito nagbago, kumuha ng pag-iisip. Totoo, tumatagal ng ilang minuto upang ma-link pagkatapos ng anumang pagbabago, ngunit ano ang maaari mong gawin? Bilang resulta, natagpuan ang isang tiyak na linya na kinopya ang BIOS mula sa pansamantalang buffer patungo sa memorya ng bisita - at, sa katunayan, walang sapat na espasyo sa buffer. Ang paghahanap sa pinagmulan ng kakaibang buffer address na iyon ay nagresulta sa isang function qemu_anon_ram_alloc sa file oslib-posix.c - ang lohika doon ay ito: kung minsan maaari itong maging kapaki-pakinabang upang ihanay ang address sa isang malaking pahina ng 2 MB ang laki, para dito kami ay magtatanong mmap kaunti pa, at pagkatapos ay ibabalik namin ang labis sa tulong munmap. At kung ang naturang pagkakahanay ay hindi kinakailangan, pagkatapos ay ipahiwatig namin ang resulta sa halip na 2 MB getpagesize() - mmap magbibigay pa rin ito ng nakahanay na address... Kaya sa Emscripten mmap tawag lang malloc, ngunit siyempre hindi ito nakahanay sa pahina. Sa pangkalahatan, ang isang bug na nakakabigo sa akin sa loob ng ilang buwan ay naitama sa pamamagitan ng pagbabago sa Π΄Π²ΡƒΡ… mga linya.

Mga tampok ng mga function ng pagtawag

At ngayon ang processor ay nagbibilang ng isang bagay, ang Qemu ay hindi nag-crash, ngunit ang screen ay hindi naka-on, at ang processor ay mabilis na napupunta sa mga loop, na hinuhusgahan ng output -d exec,in_asm,out_asm. May lumabas na hypothesis: hindi dumarating ang mga timer interrupts (o, sa pangkalahatan, lahat ng interrupts). At sa katunayan, kung i-unscrew mo ang mga pagkagambala mula sa katutubong pagpupulong, na sa ilang kadahilanan ay nagtrabaho, makakakuha ka ng isang katulad na larawan. Ngunit hindi ito ang sagot sa lahat: ang isang paghahambing ng mga bakas na ibinigay sa opsyon sa itaas ay nagpakita na ang mga trajectory ng pagpapatupad ay nag-iba nang maaga. Dito dapat sabihin na paghahambing ng kung ano ang naitala gamit ang launcher emrun Ang pag-debug ng output na may output ng katutubong pagpupulong ay hindi isang ganap na mekanikal na proseso. Hindi ko alam nang eksakto kung paano kumokonekta ang isang program na tumatakbo sa isang browser emrun, ngunit ang ilang mga linya sa output ay lumabas na muling inayos, kaya ang pagkakaiba sa diff ay hindi pa isang dahilan upang ipagpalagay na ang mga trajectory ay nagkakaiba. Sa pangkalahatan, naging malinaw na ayon sa mga tagubilin ljmpl mayroong isang paglipat sa iba't ibang mga address, at ang bytecode na nabuo ay sa panimula ay naiiba: ang isa ay naglalaman ng isang tagubilin upang tumawag sa isang function ng katulong, ang isa ay hindi. Matapos i-googling ang mga tagubilin at pag-aralan ang code na nagsasalin ng mga tagubiling ito, naging malinaw na, una, kaagad bago ito sa rehistro cr0 isang recording ang ginawa - gamit din ang isang helper - na inilipat ang processor sa protected mode, at pangalawa, na ang js version ay hindi kailanman lumipat sa protected mode. Ngunit ang katotohanan ay ang isa pang tampok ng Emscripten ay ang pag-aatubili nitong tiisin ang code tulad ng pagpapatupad ng mga tagubilin call sa TCI, na nagreresulta sa uri ng anumang function pointer long long f(int arg0, .. int arg9) - Dapat tawagan ang mga function na may tamang bilang ng mga argumento. Kung nilabag ang panuntunang ito, depende sa mga setting ng pag-debug, ang program ay maaaring mag-crash (na kung saan ay mabuti) o tatawag sa maling function sa lahat (na kung saan ay malungkot sa pag-debug). Mayroon ding pangatlong opsyon - paganahin ang henerasyon ng mga wrapper na nagdaragdag / nag-aalis ng mga argumento, ngunit sa kabuuan ang mga balot na ito ay tumatagal ng maraming espasyo, sa kabila ng katotohanan na sa katunayan kailangan ko lamang ng kaunti pa sa isang daang mga balot. Ito lamang ay napakalungkot, ngunit nagkaroon ng mas malubhang problema: sa nabuong code ng mga function ng wrapper, ang mga argumento ay na-convert at na-convert, ngunit kung minsan ang function na may nabuong mga argumento ay hindi tinawag - mabuti, tulad ng sa ang aking pagpapatupad ng libffi. Ibig sabihin, hindi pinatay ang ilang katulong.

Sa kabutihang palad, ang Qemu ay may mga listahan ng mga katulong na nababasa ng makina sa anyo ng isang header file tulad ng

DEF_HELPER_0(lock, void)
DEF_HELPER_0(unlock, void)
DEF_HELPER_3(write_eflags, void, env, tl, i32)

Ang mga ito ay ginagamit na medyo nakakatawa: una, ang mga macro ay muling tinukoy sa pinaka kakaibang paraan DEF_HELPER_n, at pagkatapos ay i-on helper.h. Sa lawak na ang macro ay pinalawak sa isang structure initializer at isang kuwit, at pagkatapos ay isang array ay tinukoy, at sa halip na mga elemento - #include <helper.h> Bilang resulta, sa wakas ay nagkaroon ako ng pagkakataon na subukan ang library sa trabaho pyparsing, at isang script ang isinulat na bumubuo ng eksaktong mga wrapper na iyon para sa eksaktong mga function kung saan kinakailangan ang mga ito.

At kaya, pagkatapos na ang processor ay tila gumagana. Tila ito ay dahil ang screen ay hindi nasimulan, kahit na ang memtest86+ ay nagawang tumakbo sa katutubong pagpupulong. Dito kinakailangan na linawin na ang Qemu block I/O code ay nakasulat sa mga coroutine. Ang Emscripten ay may sarili nitong napakahirap na pagpapatupad, ngunit kailangan pa rin itong suportahan sa Qemu code, at maaari mong i-debug ang processor ngayon: Sinusuportahan ng Qemu ang mga opsyon -kernel, -initrd, -append, kung saan maaari kang mag-boot ng Linux o, halimbawa, memtest86+, nang hindi gumagamit ng mga block device. Ngunit narito ang problema: sa katutubong pagpupulong makikita ang Linux kernel output sa console na may opsyon -nographic, at walang output mula sa browser patungo sa terminal kung saan ito inilunsad emrun, hindi dumating. Iyon ay, hindi malinaw: ang processor ay hindi gumagana o ang graphics output ay hindi gumagana. At pagkatapos ay sumagi sa isip ko na maghintay ng kaunti. Ito ay lumabas na "ang processor ay hindi natutulog, ngunit simpleng kumikislap ng dahan-dahan," at pagkatapos ng mga limang minuto ang kernel ay naghagis ng isang grupo ng mga mensahe sa console at patuloy na nag-hang. Naging malinaw na ang processor, sa pangkalahatan, ay gumagana, at kailangan nating maghukay sa code para sa pagtatrabaho sa SDL2. Sa kasamaang palad, hindi ko alam kung paano gamitin ang library na ito, kaya sa ilang lugar kailangan kong kumilos nang random. Sa ilang mga punto, ang linya parallel0 ay nag-flash sa screen sa isang asul na background, na nagmungkahi ng ilang mga saloobin. Sa huli, lumabas na ang problema ay ang Qemu ay nagbubukas ng ilang mga virtual na bintana sa isang pisikal na window, kung saan maaari kang lumipat gamit ang Ctrl-Alt-n: gumagana ito sa katutubong build, ngunit hindi sa Emscripten. Matapos mapupuksa ang mga hindi kinakailangang bintana gamit ang mga pagpipilian -monitor none -parallel none -serial none at mga tagubilin na puwersahang i-redraw ang buong screen sa bawat frame, biglang gumana ang lahat.

Mga Coroutine

Kaya, gumagana ang pagtulad sa browser, ngunit hindi ka maaaring magpatakbo ng anumang kawili-wiling single-floppy dito, dahil walang block I/O - kailangan mong magpatupad ng suporta para sa mga coroutine. Ang Qemu ay mayroon nang ilang mga coroutine backend, ngunit dahil sa likas na katangian ng JavaScript at ang Emscripten code generator, hindi ka maaaring magsimulang mag-juggling ng mga stack. Tila "wala na ang lahat, tinatanggal ang plaster," ngunit ang mga developer ng Emscripten ay nag-asikaso na sa lahat. Ito ay ipinatupad na medyo nakakatawa: tawagan natin ang isang function na tawag na tulad nitong kahina-hinala emscripten_sleep at ilang iba pa gamit ang mekanismo ng Asyncify, pati na rin ang mga pointer na tawag at tawag sa anumang function kung saan ang isa sa nakaraang dalawang kaso ay maaaring mangyari sa ibaba ng stack. At ngayon, bago ang bawat kahina-hinalang tawag, pipili kami ng kontekstong async, at kaagad pagkatapos ng tawag, susuriin namin kung may naganap na asynchronous na tawag, at kung mayroon, ise-save namin ang lahat ng lokal na variable sa kontekstong ito ng async, ipahiwatig kung aling function upang ilipat ang kontrol sa kung kailan kailangan nating magpatuloy sa pagpapatupad , at lumabas sa kasalukuyang function. Ito ay kung saan mayroong saklaw para sa pag-aaral ng epekto pagwawaldas β€” para sa mga pangangailangan ng patuloy na pagpapatupad ng code pagkatapos bumalik mula sa isang asynchronous na tawag, ang compiler ay bumubuo ng "mga stub" ng function na nagsisimula pagkatapos ng isang kahina-hinalang tawag - tulad nito: kung mayroong n kahina-hinalang tawag, ang function ay lalawak sa isang lugar n/2 beses β€” ito pa rin, kung hindi Tandaan na pagkatapos ng bawat potensyal na asynchronous na tawag, kailangan mong magdagdag ng pag-save ng ilang lokal na variable sa orihinal na function. Kasunod nito, kinailangan ko pang magsulat ng isang simpleng script sa Python, na, batay sa isang naibigay na hanay ng mga partikular na labis na paggamit ng mga function na parang "hindi pinapayagan ang asynchrony na dumaan sa kanilang mga sarili" (iyon ay, stack promotion at lahat ng inilarawan ko ay hindi gumana sa kanila), ay nagpapahiwatig ng mga tawag sa pamamagitan ng mga pointer kung saan ang mga function ay dapat na hindi papansinin ng compiler upang ang mga function na ito ay hindi maituturing na asynchronous. At pagkatapos ay ang mga JS file na wala pang 60 MB ay malinaw na napakarami - sabihin nating hindi bababa sa 30. Bagama't, minsan ay nagse-set up ako ng script ng pagpupulong, at hindi sinasadyang natapon ang mga opsyon sa linker, na kung saan ay -O3. Pinapatakbo ko ang nabuong code, at ang Chromium ay kumakain ng memorya at nag-crash. Pagkatapos ay hindi ko sinasadyang tumingin sa kung ano ang sinusubukan niyang i-download ... Well, ano ang maaari kong sabihin, ako ay nag-freeze din kung ako ay hihilingin sa akin na maingat na pag-aralan at i-optimize ang isang 500+ MB Javascript.

Sa kasamaang palad, ang mga tseke sa Asyncify support library code ay hindi ganap na kaaya-aya longjmp-s na ginagamit sa virtual processor code, ngunit pagkatapos ng isang maliit na patch na hindi pinapagana ang mga pagsusuring ito at pilit na nagpapanumbalik ng mga konteksto na parang maayos ang lahat, gumana ang code. At pagkatapos ay nagsimula ang isang kakaibang bagay: kung minsan ang mga tseke sa synchronization code ay na-trigger - ang parehong mga nag-crash sa code kung, ayon sa lohika ng pagpapatupad, dapat itong mai-block - sinubukan ng isang tao na kunin ang isang nakuha nang mutex. Sa kabutihang palad, ito ay naging hindi isang lohikal na problema sa serialized code - ginagamit ko lang ang karaniwang pangunahing pag-andar ng loop na ibinigay ng Emscripten, ngunit kung minsan ang asynchronous na tawag ay ganap na magbubukas ng stack, at sa sandaling iyon ay mabibigo ito. setTimeout mula sa pangunahing loop - kaya, ang code ay pumasok sa pangunahing loop na pag-ulit nang hindi umaalis sa nakaraang pag-ulit. Muling isinulat sa isang walang katapusang loop at emscripten_sleep, at huminto ang mga problema sa mga mutex. Ang code ay naging mas lohikal - pagkatapos ng lahat, sa katunayan, wala akong ilang code na naghahanda sa susunod na frame ng animation - kinakalkula lamang ng processor ang isang bagay at pana-panahong ina-update ang screen. Gayunpaman, ang mga problema ay hindi tumigil doon: kung minsan ang pagpapatupad ng Qemu ay tahimik lamang na matatapos nang walang anumang mga eksepsiyon o mga pagkakamali. Sa sandaling iyon ay sumuko ako dito, ngunit, sa hinaharap, sasabihin ko na ang problema ay ito: ang coroutine code, sa katunayan, ay hindi gumagamit ng setTimeout (o hindi bababa sa hindi kasingdalas ng iniisip mo): function emscripten_yield itinatakda lang ang flag ng asynchronous na tawag. Ang buong punto ay iyon emscripten_coroutine_next ay hindi isang asynchronous na function: sa loob nito sinusuri ang flag, nire-reset ito at naglilipat ng kontrol sa kung saan ito kinakailangan. Iyon ay, ang pag-promote ng stack ay nagtatapos doon. Ang problema ay dahil sa use-after-free, na lumitaw noong hindi pinagana ang coroutine pool dahil sa hindi ko kinopya ang isang mahalagang linya ng code mula sa kasalukuyang coroutine backend, ang function qemu_in_coroutine ibinalik na totoo kapag sa katunayan ito ay dapat na nagbalik ng mali. Ito ay humantong sa isang tawag emscripten_yield, kung saan walang tao sa stack emscripten_coroutine_next, ang stack ay bumungad sa pinakatuktok, ngunit hindi setTimeout, tulad ng sinabi ko na, ay hindi ipinakita.

Pagbuo ng JavaScript code

At narito, sa katunayan, ang ipinangakong "ibabalik ang tinadtad na karne." Hindi naman. Siyempre, kung patakbuhin namin ang Qemu sa browser, at Node.js dito, natural, pagkatapos ng pagbuo ng code sa Qemu ay makakakuha kami ng ganap na maling JavaScript. Ngunit gayon pa man, ilang uri ng reverse transformation.

Una, kaunti tungkol sa kung paano gumagana ang Qemu. Mangyaring patawarin ako kaagad: Hindi ako isang propesyonal na developer ng Qemu at maaaring mali ang aking mga konklusyon sa ilang lugar. Tulad ng sinasabi nila, "ang opinyon ng mag-aaral ay hindi kailangang tumugma sa opinyon ng guro, ang axiomatics at sentido komun ni Peano." Ang Qemu ay may tiyak na bilang ng mga sinusuportahang arkitektura ng panauhin at para sa bawat isa ay may katulad na direktoryo target-i386. Kapag nagtatayo, maaari mong tukuyin ang suporta para sa ilang mga arkitektura ng bisita, ngunit ang resulta ay ilang binary lamang. Ang code upang suportahan ang arkitektura ng bisita, sa turn, ay bumubuo ng ilang panloob na pagpapatakbo ng Qemu, na ang TCG (Tiny Code Generator) ay nagiging code ng makina para sa arkitektura ng host. Gaya ng nakasaad sa readme file na matatagpuan sa tcg directory, ito ay orihinal na bahagi ng isang regular na C compiler, na kalaunan ay inangkop para sa JIT. Samakatuwid, halimbawa, ang target na arkitektura sa mga tuntunin ng dokumentong ito ay hindi na isang arkitektura ng bisita, ngunit isang arkitektura ng host. Sa ilang mga punto, lumitaw ang isa pang bahagi - Tiny Code Interpreter (TCI), na dapat magsagawa ng code (halos parehong mga panloob na operasyon) sa kawalan ng isang generator ng code para sa isang partikular na arkitektura ng host. Sa katunayan, gaya ng isinasaad ng dokumentasyon nito, maaaring hindi palaging gumaganap ang interpreter na ito bilang isang generator ng JIT code, hindi lamang sa dami sa mga tuntunin ng bilis, kundi pati na rin sa qualitatively. Kahit na hindi ako sigurado kung ang kanyang paglalarawan ay ganap na nauugnay.

Sa una sinubukan kong gumawa ng isang ganap na backend ng TCG, ngunit mabilis na nalito sa source code at isang hindi lubos na malinaw na paglalarawan ng mga tagubilin sa bytecode, kaya nagpasya akong balutin ang TCI interpreter. Nagbigay ito ng ilang mga pakinabang:

  • kapag nagpapatupad ng code generator, hindi mo maaaring tingnan ang paglalarawan ng mga tagubilin, ngunit sa interpreter code
  • maaari kang bumuo ng mga function hindi para sa bawat bloke ng pagsasalin na nakatagpo, ngunit, halimbawa, pagkatapos lamang ng ika-XNUMX na pagpapatupad
  • kung ang nabuong code ay nagbabago (at ito ay tila posible, ayon sa mga pag-andar na may mga pangalan na naglalaman ng salitang patch), kakailanganin kong pawalang-bisa ang nabuong JS code, ngunit hindi bababa sa mayroon akong isang bagay upang muling buuin ito mula sa

Tungkol sa pangatlong punto, hindi ako sigurado na posible ang pag-patch pagkatapos maipatupad ang code sa unang pagkakataon, ngunit sapat na ang unang dalawang puntos.

Sa una, ang code ay nabuo sa anyo ng isang malaking switch sa address ng orihinal na pagtuturo ng bytecode, ngunit pagkatapos, ang pag-alala sa artikulo tungkol sa Emscripten, pag-optimize ng nabuong JS at relooping, nagpasya akong bumuo ng mas maraming code ng tao, lalo na dahil ito ay empirically. lumabas na ang tanging entry point sa translation block ay ang Start nito. Sa lalong madaling panahon sinabi kaysa tapos na, pagkaraan ng ilang sandali nagkaroon kami ng isang generator ng code na nakabuo ng code na may mga ifs (kahit na walang mga loop). Ngunit malas, ito ay bumagsak, na nagbibigay ng isang mensahe na ang mga tagubilin ay medyo hindi tama ang haba. Bukod dito, ang huling pagtuturo sa antas ng recursion na ito ay brcond. Okay, magdaragdag ako ng kaparehong tseke sa henerasyon ng pagtuturo na ito bago at pagkatapos ng recursive na tawag at... wala ni isa sa mga ito ang naisakatuparan, ngunit pagkatapos ng assert switch ay nabigo pa rin sila. Sa huli, pagkatapos pag-aralan ang nabuong code, napagtanto ko na pagkatapos ng switch, ang pointer sa kasalukuyang pagtuturo ay nire-reload mula sa stack at malamang na ma-overwrite ng nabuong JavaScript code. At kaya pala. Ang pagtaas ng buffer mula sa isang megabyte hanggang sampu ay hindi humantong sa anumang bagay, at naging malinaw na ang generator ng code ay tumatakbo sa mga bilog. Kinailangan naming suriin na hindi kami lumampas sa mga hangganan ng kasalukuyang TB, at kung ginawa namin, pagkatapos ay ibigay ang address ng susunod na TB na may minus sign upang maipagpatuloy namin ang pagpapatupad. Bilang karagdagan, nalulutas nito ang problema "aling mga nabuong function ang dapat na hindi wasto kung nagbago ang piraso ng bytecode na ito?" β€” tanging ang function na tumutugma sa translation block na ito ang kailangang ma-invalidate. Oo nga pala, bagama't na-debug ko ang lahat sa Chromium (dahil gumagamit ako ng Firefox at mas madali para sa akin na gumamit ng hiwalay na browser para sa mga eksperimento), tinulungan ako ng Firefox na itama ang mga hindi pagkakatugma sa pamantayan ng asm.js, pagkatapos nito ay nagsimulang gumana nang mas mabilis ang code sa Chromium.

Halimbawa ng nabuong code

Compiling 0x15b46d0:
CompiledTB[0x015b46d0] = function(stdlib, ffi, heap) {
"use asm";
var HEAP8 = new stdlib.Int8Array(heap);
var HEAP16 = new stdlib.Int16Array(heap);
var HEAP32 = new stdlib.Int32Array(heap);
var HEAPU8 = new stdlib.Uint8Array(heap);
var HEAPU16 = new stdlib.Uint16Array(heap);
var HEAPU32 = new stdlib.Uint32Array(heap);

var dynCall_iiiiiiiiiii = ffi.dynCall_iiiiiiiiiii;
var getTempRet0 = ffi.getTempRet0;
var badAlignment = ffi.badAlignment;
var _i64Add = ffi._i64Add;
var _i64Subtract = ffi._i64Subtract;
var Math_imul = ffi.Math_imul;
var _mul_unsigned_long_long = ffi._mul_unsigned_long_long;
var execute_if_compiled = ffi.execute_if_compiled;
var getThrew = ffi.getThrew;
var abort = ffi.abort;
var qemu_ld_ub = ffi.qemu_ld_ub;
var qemu_ld_leuw = ffi.qemu_ld_leuw;
var qemu_ld_leul = ffi.qemu_ld_leul;
var qemu_ld_beuw = ffi.qemu_ld_beuw;
var qemu_ld_beul = ffi.qemu_ld_beul;
var qemu_ld_beq = ffi.qemu_ld_beq;
var qemu_ld_leq = ffi.qemu_ld_leq;
var qemu_st_b = ffi.qemu_st_b;
var qemu_st_lew = ffi.qemu_st_lew;
var qemu_st_lel = ffi.qemu_st_lel;
var qemu_st_bew = ffi.qemu_st_bew;
var qemu_st_bel = ffi.qemu_st_bel;
var qemu_st_leq = ffi.qemu_st_leq;
var qemu_st_beq = ffi.qemu_st_beq;

function tb_fun(tb_ptr, env, sp_value, depth) {
  tb_ptr = tb_ptr|0;
  env = env|0;
  sp_value = sp_value|0;
  depth = depth|0;
  var u0 = 0, u1 = 0, u2 = 0, u3 = 0, result = 0;
  var r0 = 0, r1 = 0, r2 = 0, r3 = 0, r4 = 0, r5 = 0, r6 = 0, r7 = 0, r8 = 0, r9 = 0;
  var r10 = 0, r11 = 0, r12 = 0, r13 = 0, r14 = 0, r15 = 0, r16 = 0, r17 = 0, r18 = 0, r19 = 0;
  var r20 = 0, r21 = 0, r22 = 0, r23 = 0, r24 = 0, r25 = 0, r26 = 0, r27 = 0, r28 = 0, r29 = 0;
  var r30 = 0, r31 = 0, r41 = 0, r42 = 0, r43 = 0, r44 = 0;
    r14 = env|0;
    r15 = sp_value|0;
  START: do {
    r0 = HEAPU32[((r14 + (-4))|0) >> 2] | 0;
    r42 = 0;
    result = ((r0|0) != (r42|0))|0;
    HEAPU32[1445307] = r0;
    HEAPU32[1445321] = r14;
    if(result|0) {
    HEAPU32[1445322] = r15;
    return 0x0345bf93|0;
    }
    r0 = HEAPU32[((r14 + (16))|0) >> 2] | 0;
    r42 = 8;
    r0 = ((r0|0) - (r42|0))|0;
    HEAPU32[(r14 + (16)) >> 2] = r0;
    r1 = 8;
    HEAPU32[(r14 + (44)) >> 2] = r1;
    r1 = r0|0;
    HEAPU32[(r14 + (40)) >> 2] = r1;
    r42 = 4;
    r0 = ((r0|0) + (r42|0))|0;
    r2 = HEAPU32[((r14 + (24))|0) >> 2] | 0;
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    HEAPU32[1445309] = r2;
    HEAPU32[1445321] = r14;
    HEAPU32[1445322] = r15;
    qemu_st_lel(env|0, r0|0, r2|0, 34, 22759218);
if(getThrew() | 0) abort();
    r0 = 3241038392;
    HEAPU32[1445307] = r0;
    r0 = qemu_ld_leul(env|0, r0|0, 34, 22759233)|0;
if(getThrew() | 0) abort();
    HEAPU32[(r14 + (24)) >> 2] = r0;
    r1 = HEAPU32[((r14 + (12))|0) >> 2] | 0;
    r2 = HEAPU32[((r14 + (40))|0) >> 2] | 0;
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    HEAPU32[1445309] = r2;
    qemu_st_lel(env|0, r2|0, r1|0, 34, 22759265);
if(getThrew() | 0) abort();
    r0 = HEAPU32[((r14 + (24))|0) >> 2] | 0;
    HEAPU32[(r14 + (40)) >> 2] = r0;
    r1 = 24;
    HEAPU32[(r14 + (52)) >> 2] = r1;
    r42 = 0;
    result = ((r0|0) == (r42|0))|0;
    if(result|0) {
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    }
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    return execute_if_compiled(22759392|0, env|0, sp_value|0, depth|0) | 0;
    return execute_if_compiled(23164080|0, env|0, sp_value|0, depth|0) | 0;
    break;
  } while(1); abort(); return 0|0;
}
return {tb_fun: tb_fun};
}(window, CompilerFFI, Module.buffer)["tb_fun"]

Konklusyon

Kaya, hindi pa rin natatapos ang trabaho, ngunit pagod na akong palihim na dalhin ang pangmatagalang konstruksiyon sa pagiging perpekto. Samakatuwid, nagpasya akong i-publish kung ano ang mayroon ako sa ngayon. Medyo nakakatakot ang code sa mga lugar, dahil isa itong eksperimento, at hindi malinaw nang maaga kung ano ang kailangang gawin. Marahil, sulit na maglabas ng mga normal na atomic commit sa itaas ng ilang mas modernong bersyon ng Qemu. Pansamantala, mayroong isang thread sa Gita sa isang format ng blog: para sa bawat "antas" na kahit papaano ay naipasa, isang detalyadong komentaryo sa Russian ang idinagdag. Sa totoo lang, ang artikulong ito sa isang malaking lawak ay muling pagsasalaysay ng konklusyon git log.

Maaari mong subukan ang lahat dito (mag-ingat sa trapiko).

Ano ang gumagana na:

  • x86 virtual processor na tumatakbo
  • Mayroong gumaganang prototype ng JIT code generator mula sa machine code hanggang sa JavaScript
  • Mayroong isang template para sa pag-assemble ng iba pang 32-bit na mga arkitektura ng bisita: sa ngayon ay maaari mong humanga sa Linux para sa pagyeyelo ng arkitektura ng MIPS sa browser sa yugto ng paglo-load

Ano pa ang magagawa mo

  • Pabilisin ang pagtulad. Kahit na sa JIT mode ito ay tila mas mabagal kaysa sa Virtual x86 (ngunit may potensyal na isang buong Qemu na may maraming emulated na hardware at arkitektura)
  • Upang makagawa ng isang normal na interface - sa totoo lang, hindi ako isang mahusay na web developer, kaya sa ngayon ay ginawa kong muli ang karaniwang Emscripten shell sa abot ng aking makakaya.
  • Subukang maglunsad ng mas kumplikadong mga function ng Qemu - networking, VM migration, atbp.
  • UPS: kakailanganin mong isumite ang iyong ilang mga pag-unlad at ulat ng bug sa Emscripten upstream, tulad ng ginawa ng mga nakaraang porter ng Qemu at iba pang mga proyekto. Salamat sa kanila sa kakayahang magamit nang tahasan ang kanilang kontribusyon sa Emscripten bilang bahagi ng aking gawain.

Pinagmulan: www.habr.com

Magdagdag ng komento