Qemu.js me mbështetje JIT: mishin gjithsesi mund ta kthejmë mbrapsht

Несколько лет назад Фабрис Беллар написал jslinux — эмулятор ПК, написанный на JavaScript. После этого был ещё как минимум Virtual x86. Но все они, насколько мне известно, являлись интерпретаторами, в то время как написанный значительно раньше тем же Фабрисом Белларом Qemu, да и, наверное, любой уважающий себя современный эмулятор, использует JIT-компиляцию гостевого кода в код хостовой системы. Мне показалось, что самое время реализовать обратную задачу по отношению к той, которую решают браузеры: JIT-компиляцию машинного кода в JavaScript, для чего логичнее всего виделось портировать Qemu. Казалось бы, почему именно Qemu, есть же более простые и user-friendly эмуляторы — тот же VirtualBox, например — поставил и работает. Но у Qemu есть несколько интересных особенностей

  • открытые исходники
  • возможность работать без драйвера ядра
  • возможность работать в режиме интерпретатора
  • поддержка большого количества как хостовых, так и гостевых архитектур

На счёт третьего пункта теперь-то я уже могу пояснить, что на самом деле в режиме TCI интерпретируются не сами гостевые машинные инструкции, а полученный из них байткод, но сути это не меняет — чтобы собрать и запустить Qemu на новой архитектуре, если повезёт, достаточно компилятора C — написание кодогенератора можно отложить.

И вот, после двух лет неспешного ковыряния в свободное время исходников Qemu появился работающий прототип, в котором уже можно запустить, например, Kolibri OS.

Что такое Emscripten

В наше время появилось много компиляторов, конечным результатом работы которых является JavaScript. Некоторые, такие как Type Script, изначально задумывались как лучший способ писать для веба. В то же время, Emscripten — это способ взять существующий код на C или C++, и скомпилировать его в вид, понятный браузеру. На këtë faqe собрано немало портов известных программ: këtu, например, можно посмотреть на PyPy — кстати, как утверждается, у них уже есть JIT. На самом деле, не любую программу можно просто скомпилировать и запустить в браузере — есть ряд karakteristikat, с которыми приходится мириться, впрочем, как гласит надпись на этой же странице "Emscripten can be used to compile almost any portable C/C++ code to JavaScript". То есть существует ряд операций, которые являются неопределённым поведением по стандарту, но обычно работают на x86 — к примеру, невыровненный доступ к переменным, который на некоторых архитектурах вообще запрещён. В общем, Qemu — программа кроссплатформенная и, хотелось верить, и так не содержит большого количества неопредлённого поведения — бери и компилируй, потом немного повозиться с JIT — и говото! Но не тут-то было…

Prova e parë

Вообще говоря, я не первый, кому пришла в голову идея портировать Qemu на JavaScript. На форуме ReactOS задавался вопрос, возможно ли это с помощью Emscripten. Ещё раньше ходили слухи, что это сделал лично Фабрис Беллар, но речь шла о jslinux, который, насколько мне известно, является как раз попыткой вручную добиться на JS достаточной производительности, и написан с нуля. Позднее был написан Virtual x86 — к нему были выложены необфусцированные исходники, и, как утверждалась, большая "реалистичность" эмуляции позволила использовать SeaBIOS в качестве firmware. Кроме того, была как минимум одна попытка портировать Qemu с помощью Emscripten — это пытался сделать socketpair, но разработка, насколько я понял, была заморожена.

Итак, казалось бы, вот исходники, вот Emscripten — бери и компилируй. Но есть ещё и библиотеки, от которых Qemu зависит, и библиотеки от которых зависят те библиотеки и т.д., причём одна из них — libffi, от которой зависит glib. В интернете находились слухи, что в большой коллекции портов библиотек под Emscripten есть и она, но верилось как-то с трудом: во-первых, новым компилятором она не собиралась, во-вторых, это слишком низкоуровневая библиотека, чтобы просто так взять, и скомпилироваться в JS. И дело даже не только в ассемблерных вставках — наверное, если извратиться, то для некоторых calling conventions можно и без них сформировать нужные аргументы на стеке и вызвать функцию. Вот только Emscripten — штуковина хитрая: для того, чтобы сгенерированный код выглядел привычно для оптимизатора JS-движка браузера, используются некоторые трюки. В частности, так называемый relooping — кодогенератор по полученному LLVM IR с некими абстрактными инструкциями переходов пытается воссоздать правдоподобные if-ы, циклы и т.д. Ну а аргументы в функции передаются как? Естественно, как аргументы JS-функций, то есть по возможности не через стек.

Në fillim mendova thjesht të shkruaj një zëvendësim të libffi në JS dhe të kaloj testet standarde, por përfundimisht u ngatërrova se si të krijoj skedarët e mi të titujve për t'u punuar me kodin ekzistues — çfarë të bëj, siç thuhet, "Ajo ose janë detyrat kaq të komplikuara, ose ne jemi kaq të kufizuar". Më duhej të portoja libffi në një arkitekturë tjetër, nëse mund ta shpreh kështu — fatmirësisht, në Emscripten ka si makro për asamblimin inline (në JavaScript, po — cila arkitekturë, njësoj dhe asamble), ashtu edhe mundësinë për të ekzekutuar kod të gjeneruar në fluks. Pra, pas një kohe të kaluar me fragmente të varura nga platforma të libffi, arrita të krijoj një kod të kompilueshëm dhe e testova atë në testin e parë që më ra në dorë. Për habinë time, testi kaloi me sukses. Duke u mahnitur nga gjenialiteti im — është një shaka, funksionoi në përpjekjen e parë — unë, ende pa e besuar sytë e mi, shkuam për të parë sërish kodin e krijuar, për të vlerësuar se ku duhet të vazhdoj. Atëherë u mahnita sërish — e vetmja gjë që bënte funksioni im ffi_call — ishte të raportonte një thirrje të suksesshme. Vetë thirrja nuk ndodhi. Kështu dërgova kërkesën time të parë për tërheqje, e cila rregullonte një gabim të njohur për çdo olimpist në test — numrat realë nuk duhet të krahasohen si a == b as përmes a - b < EPS — duhet të mos harrojmë as modulin, pasi që 0 do të rezultonte shumë i barabartë me 1/3… Pra, më rezultoi një port i libffi, i cili kalon testet më të thjeshta dhe me të cilin kompilohen glib — vendosa se, do të shtoj më vonë nëse është e nevojshme. Duke u shkuar përpara, duhet të them se, siç doli, kompjuterët nuk e përfshinin kodin përfundimtar të funksionit libffi.

Por, siç kam thënë, ka disa kufizime, dhe mes përdorimit të lirë të sjelljeve të pa përkufizuara, u fut një karakteristikë më pak e këndshme — JavaScript me dizajn nuk mbështet shumë-procesimin me kujtesë të përbashkët. Në parim, kjo zakonisht mund të quhet një ide e mirë, por jo për portimin e kodit, i cili është e ndërlidhur me proceset e C. Në të vërtetë, në Firefox po bëhen eksperimentime për mbështetje të punonjësve të ndarë, dhe implementimi i pthread për ta është i pranishëm në Emscripten, por nuk doja të varen nga kjo. Më duhej të filloja ngadalë të heq shumë-procesimin nga kodi i Qemu — dmth, të gjej se ku nisnin proceset, të nxjerr trupin e ciklit, që ekzekutohej në këtë proces në një funksion të veçantë, dhe të thërrisja për një herë të tillë nga cikli kryesor.

Përpjekja e dytë

Në një moment bëhet e qartë se nuk po arrij asgjë dhe se shpërndarja e pa sistemuar e dobësive nuk do të çojë askund. Nga kjo dalim: nevojitet ndonjë mënyrë për të sistematizuar procesin e shtimit të dobësive. Prandaj, u mor versioni më i ri në atë kohë 2.4.1 (jo 2.5.0, sepse, kush e di, atje mund të ketë akoma disa gabime të pa kapura të versionit të ri, dhe unë kam mjaft gabimet e mia), dhe në fillim u rishkrua në një mënyrë të sigurt thread-posix.c. Pra, si e sigurt: nëse dikush përpiqej të kryente një operacion që çonte në bllokim, menjëherë thirrej funksioni abort() — sigurisht, kjo nuk e zgjidhte menjëherë të gjitha problemet, por së paku, kjo ishte më e këndshme se sa të merrja në heshtje një konsistencë të dhënash të munguar.

Gjithsesi, në portimin e kodit në JS ndihmojnë shumë opsionet Emscripten -s ASSERTIONS=1 -s SAFE_HEAP=1 — ato kapin disa lloje sjelljesh të pa përkufizuara siç janë qasjet në adresa të pa përkulura (që aspak nuk përputhet me kodin për arrays të tipizuara si HEAP32[addr >> 2] = 1) ose thirrjen e një funksioni me një numër të gabuar argumentesh.

Përsa i përket gabimeve të përkuljes — është një temë e veçantë. Siç kam përmendur, në Qemu ka një "të shpërndarë" interpretuese të kodit TCI (tiny code interpreter), dhe për të ndërtuar dhe ekzekutuar Qemu në një arkitekturë të re, nëse ke fat, mjafton një kompjuter C. Fjalët kyçe "nëse ke fat". Mua nuk më ndodhi, dhe rezultoi se TCI gjatë analizës së kodit të tij të bajtëve përdor qasje të pa përkulura. D.m.th., në arkitektura të tilla si ARM e të tjera me qasje të detyrueshme të përkulur, Qemu kompiloheshin sepse për to kishte një backend normal TCG, që gjeneronte kod natyror, dhe do të funksionojë TCI aty — kjo është akoma një pyetje. Megjithatë, siç rezultoi, në dokumentacionin e TCI kishte diçka të ngjashme. Në fund, ishin shtuar thirrjet për funksione për leximin e pa përkulur, të cilat u zbuluan në një pjesë tjetër të Qemu.

Shkatërrimi i heap

Në fund, qasja e pa përkulur në TCI u rregullua, u krijua cikli kryesor, që thirri njëri pas tjetrit procesorin, RCU dhe disa gjëra të vogla. Dhe kështu po e nis Qemu me opsionin -d exec,in_asm,out_asm, që do të thotë se duhet të flasim se cilat blloqe kodi ekzekutohen, si dhe në momentin e përkthimit të shkruajmë, cili kod i mysafirëve ishte, cili kod dhe hosti u bë (në këtë rast, bytecode). Ajo niset, ekzekuton disa blloqe përkthimi, shkruan mesazhin tim të lënë për debuggim, që tani do të niset RCU dhe... rrëzohet nga abort() brenda funksionit free(). Nëpërmjet gërmimit të funksionit free() arrita të zbuloj se në titullin e bllokut të grumbullimit, i cili ndodhet në tetë byte, që i paraprijnë memorjes të ndarë, në vend të madhësisë së bllokut ose diçkash të ngjashme kishte mbeturinë.

Shkatërrimi i grumbullimit — sa për të bukur… Në një rast të tillë ka një mjet të dobishëm — të mbledhësh një binar natyror (nëse është e mundur) nga të njëjtat kode burimi dhe ta kalosh nën Valgrind. Pas pak kohe binari ishte gati. E nisa me të njëjtat opsione — qëndroi sipër dhe rrëzohet edhe gjatë inicializimit, pa arritur në, me të vërtetë, ekzekutimin. Sigurisht, është e pakëndshme — duket se kodet burimore nuk ishin saktësisht ato të njëjtat, e cila nuk është e habitshme, sepse ndihma konfiguroi disa opsione të tjera, por unë kam Valgrind — fillimisht do ta rregulloj këtë defekt, dhe pastaj, nëse kam fat, ndoshta do të dalë edhe burimi origjinal. E nisa të njëjtën gjë nën Valgrind… Oooh, pra, ai nisi, kaloi normalisht inicializimin dhe shkoi më tej pa asnjë paralajmërim për qasje të gabuar në memorje, e mos flasim për rrëzime. Kësaj jeta, siç thotë, nuk më kishte përgatitur — programi që rrëzohej nuk rrëzohej kur niste nën Valgrind. Çfarë ishte kjo — një enigmë. Hipoteza ime është se pas instrukcionit aktual, pas një rënie gjatë inicializimit, gdb tregoi se funksiononte memset-a me një tregues të vlefshëm, duke përdorur ndoshta mmx, ndoshta xmm regjistrat, ndoshta kjo ishte një gabim ndarje, megjithatë gjithmonë besohet me doza të vogla.

O-kay, Valgrind këtu, duket nuk është ndihmës. Dhe këtu filloi gjithçka më e bezdisshme — gjithçka, duket, madje fillon, por rrëzohet për arsye krejtësisht të panjohura për një ngjarje që mund të ketë ndodhur miliona instrukcione më parë. Për një kohë të gjatë nuk ishte e qartë se si të afrohesh. Në fund, nuk më mbeti veçse të ulem dhe të debugg. Shtypja e asaj që kishte shkruar titulli tregoi, se kjo duket jo si një numër, por, përkundrazi, ka të bëjë me disa të dhëna binarë. Dhe, oh mrekulli, kjo varg binar u gjet në skedarin e BIOS-it — dmth tani mund të thuash me mjaft siguri se ishte një mbushje buffer dhe edhe e kuptueshme se çfarë ishte shkruar në këtë buffer. Pastaj si ndodhi — në Emscripten, fatmirësisht, nuk ka rastësie të hapësirës adresuese, siç nuk ka edhe vrima në të, prandaj mund të shkruash diku në mes të kodit nxjerrjen e të dhënave përmes treguesit nga nisma e kaluar, të shohësh të dhënat, të shohësh treguesin dhe, nëse ai nuk ka ndryshuar, të marrësh informacion për të reflektuar. Ama, në lidhje me lidhjen pas çdo ndryshimi shpenzohen disa minuta, por çfarë mund të bësh. Si rezultat, u gjet vargu specifik që kopjon BIOS-in nga buffer i përkohshëm në memorjen e mysafirëve — dhe, me të vërtetë, bufferi nuk kishte hapësirë të mjaftueshme. Kërkimi i burimit të asaj adrese të çuditshme të bufferit çoi në funksionin qemu_anon_ram_alloc në skedarin oslib-posix.c — logjika aty ishte e tillë: ndonjëherë mund të jetë e dobishme të anashkalosh adresën sipas faqeve të mëdha me madhësi 2 MB, për këtë do të kërkojmë prej mmap fillimisht pak më shumë, pastaj do ta kthejmë atë të tepërt me ndihmën e munmap. Dhe nëse një përputhje e tillë nuk është e nevojshme, atëherë do të përcaktojmë në vend të 2 MB rezultatin getpagesize()mmap prapë do të japë një adresë të përputhur… Prandaj, në Emscripten mmap thjesht thërret malloc, dhe ai, natyrisht, nuk i del sipas faqes. Në përgjithësi, një defekt që më shqetësonte për disa muaj, u zgjidh duke ndryshuar në dy rreshtat.

Veçoritë e thirrjes së funksioneve

Dhe tani procesori po llogarit diçka, Qemu nuk rrëzohet, por ekrani nuk ndizet, dhe procesori shpejt bllokohet, sipas daljes -d exec,in_asm,out_asm. U shfaq një hipotezë: nuk po vijnë ndërprerjet e orës (apo të gjitha ndërprerjet në përgjithësi). Dhe me të vërtetë, nëse nga ndarja natyrale, e cila për ndonjë arsye funksionoi, hiqen ndërprerjet, rezultatet duken të ngjashme. Por zgjidhja doli të ishte krejtësisht ndryshe: krahasimi i gjurmëve të makinerive, të dhëna sipas opsionit të mësipërm, tregoi se trajektoret e ekzekutimit shpërndaheshin shumë herët. Këtu duhet thënë, se krahasimi i daljes së regjistrit të debuggimit me daljen e ndarjes natyrale — nuk është një proces krejt mekanik. Nuk e di saktësisht se si programi i nisur në shfletues lidhet me , por disa linja në dalje rezultojnë se janë të ndryshuara, prandaj ndryshimi në diferencë — nuk është një arsye për të menduar se trajektoret janë shpërndarë. Në përgjithësi, bëri të qartë se sipas instrukcionit отладочного вывода с выводом нативной сборки — не совсем механический процесс. Не знаю точно, как запущенная в браузере программа соединяется с , por disa linja në dalje rezultojnë se janë të ndryshuara, prandaj ndryshimi në diferencë — nuk është një arsye për të menduar se trajektoret janë shpërndarë. Në përgjithësi, bëri të qartë se sipas instrukcionit, но некоторые строчки в выводе оказываются переставленные местами, поэтому различие в диффе — это ещё не повод считать, что траектории разошлись. В общем, стало понятно, что по инструкции ljmpl në proces po bëhet kalimi mes adresave të ndryshme, dhe byte kodi që gjenerohet është në parim ndryshe: në një rast ka një instruksion për thirrjen e një funksioni ndihmës C, ndërsa në tjetrin nuk e ka atë. Pas hulumtimit të instruksioneve dhe studimit të kodit që i përkthen këto instruksione, u bë e qartë se, së pari, direkt para tij në regjistrin cr0 po bëhej regjistrimi - gjithashtu me ndihmën e ndihmësit - që kalonte procesorin në modalitetin e mbrojtur, dhe së dyti, versioni js nuk kaloi asnjëherë në modalitetin e mbrojtur. Çështja është se një nga karakteristikat e tjera të Emscripten është përshtatshmëria e tij ndaj kodit që implementon instruksionin call në TCI, që çdo tregues në një funksion e çon në tipin long long f(int arg0, .. int arg9) - funksionet duhet të thirren me numrin e duhur të argumenteve. Nëse kjo rregull i shkelet, varësisht nga konfigurimet e debug-ut, programi ose do të bjerë (çka është e mirë), ose do të thërrasë një funksion të gabuar (çka do të jetë e trishtueshme për të debuguar). Ka edhe një mundësi të tretë - të aktivizohet gjenerimi i mbështjellësve, që shtojnë/hoqin argumente, por në total këta mbështjellës do të zënë shumë vend, ndërsa në të vërtetë m'u nevojiten vetëm pak mbi njëqind mbështjellës. Vetëm kjo është tepër trishtuese, por doli një problem më serioz: në kodin e gjeneruar të mbështjellësve të funksioneve, argumentet u konvertuan, por funksioni me argumentet e gjeneruara ndonjëherë nuk u thirr - si në implementimin tim të libffi. Domethënë, disa ndihmës thjesht nuk u ekzekutuan.

Fatkeqësisht, në Qemu ka lista të lexueshme nga makina të ndihmësve në formën e një skedari titull, si

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

Ato përdoren mjaft në mënyra interesante: së pari, makros janë ripërcaktuar në një mënyrë krejtësisht të çuditshme DEF_HELPER_n, dhe pastaj përfshihet helper.h. deri në atë pikë, sa makroja zgjerohet në inicializuesin e strukturës dhe presjen, e më pas përcaktohet një masiv, dhe në vend të elementeve - #include <helper.h> Si rezultat, në fund të fundit, u shfaq një rast për të provuar bibliotekën pyparsing, dhe u shkrua një skript që gjeneron mbështjellësit ata dhe vetëm për funksionet për të cilat është e nevojshme.

Dhe ja, pas kësaj procesori duket se filloi të punojë. Duket se, sepse ekrani nuk është inicializuar, megjithëse në ndërtimin natyror arrita të startoj memtest86+. Këtu duhet theksuar se kodi i hyrjes bllokuese të Qemu është shkruar me korutina. Në Emscripten ka një implementim mjaft të ndërlikuar, por duhej të mbështetej gjithashtu në kodin e Qemu, dhe procesori mund të debugohet tani: Qemu mbështet opsionet -kernel, -initrd, -append, me ndihmën e të cilave mund të ngarkohet Linux ose, për shembull, memtest86+, pa përdorur fare pajisje bllokuese. Por ja një problem: në ndërtimin natyror mund të shihja daljen e bërthamës Linux në konsolën me opsionin -nographic, ndërsa nga shfletuesi nuk po vinte asnjë dalje në terminalin nga ku ishte nisur , por disa linja në dalje rezultojnë se janë të ndryshuara, prandaj ndryshimi në diferencë — nuk është një arsye për të menduar se trajektoret janë shpërndarë. Në përgjithësi, bëri të qartë se sipas instrukcionit, nuk erdhi. Domethënë nuk është e qartë: procesori nuk punon apo dalja grafike. Pastaj më erdhi ideja të prisja pak. Doli se "procesori nuk fle, por thjesht ndriçon ngadalë", dhe pas pesë minutash bërthama shkarkoi një grup mesazhesh në konsolë dhe vazhdoi të ngecë. U bë e qartë se procesori, në përgjithësi, punon, dhe duhet të hulumtoj në kodin e punës me SDL2. Përdorimi i kësaj biblioteke, fatkeqësisht, nuk e di, prandaj në disa raste duhej të veproja me rastësishëm. Në një moment në ekran shkelqeu linja parallel0 mbi një sfond blu, që sugjeronte disa mendime. Në përfundim doli se ishte që Qemu hap disa dritare virtuale në një dritare fizike, midis të cilave mund të kalosh me Ctrl-Alt-n: në ndërtimin natyror funksionon, në Emscripten - jo. Pas heqjes së dritareve të panevojshme me ndihmën e opsioneve -monitor none -parallel none -serial none dhe caktimit për të riluajtur të gjithë ekranin në çdo kornizë, gjithçka papritmas filloi të funksionojë.

Korutina

Pra, emulimi në shfletues punon, por nuk mund të ndizet asgjë interesante me një disk, sepse nuk ka hyrje-bllokuese - nevojitet të realizohet mbështetja për korutina. Në Qemu ka disa backend të korutinave, por për shkak të karakteristikave të JavaScript dhe gjeneratorit të kodit Emscripten nuk mund të thua vetëm të fillosh të hedhësh steka. Dukej si, "gjithçka është e humbur, gjipsin e heqin", por zhvilluesit e Emscripten tashmë e kanë marrë parasysh gjithçka. Kjo është realizuar mjaft argëtuese: he, le të quajmë një thirrje të dyshimtë të funksionit si emscripten_sleep dhe disa të tjerë, që përdorin mekanizmin Asyncify, si dhe thirrje me tregues dhe thirrje të çdo funksioni, ku poshtë në stack mund të ndodhë një nga dy rastet e mëparshme. Tani, para çdo thirrjeje të dyshimtë, do të ndërrmarim kontekstin async, dhe menjëherë pas thirrjes do të kontrollojmë nëse ka ndodhur ndonjë thirrje asinkrone, dhe nëse ka ndodhur, do ta ruajmë të gjithë variablat lokalë në këtë kontekst async, do të përcaktojmë se në cilin funksion duhet të kalojmë kontrollin kur të nevojitet vazhdimi, dhe do të dalim nga funksioni aktual. Këtu ka hapësirë për studimin e efektit. shkëputjes — për nevojat e vazhdimit të ekzekutimit të kodit pas kthimit nga një thirrje asinkrone, kompajleri gjeneron "mbetje" të funksionit, që fillojnë pas thirrjes dyshuese — kështu: nëse ka n thirrje dyshuese, funksioni do të jetë i shpërndarë ndoshta n/2 herë — kjo është akoma, nëse nuk merret parasysh se në funksionin origjinal duhet pas çdo thirrjeje potencialisht asinkrone të shtohet ruajtja e një pjese të variablave lokalë. Më vonë, madje më duhej të shkruaja një skenar të thjeshtë në Python, që për një grup të caktuar të funksioneve të veçanta, të cilat, supozohet, "nuk lejojnë asinkronitetin të kalojë nëpër ta" (domethënë, në to nuk ndodh rrotullimi i stack-ut dhe të gjitha ato që sapo përmenda), tregon se thirrjet përmes treguesve në cilat funksione duhet t'i injorojë kompajleri, në mënyrë që këto funksione të mos konsiderohen si asinkrone. Sepse skedarët JS prej 60 Mb — kjo është tashmë një tejkalim — le të paktën 30. Megjithatë, njëherë, ndihmova të konfiguronte një skript ndërtimi, dhe rastësisht hoqa opsionet e linker-it, ndër të cilat ishte... -O3. Aktivizoj kodin e gjeneruar, dhe Chromium konsumon kujtesën dhe bie. Pastaj rastësisht shikova atë që përpiqej të ngarkohej... Tani, çfarë mund të them, do të isha edhe unë i bllokuar nëse do të kërkohej të studioja me kujdes dhe të optimizoja JavaScript-in mbi 500+ Mb.

Fatkeqësisht, verifikimet në kodin e bibliotekës së mbështetjes Asyncify nuk ishin shumë të harmonizuara me longjmp-ët, që përdoren në kodin e procesorit virtual, por pas një patch-i të vogël që çaktivizonte këto verifikime dhe që rikthente kontekstet ashtu siç dukej se gjithçka ishte në rregull, kodi filloi të punonte. Dhe këtu filloi e çuditshme: ndonjëherë verifikimet në kodin e sinkronizimit ndodhnin — ato që ndalonin emergjente kodin, nëse sipas logjikës së ekzekutimit duhej të bllokohej — dikush përpiqej të merrte një mutex që tashmë ishte kapur. Fatmirësisht, kjo nuk ishte një problem logjik në kodin e serializuar — thjesht përdorja funksionalitetin e zakonshëm të ndërprerjeve të ciklit kryesor, të ofruar nga Emscripten, por ndonjëherë thirrja asinkrone shpërndante plotësisht stack-un, dhe në atë moment ndodhte setTimeout nga cikli kryesor — kështu, kodi hynte në një iteracion të ciklit kryesor, pa dalë nga iteracioni i mëparshëm. E rihapëm në një cikël të pafund dhe emscripten_sleep, dhe problemet me mutex-at mbetën. Kodi u bë edhe më logjik — sepse, në thelb, nuk kam një kod që përgatit kadrin e ardhshëm të animacionit — thjesht procesori llogarit diçka dhe ekrani përditësohet periodikisht. Megjithatë, problemet nuk u ndaluan këtu: ndonjëherë ekzekutimi i Qemu thjesht përfundonte heshtazi pa asnjë përjashtim apo gabim. Në atë moment e lashë mënjanë, por, duke kaluar përpara, do të them se problemi qëndronte në këtë: kodi i korutinave, në të vërtetë, nuk e përdor aspak setTimeout (ose, për të qenë të sakta, jo aq shpesh sa mund të mendohet): funksioni emscripten_yield thjesht vendos një flamur thirrjeje asinkrone. E gjithë thelbi është se emscripten_coroutine_next nuk është një funksion asinkron: brenda vetes kontrollon flamurin, e reseton dhe kalon kontrollin atje ku është e nevojshme. Kjo do të thotë se rrotullimi i stack-ut përfundon aty. Problemi ishte se për shkak të use-after-free, i cili shfaqej duke çaktivizuar pulin e korutinave për shkak se nuk e kisha copëzuar një rresht të rëndësishëm të kodit nga backend-i ekzistues i korutinave, funksioni qemu_in_coroutine ktheu true, kur në të vërtetë duhej të kthehej false. Kjo çoi në thirrjen emscripten_yield, mbi të cilin në stack nuk kishte emscripten_coroutine_next, stack-u u zhvillua deri në majë, por asnjë setTimeout, siç thashë, nuk po vendoseshin.

Kodigenerimi JavaScript

Ja, në fakt, "rrotullimi i mishit mbrapa" që u premtuar. Në të vërtetë, jo. Sigurisht, nëse e aktivizoni Qemu në shfletues, dhe në të Node.js, atëherë natyrisht pas kodigenerimit në Qemu do të kemi një JavaScript krejtësisht ndryshe. Por ndonjëherë, ka një transformim të caktuar pasiver.

Së pari, le të flasim pak rreth mënyrës se si funksionon Qemu. Ju lutem, më falni: nuk jam një zhvillues profesionist i Qemu dhe përfundimet e mia mund të jenë disa herë të gabuara. Siç thotë një shprehje, "opinionet e studentëve nuk duhet të përputhen me ato të profesorëve, aksiomat e Peano dhe shëndeti i arsyes". Qemu ka një numër të caktuar të arkitekturave të mbështetura të mysafirëve dhe për secilën ka një katalog si target-i386. Gjatë ndërtimit, mund të tregoni mbështetje për disa arkitektura mysafirësh, por si rezultat do të merrni thjesht disa binarë. Kodi për të mbështetur arkitekturën mysafire, nga ana tjetër, gjeneron disa operacione të brendshme të Qemu, të cilat TCG (Tiny Code Generator) i shndërron në kodin e makinës së arkitekturës së hostit. Siç thuhet në dokumentin readme, i cili ndodhet në katalogun tcg, fillimisht ka qenë një pjesë e zakonshme e kompilatorit C, të cilin më pas e kanë përshtatur për JIT. Prandaj, për shembull, arkitektura e synuar në terma të këtij dokumenti – tashmë nuk është arkitektura mysafire, por ajo e hostit. Në një moment, kishte një komponent tjetër – Tiny Code Interpreter (TCI), i cili duhet të ekzekutojë kodin (pothuajse të njëjtat operacione të brendshme) në mungesë të gjeneruesit të kodit për një arkitekturë të caktuar hosti. Në të vërtetë, siç thuhet në dokumentacionin e tij, ky interpretues mund të mos funksionojë gjithmonë po aq mirë sa gjeneruesi i kodit JIT, jo vetëm sa i përket shpejtësisë, por edhe cilësisë. Megjithatë, nuk jam i sigurt se përshkrimi i tij është plotësisht i saktë.

Së pari, u përpoqa të krijoj një backend të plotë TCG, por u ngatërrova shpejt në burimet dhe përshkrimin e paqartë të instrukcioneve të kodit të byte, prandaj vendosa të mbaj interpretuesin TCI. Kjo ofroi disa përfitime:

  • në realizimin e gjeneruesit të kodit, mund të shikoja jo në përshkrimin e instrukcioneve, por në kodin e interpretuesit
  • mund të gjeneroja funksione jo për çdo bllok të takuar të përkthimit, por për shembull, vetëm pas së njëqindit të ekzekutimeve
  • në rast të ndryshimit të kodit të gjeneruar (dhe duket se është e mundur, gjykimit nga funksionet me emra që përmbajnë fjalën patch), duhet të invalidizoj kodin JS të gjeneruar, por të paktën do të kem diçka nga të cilat mund ta përshtat atë përsëri

Për sa i përket pikës së tretë, nuk jam i sigurt se patchimi është i mundur pas herës së parë të ekzekutimit të kodit, por edhe dy të parat janë të mjaftueshme.

Fillimisht, kodi gjenerohej në formën e një switch të madh sipas adresës së instrukcionit të kodit të byte, por më pas, duke përkujtuar një artikull mbi Emscripten, optimizimin e JS të gjeneruar dhe relooping, vendosa të gjeneroj kod më shumë njerëzor, sidomos kur u vërtetua empirike që pika e vetme e hyrjes në bllokun e përkthimit është fillimi i tij. E thënë — e bërë, pas pak kohësh, arrita një gjenerues kodi që gjeneron kod me if-e (edhe pse pa cikle). Por, kështu ndodhi, ai dështoi, duke dhënë një mesazh se instruksioni kishte një gjatësi të gabuar. Në të njëjtën kohë, instrukcioni i fundit në këtë nivel rekursiv ishte brcond. Mirë, do të shtoj një kontroll identik në gjenerimin e këtij instruksioni para thirrjes rekursive dhe pas saj dhe… asnjëra prej tyre nuk u ekzekutua, por pas switch-it për assert, përsëri ranë. Në fund, pasi studiuam kodin e gjeneruar, kuptova se pas switch-it, treguesi në instruksionin aktual rinovohej nga staku dhe ndoshta ishte duke u mbivendosur nga kodi JavaScript i gjeneruar. Ashtu doli. Rritja e buffers nga një megabajt në dhjetë nuk çoi në asgjë dhe bëri të qartë që gjeneruesi i kodit po bënte një cikël. Duhej të kontrolloja nëse nuk ishim tejkaluar kufijtë e TB aktual, dhe nëse e kishim bërë, të jepja adresën e TB-të tjetër me një shenjë negative, në mënyrë që të mund të vazhdoja ekzekutimin. Për më tepër, kjo zgjidh problemin "cila funksion e gjeneruar duhet të invalidizohet nëse ky pjesë kodit të byte ka ndryshuar?" - invalidizuese është vetëm ajo funksion që i përket këtij blloku të përkthimit. Meqenëse unë e debug-ova gjithçka në Chromium (pasi përdor Firefox dhe më është më e lehtë të përdor një shfletues të veçantë për eksperimentet), por Firefox më ndihmoi të rregulloj disa moskompatibilitete me standardin asm.js, pas së cilës kodi filloi të funksionojë më shpejt në Chromium.

Shembulli i kodit të gjeneruar

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"]

Përfundimi

Pra faqat, puna nuk është përfunduar ende, por më ka lodhur të përmirësoj këtë projekt të gjatë në fshehtësi. Prandaj vendosa të publikoj atë që kam deri tani. Kodi në disa vende është i frikshëm, pasi është një eksperiment dhe nuk është e qartë paraprakisht se çfarë duhet bërë. Ndoshta, më vonë do të jetë e mençur të krijoj komitete të zakonshme mbi një version më modern të Qemu. Për momentin, ekziston një degë në git në formatin e blogut: për çdo "nivel" të kaluar, është shtuar një koment i detajuar në gjuhën ruse. Në thelb, ky artikull është në një masë të madhe — përshtatja e daljes. git log.

Mund ta provoni këtë të gjithë këtu (kujdes, trafik).

Çfarë funksionon tashmë:

  • Funksionon procesori virtual x86
  • Ekziston një prototip i funksionimit të gjeneruesit të kodit JIT nga kodi makinerik në JavaScript
  • Ekziston një bazë për ndërtimin e arkitekturave të tjera të vizitorëve 32-bit: ju mund ta shihni tani Linux-in gjatë ngarkimit për arkitekturën MIPS që është duke ngelur në shfletues.

Çfarë tjetër mund të bëni

  • Të përshpejtoni emulimin. Edhe në modalitetin JIT, duket se punon më ngadalë se Virtual x86 (përveç kësaj, ka një Qemu me një sasi të madhe harduerësh dhe arkitekturash që emulon).
  • Të krijoni një ndërfaqe normale - unë nuk jam një zhvillues web-i shumë i mirë, ndaj deri tani kam rindërtuar pamjen standarde të Emscripten, sa më kam mundur.
  • Të provoni të lançoni funksione më të komplikuara të Qemu - rrjeti, migrazione VM, etj.
  • UPD: duhet të dërgoj në upstream Emscripten zhvillimet e mia të pakta dhe raportet e problemeve, ashtu siç bënë portuesit e mëparshëm të Qemu dhe projekteve të tjera. Falë tyre për mundësinë e përdorimit implicit të kontributit të tyre në Emscripten në kuadër të detyrës time.

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