Qemu.js me mbështetjen JIT: mund ta kthesh mbrapsht mishin e grirë

Disa vite më parë Fabrice Bellard shkruar nga jslinux është një emulator PC i shkruar në JavaScript. Pas kësaj kishte të paktën më shumë Virtual x86. Por të gjithë, me sa di unë, ishin përkthyes, ndërsa Qemu, i shkruar shumë më herët nga i njëjti Fabrice Bellard, dhe, ndoshta, çdo emulator modern që respekton veten, përdor përpilimin JIT të kodit të mysafirëve në kodin e sistemit pritës. Më dukej se ishte koha për të zbatuar detyrën e kundërt në lidhje me atë që shfletuesit zgjidhin: përpilimin JIT të kodit të makinës në JavaScript, për të cilën dukej më logjike të portosh Qemu. Do të duket, pse Qemu, ka emulatorë më të thjeshtë dhe miqësorë - i njëjti VirtualBox, për shembull - të instaluar dhe funksionon. Por Qemu ka disa veçori interesante

  • burim i hapur
  • aftësia për të punuar pa një drejtues kernel
  • aftësia për të punuar në modalitetin e përkthyesve
  • mbështetje për një numër të madh të arkitekturave host dhe mysafir

Në lidhje me pikën e tretë, tani mund të shpjegoj se në fakt, në modalitetin TCI, nuk interpretohen vetë udhëzimet e makinës së ftuar, por bajtkodi i marrë prej tyre, por kjo nuk e ndryshon thelbin - për të ndërtuar dhe ekzekutuar Qemu në një arkitekturë të re, nëse jeni me fat, mjafton një përpilues C - shkrimi i një gjeneratori kodi mund të shtyhet.

Dhe tani, pas dy vitesh ngatërrese të heshtur me kodin burimor Qemu në kohën time të lirë, u shfaq një prototip pune, në të cilin tashmë mund të ekzekutoni, për shembull, Kolibri OS.

Çfarë është Emscripten

Në ditët e sotme, janë shfaqur shumë përpilues, rezultati përfundimtar i të cilëve është JavaScript. Disa, si Type Script, fillimisht ishin menduar të ishin mënyra më e mirë për të shkruar për ueb. Në të njëjtën kohë, Emscripten është një mënyrë për të marrë kodin ekzistues C ose C++ dhe për ta përpiluar atë në një formë të lexueshme nga shfletuesi. Aktiv kjo faqe Ne kemi mbledhur shumë porte të programeve të njohura: këtuPër shembull, mund të shikoni PyPy - meqë ra fjala, ata pretendojnë se tashmë kanë JIT. Në fakt, jo çdo program mund të kompilohet dhe ekzekutohet thjesht në një shfletues - ka një numër karakteristikat, të cilën duhet ta duroni, megjithatë, pasi mbishkrimi në të njëjtën faqe thotë “Emscripten mund të përdoret për të përpiluar pothuajse çdo portativ Kodi C/C++ në JavaScript". Kjo do të thotë, ka një sërë operacionesh që janë sjellje të papërcaktuara sipas standardit, por zakonisht funksionojnë në x86 - për shembull, akses i pabarabartë në variabla, i cili përgjithësisht është i ndaluar në disa arkitektura. Në përgjithësi. , Qemu është një program ndër-platformë dhe, doja të besoja, dhe nuk përmban tashmë shumë sjellje të papërcaktuara - merre dhe përpiloje, pastaj ndërro pak me JIT - dhe ke mbaruar! Por kjo nuk është rast...

Së pari provoni

Në përgjithësi, unë nuk jam personi i parë që lindi me idenë e transferimit të Qemu në JavaScript. U bë një pyetje në forumin ReactOS nëse kjo ishte e mundur duke përdorur Emscripten. Edhe më herët, kishte zëra se Fabrice Bellard e bëri këtë personalisht, por ne po flisnim për jslinux, i cili, me sa di unë, është vetëm një përpjekje për të arritur manualisht performancë të mjaftueshme në JS dhe është shkruar nga e para. Më vonë, u shkrua Virtual x86 - për të u postuan burime të paqarta, dhe, siç u tha, "realizmi" më i madh i emulimit bëri të mundur përdorimin e SeaBIOS si firmware. Për më tepër, ka pasur të paktën një përpjekje për të portuar Qemu duke përdorur Emscripten - u përpoqa ta bëja këtë palë prizë, por zhvillimi, me sa kuptoj, ishte i ngrirë.

Pra, do të duket, këtu janë burimet, këtu është Emscripten - merre dhe përpiloje. Por ka edhe biblioteka nga të cilat varet Qemu, dhe biblioteka nga të cilat varen ato biblioteka etj., dhe njëra prej tyre është libffi, nga e cila varet glib. Kishte thashetheme në internet se ekzistonte një në koleksionin e madh të porteve të bibliotekave për Emscripten, por ishte disi e vështirë të besohej: së pari, nuk kishte për qëllim të ishte një përpilues i ri, së dyti, ishte një nivel shumë i ulët. bibliotekë për të marrë dhe përpiluar në JS. Dhe nuk është vetëm një çështje e futjeve të montimit - me siguri, nëse e ktheni atë, për disa konventa thirrjesh mund të gjeneroni argumentet e nevojshme në pirg dhe të thërrisni funksionin pa to. Por Emscripten është një gjë e ndërlikuar: për ta bërë kodin e krijuar të duket i njohur për optimizuesin e motorit të shfletuesit JS, përdoren disa truke. Në veçanti, i ashtuquajturi relooping - një gjenerator kodi që përdor LLVM IR të marrë me disa udhëzime abstrakte të tranzicionit përpiqet të rikrijojë nëse, sythe të besueshme, etj. Epo, si kalohen argumentet në funksion? Natyrisht, si argumente për funksionet JS, që është, nëse është e mundur, jo përmes stek.

Në fillim kishte një ide për të shkruar thjesht një zëvendësim për libffi me JS dhe për të ekzekutuar teste standarde, por në fund u hutova se si t'i bëj skedarët e mi të kokës në mënyrë që ata të funksionojnë me kodin ekzistues - çfarë mund të bëj? siç thonë ata, "A janë detyrat kaq komplekse "A jemi kaq budallenj?" Më duhej ta transferoja libffi në një arkitekturë tjetër, si të thuash - për fat të mirë, Emscripten ka të dyja makrot për montim inline (në Javascript, po - mirë, cilado qoftë arkitektura, pra asembleri), dhe aftësinë për të ekzekutuar kodin e krijuar menjëherë. Në përgjithësi, pasi punova me fragmente libffi të varura nga platforma për ca kohë, mora një kod të përpilueshëm dhe e ekzekutova në provën e parë që hasa. Për habinë time, testi ishte i suksesshëm. I shtangur nga gjenialiteti im - pa shaka, funksionoi që në fillimin e parë - unë, ende duke mos u besuar syve të mi, shkova të shikoja përsëri kodin që rezulton, për të vlerësuar se ku të gërmoj më pas. Këtu u çmenda për herë të dytë - e vetmja gjë që bëra funksioni im ishte ffi_call - kjo raportoi një telefonatë të suksesshme. Nuk kishte asnjë thirrje vetë. Kështu që dërgova kërkesën time të parë për tërheqje, e cila korrigjoi një gabim në test që është i qartë për çdo student olimpiadë - numrat realë nuk duhet të krahasohen si a == b madje edhe si a - b < EPS - ju gjithashtu duhet të mbani mend modulin, përndryshe 0 do të rezultojë të jetë shumë e barabartë me 1/3... Në përgjithësi, kam ardhur me një port të caktuar të libffi, i cili kalon testet më të thjeshta dhe me të cilin glib është përpiluar - Vendosa se do të ishte e nevojshme, do ta shtoj më vonë. Duke parë përpara, do të them që, siç doli, përpiluesi nuk e përfshiu as funksionin libffi në kodin përfundimtar.

Por, siç thashë tashmë, ka disa kufizime, dhe midis përdorimit të lirë të sjelljeve të ndryshme të papërcaktuara, është fshehur një veçori më e pakëndshme - JavaScript sipas dizajnit nuk mbështet multithreading me memorie të përbashkët. Në parim, kjo zakonisht mund të quhet edhe një ide e mirë, por jo për transferimin e kodit, arkitektura e të cilit është e lidhur me fijet C. Në përgjithësi, Firefox-i po eksperimenton me mbështetjen e punëtorëve të përbashkët, dhe Emscripten ka një zbatim të fillesave për ta, por unë nuk doja të varesha prej tij. Më duhej të çrrënjosja ngadalë multithreading nga kodi Qemu - domethënë, të zbuloja se ku po funksionojnë fijet, të zhvendosja trupin e lakut që funksionon në këtë fije në një funksion të veçantë dhe t'i thërras funksionet e tilla një nga një nga cikli kryesor.

Përpjekja e dytë

Në një moment, u bë e qartë se problemi ishte ende atje dhe se shtyrja e rastësishme e patericave rreth kodit nuk do të çonte në ndonjë të mirë. Përfundim: duhet të sistemojmë disi procesin e shtimit të patericave. Prandaj, u mor versioni 2.4.1, i cili ishte i freskët në atë kohë (jo 2.5.0, sepse, nuk e dini kurrë, do të ketë gabime në versionin e ri që nuk janë kapur ende, dhe unë kam mjaft të miat bugs), dhe gjëja e parë që bëra ishte ta rishkruaja atë në mënyrë të sigurt thread-posix.c. Epo, domethënë, po aq e sigurt: nëse dikush u përpoq të kryente një operacion që çon në bllokim, funksioni thirrej menjëherë abort() - sigurisht, kjo nuk i zgjidhi të gjitha problemet menjëherë, por të paktën ishte disi më e këndshme sesa të merrje në heshtje të dhëna jokonsistente.

Në përgjithësi, opsionet Emscripten janë shumë të dobishme në transferimin e kodit në JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - ata kapin disa lloje sjelljesh të papërcaktuara, të tilla si thirrjet në një adresë të pabarabartë (që nuk është aspak në përputhje me kodin për grupet e shtypura si p.sh. HEAP32[addr >> 2] = 1) ose thirrja e një funksioni me numrin e gabuar të argumenteve.

Nga rruga, gabimet e shtrirjes janë një çështje më vete. Siç e thashë tashmë, Qemu ka një sfond interpretues "të degjeneruar" për gjenerimin e kodit TCI (interpretues të vogël kodi), dhe për të ndërtuar dhe ekzekutuar Qemu në një arkitekturë të re, nëse jeni me fat, mjafton një përpilues C. Fjalët kyçe "nese je me fat". Unë isha i pafat dhe doli që TCI përdor akses të palidhur kur analizon bajtkodin e tij. Kjo do të thotë, në të gjitha llojet e ARM dhe arkitekturave të tjera me akses domosdoshmërisht të niveluar, Qemu përpilon sepse ato kanë një backend normal TCG që gjeneron kodin vendas, por nëse TCI do të funksionojë në to është një pyetje tjetër. Sidoqoftë, siç doli, dokumentacioni i TCI tregonte qartë diçka të ngjashme. Si rezultat, kodit iu shtuan thirrjet e funksioneve për lexim të padrejtuar, të cilat u gjetën në një pjesë tjetër të Qemu.

Shkatërrimi i grumbullit

Si rezultat, qasja e palidhur në TCI u korrigjua, u krijua një lak kryesor që nga ana e tij quhej procesor, RCU dhe disa gjëra të tjera të vogla. Dhe kështu nis Qemu me opsionin -d exec,in_asm,out_asm, që do të thotë se ju duhet të thoni se cilat blloqe kodi janë duke u ekzekutuar, dhe gjithashtu në momentin e transmetimit të shkruani se cili ishte kodi i mysafirit, çfarë u bë kodi i hostit (në këtë rast, bytecode). Fillon, ekzekuton disa blloqe përkthimi, shkruan mesazhin e korrigjimit që lashë që RCU tani do të fillojë dhe ... rrëzohet abort() brenda një funksioni free(). Duke ndërhyrë me funksionin free() Ne arritëm të zbulojmë se në kokën e bllokut të grumbullit, i cili shtrihet në tetë bajt që paraprijnë memorien e caktuar, në vend të madhësisë së bllokut ose diçka të ngjashme, kishte mbeturina.

Shkatërrimi i grumbullit - sa e lezetshme... Në një rast të tillë, ekziston një ilaç i dobishëm - nga (nëse është e mundur) të njëjtat burime, mblidhni një binar vendas dhe drejtojeni nën Valgrind. Pas ca kohësh, binar ishte gati. Unë e nis atë me të njëjtat opsione - ai rrëzohet edhe gjatë inicializimit, përpara se të arrijë në të vërtetë ekzekutimin. Është e pakëndshme, natyrisht - me sa duket, burimet nuk ishin saktësisht të njëjta, gjë që nuk është për t'u habitur, sepse konfigurimi ka zbuluar opsione paksa të ndryshme, por unë kam Valgrind - së pari do ta rregulloj këtë gabim, dhe më pas, nëse jam me fat , do të shfaqet origjinali. Unë jam duke ekzekutuar të njëjtën gjë nën Valgrind... Y-y-y, y-y-y, uh-uh, filloi, kaloi në inicializimin normalisht dhe kaloi përpara defektit origjinal pa asnjë paralajmërim të vetëm për aksesin e pasaktë të memories, për të mos përmendur për rëniet. Jeta, siç thonë ata, nuk më përgatiti për këtë - një program përplasjeje ndalon të rrëzohet kur niset nën Walgrind. Ajo që ishte është një mister. Hipoteza ime është se një herë në afërsi të udhëzimit aktual pas një përplasjeje gjatë inicializimit, gdb tregoi punë memset-a me një tregues të vlefshëm duke përdorur ose mmx, ose xmm regjistron, atëherë ndoshta ishte një lloj gabimi i shtrirjes, megjithëse është ende e vështirë të besohet.

Mirë, Valgrind nuk duket se ndihmon këtu. Dhe këtu filloi gjëja më e neveritshme - gjithçka duket se fillon, por rrëzohet për arsye absolutisht të panjohura për shkak të një ngjarjeje që mund të kishte ndodhur miliona udhëzime më parë. Për një kohë të gjatë, as që ishte e qartë se si të afrohej. Në fund, më duhej të ulem dhe të korrigjoj gabimet. Printimi i asaj me të cilën u rishkrua titulli tregoi se nuk dukej si një numër, por më tepër si një lloj të dhënash binare. Dhe, ja, ky varg binar u gjet në skedarin BIOS - domethënë, tani ishte e mundur të thuhej me besim të arsyeshëm se ishte një tejmbushje buferi, madje është e qartë se ishte shkruar në këtë buffer. Epo, atëherë diçka e tillë - në Emscripten, për fat të mirë, nuk ka asnjë rastësi të hapësirës së adresave, nuk ka as vrima në të, kështu që mund të shkruani diku në mes të kodit për të nxjerrë të dhëna me tregues nga nisja e fundit, shikoni të dhënat, shikoni treguesin dhe, nëse nuk ka ndryshuar, merrni ushqim për të menduar. Vërtetë, duhen disa minuta për t'u lidhur pas çdo ndryshimi, por çfarë mund të bëni? Si rezultat, u gjet një linjë specifike që kopjoi BIOS-in nga buferi i përkohshëm në kujtesën e mysafirëve - dhe, në të vërtetë, nuk kishte hapësirë ​​të mjaftueshme në tampon. Gjetja e burimit të asaj adrese të çuditshme buferi rezultoi në një funksion qemu_anon_ram_alloc në dosje oslib-posix.c - logjika atje ishte kjo: ndonjëherë mund të jetë e dobishme të lidhni adresën në një faqe të madhe me madhësi 2 MB, për këtë do të pyesim mmap fillimisht pak më shumë, dhe më pas do ta kthejmë tepricën me ndihmë munmap. Dhe nëse një shtrirje e tillë nuk kërkohet, atëherë ne do të tregojmë rezultatin në vend të 2 MB getpagesize() - mmap do të japë ende një adresë të përafruar... Pra në Emscripten mmap thjesht thirrje malloc, por sigurisht që nuk përputhet në faqe. Në përgjithësi, një defekt që më zhgënjeu për disa muaj u korrigjua nga një ndryshim në двух linjat.

Karakteristikat e funksioneve të thirrjes

Dhe tani procesori po numëron diçka, Qemu nuk rrëzohet, por ekrani nuk ndizet, dhe procesori shpejt shkon në sythe, duke gjykuar nga dalja -d exec,in_asm,out_asm. Ka dalë një hipotezë: ndërprerjet e kohëmatësit (ose, në përgjithësi, të gjitha ndërprerjet) nuk arrijnë. Dhe me të vërtetë, nëse hiqni ndërprerjet nga asambleja vendase, e cila për ndonjë arsye funksionoi, ju merrni një pamje të ngjashme. Por kjo nuk ishte aspak përgjigjja: një krahasim i gjurmëve të lëshuara me opsionin e mësipërm tregoi se trajektoret e ekzekutimit ndryshuan shumë herët. Këtu duhet thënë se krahasimi i asaj që u regjistrua duke përdorur lëshuesin emrun korrigjimi i prodhimit me daljen e asamblesë vendase nuk është një proces plotësisht mekanik. Nuk e di saktësisht se si lidhet një program që funksionon në një shfletues emrun, por disa linja në dalje rezultojnë të jenë të riorganizuara, kështu që ndryshimi në ndryshim nuk është ende një arsye për të supozuar se trajektoret kanë ndryshuar. Në përgjithësi, u bë e qartë se sipas udhëzimeve ljmpl ka një kalim në adresa të ndryshme, dhe bytekodi i gjeneruar është thelbësisht i ndryshëm: njëri përmban një udhëzim për të thirrur një funksion ndihmës, tjetri jo. Pas kërkimit të udhëzimeve në google dhe studimit të kodit që përkthen këto udhëzime, u bë e qartë se, së pari, menjëherë përpara tij në regjistër cr0 u bë një regjistrim - gjithashtu duke përdorur një ndihmës - i cili kaloi procesorin në modalitetin e mbrojtur dhe së dyti, se versioni js nuk kaloi kurrë në modalitetin e mbrojtur. Por fakti është se një veçori tjetër e Emscripten është ngurrimi i tij për të toleruar kode të tilla si zbatimi i udhëzimeve call në TCI, të cilin çdo tregues funksioni rezulton në lloj long long f(int arg0, .. int arg9) - funksionet duhet të thirren me numrin e saktë të argumenteve. Nëse ky rregull shkelet, në varësi të cilësimeve të korrigjimit, programi ose do të rrëzohet (që është mirë) ose do të thërrasë funksionin e gabuar fare (që do të jetë e trishtueshme për të korrigjuar). Ekziston edhe një opsion i tretë - aktivizoni gjenerimin e mbështjellësve që shtojnë / heqin argumente, por në total këta mbështjellës zënë shumë hapësirë, pavarësisht se në fakt më duhen vetëm pak më shumë se njëqind mbështjellës. Vetëm kjo është shumë e trishtueshme, por doli të ishte një problem më serioz: në kodin e krijuar të funksioneve të mbështjellësit, argumentet u konvertuan dhe u konvertuan, por ndonjëherë funksioni me argumentet e krijuara nuk thirrej - mirë, ashtu si në zbatimi im libffi. Kjo do të thotë, disa ndihmës thjesht nuk u ekzekutuan.

Për fat të mirë, Qemu ka lista të ndihmësve të lexueshme nga makina në formën e një skedari kokë si

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

Ato përdoren mjaft qesharake: së pari, makro-të ripërcaktohen në mënyrën më të çuditshme DEF_HELPER_n, dhe më pas ndizet helper.h. Në masën që makro zgjerohet në një inicializues strukture dhe një presje, dhe më pas përcaktohet një grup, dhe në vend të elementeve - #include <helper.h> Si rezultat, më në fund pata një shans për të provuar bibliotekën në punë pyparsing, dhe u shkrua një skenar që gjeneron pikërisht ato mbështjellës për funksionet për të cilat nevojiten.

Dhe kështu, pas kësaj procesori dukej se funksiononte. Duket se është sepse ekrani nuk u inicializua kurrë, megjithëse memtest86+ ishte në gjendje të ekzekutohej në asamblenë origjinale. Këtu është e nevojshme të sqarohet se kodi I/O i bllokut Qemu është i shkruar në korutina. Emscripten ka zbatimin e tij shumë të ndërlikuar, por ai ende duhej të mbështetej në kodin Qemu, dhe mund të korrigjoni procesorin tani: Qemu mbështet opsionet -kernel, -initrd, -append, me të cilin mund të nisni Linux ose, për shembull, memtest86+, pa përdorur fare pajisje bllok. Por këtu është problemi: në asamblenë vendase mund të shihet prodhimi i kernelit Linux në tastierë me opsionin -nographic, dhe asnjë dalje nga shfletuesi në terminal nga ku është nisur emrun, nuk erdhi. Kjo do të thotë, nuk është e qartë: procesori nuk funksionon ose dalja grafike nuk funksionon. Dhe pastaj më erdhi në mendje të prisja pak. Doli që "procesori nuk po fle, por thjesht pulson ngadalë" dhe pas rreth pesë minutash kerneli hodhi një tufë mesazhesh në tastierë dhe vazhdoi të varej. U bë e qartë se procesori, në përgjithësi, funksionon, dhe ne duhet të gërmojmë në kodin për të punuar me SDL2. Fatkeqësisht, nuk di si ta përdor këtë bibliotekë, kështu që në disa vende më duhej të veproja rastësisht. Në një moment, linja paralel0 u ndez në ekran në një sfond blu, gjë që sugjeroi disa mendime. Në fund, rezultoi se problemi ishte se Qemu hap disa dritare virtuale në një dritare fizike, midis të cilave mund të kaloni duke përdorur Ctrl-Alt-n: funksionon në ndërtimin e origjinës, por jo në Emscripten. Pas heqjes së dritareve të panevojshme duke përdorur opsionet -monitor none -parallel none -serial none dhe udhëzime për të rivizatuar me forcë të gjithë ekranin në çdo kornizë, gjithçka funksionoi papritmas.

Korutina

Pra, emulimi në shfletues funksionon, por nuk mund të ekzekutoni asgjë interesante me një disketë në të, sepse nuk ka bllok I/O - duhet të zbatoni mbështetje për korutinat. Qemu tashmë ka disa mbështetëse korutine, por për shkak të natyrës së JavaScript dhe gjeneratorit të kodit Emscripten, nuk mund të filloni thjesht të manipuloni me rafte. Duket se "gjithçka është zhdukur, suva po hiqet", por zhvilluesit e Emscripten tashmë janë kujdesur për gjithçka. Kjo është zbatuar mjaft qesharake: le ta quajmë një thirrje funksioni si kjo të dyshimtë emscripten_sleep dhe disa të tjerë duke përdorur mekanizmin Asyncify, si dhe thirrjet e treguesve dhe thirrjet për çdo funksion ku një nga dy rastet e mëparshme mund të ndodhë më poshtë në rafte. Dhe tani, para çdo telefonate të dyshimtë, ne do të zgjedhim një kontekst asinkron, dhe menjëherë pas thirrjes, do të kontrollojmë nëse ka ndodhur një thirrje asinkrone dhe nëse ka ndodhur, ne do t'i ruajmë të gjitha variablat lokale në këtë kontekst asinkron, duke treguar se cili funksion për të transferuar kontrollin kur duhet të vazhdojmë ekzekutimin dhe të dalim nga funksioni aktual. Këtu ka hapësirë ​​për të studiuar efektin duke shkapërderdhur - për nevojat e vazhdimit të ekzekutimit të kodit pas kthimit nga një thirrje asinkrone, përpiluesi gjeneron "cung" të funksionit duke filluar pas një thirrjeje të dyshimtë - si kjo: nëse ka n thirrje të dyshimta, atëherë funksioni do të zgjerohet diku n/2 herë — kjo është ende, nëse jo Mbani parasysh se pas çdo telefonate potencialisht asinkrone, duhet të shtoni ruajtjen e disa variablave lokale në funksionin origjinal. Më pas, madje më duhej të shkruaja një skenar të thjeshtë në Python, i cili, bazuar në një grup të caktuar funksionesh veçanërisht të mbipërdorura që supozohet se "nuk lejojnë asinkroninë të kalojë vetë" (d.m.th., promovimi i stivës dhe gjithçka që sapo përshkrova nuk punoni në to), tregon thirrjet përmes pointerëve në të cilat funksionet duhet të injorohen nga kompajleri në mënyrë që këto funksione të mos konsiderohen asinkrone. Dhe atëherë skedarët JS nën 60 MB janë qartësisht shumë - le të themi të paktën 30. Edhe pse, një herë isha duke krijuar një skenar montimi dhe aksidentalisht hodha opsionet e lidhjes, ndër të cilat ishte -O3. Unë ekzekutoj kodin e krijuar dhe Chromium ha memorien dhe rrëzohet. Më pas pashë aksidentalisht atë që ai po përpiqej të shkarkonte... Epo, çfarë mund të them, do të kisha ngrirë edhe unë nëse do të më kërkonin të studioja me kujdes dhe të optimizoja një Javascript prej 500+ MB.

Fatkeqësisht, kontrollet në kodin e bibliotekës mbështetëse Asyncify nuk ishin plotësisht miqësore longjmp-të që përdoren në kodin e procesorit virtual, por pas një patch të vogël që çaktivizon këto kontrolle dhe rikthen me forcë kontekstet sikur gjithçka të ishte mirë, kodi funksionoi. Dhe më pas filloi një gjë e çuditshme: ndonjëherë aktivizoheshin kontrolle në kodin e sinkronizimit - të njëjtat që rrëzojnë kodin nëse, sipas logjikës së ekzekutimit, ai duhej të bllokohej - dikush u përpoq të kapte një mutex tashmë të kapur. Për fat të mirë, ky doli të mos ishte një problem logjik në kodin e serializuar - thjesht po përdorja funksionalitetin standard të ciklit kryesor të ofruar nga Emscripten, por ndonjëherë thirrja asinkrone do ta zhbënte plotësisht pirgun dhe në atë moment do të dështonte setTimeout nga cikli kryesor - kështu, kodi hyri në përsëritjen e ciklit kryesor pa u larguar nga përsëritja e mëparshme. Rishkrua në një lak të pafund dhe emscripten_sleep, dhe problemet me mutexes u ndalën. Kodi është bërë edhe më logjik - në fund të fundit, në fakt, nuk kam ndonjë kod që përgatit kornizën tjetër të animacionit - procesori thjesht llogarit diçka dhe ekrani përditësohet periodikisht. Megjithatë, problemet nuk ndaleshin me kaq: ndonjëherë ekzekutimi i Qemu thjesht përfundonte në heshtje pa asnjë përjashtim apo gabim. Në atë moment hoqa dorë nga ajo, por, duke parë përpara, do të them se problemi ishte ky: kodi korutin, në fakt, nuk përdor setTimeout (ose të paktën jo aq shpesh sa mund të mendoni): funksion emscripten_yield thjesht vendos flamurin e thirrjes asinkrone. E gjithë çështja është se emscripten_coroutine_next nuk është një funksion asinkron: nga brenda kontrollon flamurin, e rivendos atë dhe e transferon kontrollin aty ku nevojitet. Kjo do të thotë, promovimi i pirgut përfundon atje. Problemi ishte se për shkak të përdorimit pas pa pagesë, i cili u shfaq kur grupi i korutinës u çaktivizua për shkak të faktit se nuk kopjova një linjë të rëndësishme kodi nga prapavija ekzistuese korutine, funksioni qemu_in_coroutine u kthye e vërtetë kur në fakt duhet të ishte kthyer false. Kjo çoi në një telefonatë emscripten_yield, mbi të cilin nuk kishte njeri në pirg emscripten_coroutine_next, pirgu u shpalos deri në majë, por jo setTimeout, siç thashë tashmë, nuk u ekspozua.

Gjenerimi i kodit JavaScript

Dhe këtu, në fakt, është premtimi "kthimi i mishit të grirë". Jo ne te vertete. Sigurisht, nëse ekzekutojmë Qemu në shfletues dhe Node.js në të, atëherë, natyrisht, pas gjenerimit të kodit në Qemu do të kemi JavaScript krejtësisht të gabuar. Por megjithatë, një lloj transformimi i kundërt.

Së pari, pak për mënyrën se si funksionon Qemu. Ju lutem më falni menjëherë: Unë nuk jam një zhvillues profesionist i Qemu dhe përfundimet e mia mund të jenë të gabuara në disa vende. Siç thonë ata, "mendimi i studentit nuk duhet të përkojë me mendimin e mësuesit, aksiomatikën dhe sensin e shëndoshë të Peano". Qemu ka një numër të caktuar arkitekturash të ftuar të mbështetur dhe për secilën ka një direktori si target-i386. Kur ndërtoni, mund të specifikoni mbështetjen për disa arkitektura të ftuar, por rezultati do të jetë vetëm disa binare. Kodi për të mbështetur arkitekturën e mysafirëve, nga ana tjetër, gjeneron disa operacione të brendshme Qemu, të cilat TCG (Tiny Code Generator) i kthen tashmë në kodin e makinës për arkitekturën pritës. Siç thuhet në skedarin readme të vendosur në direktorinë tcg, ky fillimisht ishte pjesë e një përpiluesi të rregullt C, i cili më vonë u përshtat për JIT. Prandaj, për shembull, arkitektura e synuar për sa i përket këtij dokumenti nuk është më një arkitekturë e ftuar, por një arkitekturë pritës. Në një moment, u shfaq një komponent tjetër - Interpretuesi i Tiny Code (TCI), i cili duhet të ekzekutojë kodin (pothuajse të njëjtat operacione të brendshme) në mungesë të një gjeneruesi kodi për një arkitekturë specifike të hostit. Në fakt, siç thuhet në dokumentacionin e tij, ky përkthyes mund të mos funksionojë gjithmonë aq mirë sa një gjenerues i kodeve JIT, jo vetëm nga pikëpamja sasiore, por edhe nga pikëpamja cilësore. Edhe pse nuk jam i sigurt se përshkrimi i tij është plotësisht i rëndësishëm.

Në fillim u përpoqa të bëja një backend të plotë TCG, por shpejt u ngatërrova në kodin burimor dhe një përshkrim jo plotësisht të qartë të udhëzimeve të bytekodit, kështu që vendosa të mbështjell interpretuesin TCI. Kjo dha disa avantazhe:

  • kur zbatoni një gjenerator kodi, mund të shikoni jo përshkrimin e udhëzimeve, por kodin e interpretuesit
  • ju mund të gjeneroni funksione jo për çdo bllok përkthimi të hasur, por, për shembull, vetëm pas ekzekutimit të njëqindtë
  • nëse kodi i gjeneruar ndryshon (dhe kjo duket se është e mundur, duke gjykuar nga funksionet me emra që përmbajnë fjalën patch), do të më duhet të zhvlerësoj kodin e gjeneruar JS, por të paktën do të kem diçka për ta rigjeneruar atë nga

Sa i përket pikës së tretë, nuk jam i sigurt që arnimi është i mundur pasi kodi të ekzekutohet për herë të parë, por dy pikat e para janë të mjaftueshme.

Fillimisht, kodi u krijua në formën e një ndërprerësi të madh në adresën e udhëzimit origjinal të bytecode, por më pas, duke kujtuar artikullin për Emscripten, optimizimin e JS të gjeneruar dhe rilooping, vendosa të gjeneroj më shumë kod njerëzor, veçanërisht pasi ai në mënyrë empirike doli se e vetmja pikë hyrëse në bllokun e përkthimit është Fillimi i tij. Jo më shpejt se u bë, pas një kohe kishim një gjenerues kodesh që gjeneronte kode me if (edhe pse pa sythe). Por fati i keq, ai u rrëzua, duke dhënë një mesazh se udhëzimet ishin të një gjatësie të gabuar. Për më tepër, udhëzimi i fundit në këtë nivel rekursioni ishte brcond. Mirë, unë do të shtoj një kontroll identik në gjenerimin e këtij udhëzimi para dhe pas thirrjes rekursive dhe... asnjëri prej tyre nuk u ekzekutua, por pas ndërprerësit të pohimit ata përsëri dështuan. Në fund, pasi studiova kodin e gjeneruar, kuptova se pas ndërrimit, treguesi i instruksionit aktual rifreskohet nga steka dhe ndoshta mbishkruhet nga kodi i gjeneruar JavaScript. Dhe kështu doli. Rritja e tamponit nga një megabajt në dhjetë nuk çoi në asgjë dhe u bë e qartë se gjeneratori i kodit po funksiononte në rrathë. Ne duhej të kontrollonim që të mos dilnim përtej kufijve të TB-së aktuale, dhe nëse e bënim, atëherë të jepnim adresën e TB-së tjetër me një shenjë minus në mënyrë që të mund të vazhdonim ekzekutimin. Përveç kësaj, kjo zgjidh problemin "cilat funksione të gjeneruara duhet të anulohen nëse kjo pjesë e bytekodit ka ndryshuar?" — vetëm funksioni që korrespondon me këtë bllok përkthimi duhet të zhvlerësohet. Nga rruga, megjithëse korrigjova gjithçka në Chromium (meqenëse përdor Firefox dhe është më e lehtë për mua të përdor një shfletues të veçantë për eksperimente), Firefox më ndihmoi të korrigjoj papajtueshmëritë me standardin asm.js, pas së cilës kodi filloi të funksionojë më shpejt në Krom.

Shembull i kodit të krijuar

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ërfundim

Pra, puna ende nuk ka përfunduar, por jam lodhur duke e çuar fshehurazi këtë ndërtim afatgjatë në perfeksion. Prandaj, vendosa të publikoj atë që kam për momentin. Kodi është pak i frikshëm në vende, sepse ky është një eksperiment dhe nuk është e qartë paraprakisht se çfarë duhet bërë. Ndoshta, atëherë ia vlen të lëshohen komponime normale atomike në krye të një versioni më modern të Qemu. Ndërkohë, ekziston një fije në Gita në një format blog: për çdo "nivel" që është kaluar të paktën disi, është shtuar një koment i detajuar në Rusisht. Në fakt, ky artikull është në një masë të madhe një ritregim i përfundimit git log.

Mund t'i provoni të gjitha këtu (kujdes nga trafiku).

Çfarë tashmë funksionon:

  • procesori virtual x86 funksionon
  • Ekziston një prototip funksional i një gjeneruesi të kodit JIT nga kodi i makinës në JavaScript
  • Ekziston një shabllon për montimin e arkitekturave të tjera të ftuar 32-bit: tani ju mund të admironi Linux për ngrirjen e arkitekturës MIPS në shfletues në fazën e ngarkimit

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

  • Përshpejtoni emulimin. Edhe në modalitetin JIT duket se funksionon më ngadalë se Virtual x86 (por potencialisht ekziston një Qemu e tërë me shumë pajisje dhe arkitektura të emuluara)
  • Për të krijuar një ndërfaqe normale - sinqerisht, unë nuk jam një zhvillues i mirë në internet, kështu që tani për tani kam ribërë guaskën standarde Emscripten sa më mirë që mundem
  • Përpiquni të nisni funksione më komplekse Qemu - rrjetëzim, migrim VM, etj.
  • UPD: do t'ju duhet të paraqisni zhvillimet tuaja të pakta dhe raportet e gabimeve në Emscripten në rrjedhën e sipërme, siç bënë portierët e mëparshëm të Qemu dhe projekteve të tjera. Falënderoj ata që mundën të përdorin në mënyrë implicite kontributin e tyre në Emscripten si pjesë e detyrës sime.

Burimi: www.habr.com

Shto një koment