JIT qo'llab-quvvatlanadigan Qemu.js: siz hali ham qiymani orqaga burishingiz mumkin

Bir necha yil oldin Fabris Bellard jslinux tomonidan yozilgan JavaScript-da yozilgan kompyuter emulyatoridir. Shundan keyin hech bo'lmaganda ko'proq narsa bor edi Virtual x86. Ammo, men bilishimcha, ularning barchasi tarjimon edi, Qemu esa o'sha Fabris Bellard tomonidan ancha oldin yozilgan va, ehtimol, har qanday o'zini hurmat qiladigan zamonaviy emulyator, mehmon kodining JIT kompilyatsiyasini xost tizimi kodiga kiritadi. Menimcha, brauzerlar hal qiladigan vazifaga nisbatan qarama-qarshi vazifani amalga oshirish vaqti keldi: JIT mashina kodini JavaScript-ga kompilyatsiya qilish, buning uchun Qemu portiga eng mantiqiy tuyuldi. Ko'rinishidan, nima uchun Qemu, oddiyroq va foydalanuvchilar uchun qulay emulyatorlar mavjud - xuddi shu VirtualBox, masalan - o'rnatilgan va ishlaydi. Ammo Qemu bir nechta qiziqarli xususiyatlarga ega

  • ochiq manba
  • yadro drayverisiz ishlash qobiliyati
  • tarjimon rejimida ishlash qobiliyati
  • ko'p sonli mezbon va mehmon arxitekturasini qo'llab-quvvatlash

Uchinchi nuqtaga kelsak, men endi tushuntira olamanki, aslida TCI rejimida mehmon mashinasi ko'rsatmalarining o'zi emas, balki ulardan olingan bayt-kod talqin qilinadi, lekin bu mohiyatni o'zgartirmaydi - qurish va ishga tushirish uchun Qemu yangi arxitekturada, agar omadingiz bo'lsa, C kompilyatori kifoya qiladi - kod generatorini yozishni keyinga qoldirish mumkin.

Va endi, ikki yillik bo'sh vaqtimda Qemu manba kodi bilan bemalol o'ylab ko'rganimdan so'ng, ishlaydigan prototip paydo bo'ldi, unda siz allaqachon ishga tushirishingiz mumkin, masalan, Kolibri OS.

Emscripten nima

Hozirgi vaqtda ko'plab kompilyatorlar paydo bo'ldi, ularning yakuniy natijasi JavaScript hisoblanadi. Ba'zilari, masalan, Type Script, dastlab veb uchun yozishning eng yaxshi usuli bo'lishga mo'ljallangan edi. Shu bilan birga, Emscripten mavjud C yoki C++ kodini olish va uni brauzer tomonidan o'qiladigan shaklga kompilyatsiya qilish usulidir. Yoniq Ushbu sahifa Biz taniqli dasturlarning ko'plab portlarini to'pladik: shu yerdaMisol uchun, siz PyPy-ga qarashingiz mumkin - aytmoqchi, ular allaqachon JITga ega deb da'vo qilishadi. Aslida, har bir dasturni oddiygina kompilyatsiya qilish va brauzerda ishga tushirish mumkin emas - ularning soni bor Xususiyatlari, bunga chidashingiz kerak, ammo xuddi shu sahifadagi yozuvda aytilishicha, "Emscripten deyarli har qanday kompilyatsiya qilish uchun ishlatilishi mumkin. ko'chma JavaScript-ga C/C++ kodi". Ya'ni, standart bo'yicha aniqlanmagan xatti-harakatlar bo'lgan bir qator operatsiyalar mavjud, lekin odatda x86 da ishlaydi - masalan, ba'zi arxitekturalarda umuman taqiqlangan o'zgaruvchilarga tenglashtirilmagan kirish. Umuman olganda. , Qemu bu oʻzaro platformali dastur va men ishonmoqchi edim va unda koʻplab noaniq xatti-harakatlar mavjud emas – uni oling va kompilyatsiya qiling, soʻngra JIT bilan biroz oʻylab koʻring – va siz tugatdingiz! ish...

Birinchi urinib ko'ring

Umuman olganda, men Qemu-ni JavaScript-ga ko'chirish g'oyasini ilgari surgan birinchi odam emasman. ReactOS forumida bu Emscripten yordamida mumkinmi degan savol bor edi. Ilgari, Fabris Bellard buni shaxsan qilgani haqida mish-mishlar bor edi, lekin biz jslinux haqida gapirgan edik, bu men bilishimcha, bu JS-da etarli darajada ishlashga qo'lda erishishga urinishdir va noldan yozilgan. Keyinchalik Virtual x86 yozildi - buning uchun aniq manbalar joylashtirildi va ta'kidlanganidek, emulyatsiyaning kattaroq "realizmi" SeaBIOS-dan proshivka sifatida foydalanishga imkon berdi. Bundan tashqari, Emscripten yordamida Qemu portiga kamida bitta urinish bo'lgan - men buni qilishga harakat qildim rozetka juftligi, lekin rivojlanish, men tushunganimdek, muzlatilgan edi.

Shunday qilib, ko'rinadi, mana manbalar, mana Emscripten - uni oling va tuzing. Ammo Qemu bog'liq bo'lgan kutubxonalar va bu kutubxonalar bog'liq bo'lgan kutubxonalar va boshqalar ham bor va ulardan biri libffi, qaysi glibga bog'liq. Internetda Emscripten uchun kutubxonalar portlarining katta to'plamida bittasi borligi haqida mish-mishlar tarqaldi, ammo bunga ishonish qiyin edi: birinchidan, u yangi kompilyator bo'lish uchun mo'ljallanmagan, ikkinchidan, u juda past darajadagi dastur edi. kutubxonani olib, JSga kompilyatsiya qilish uchun. Va bu shunchaki montaj qo'shimchalari masalasi emas - ehtimol, agar siz uni aylantirsangiz, ba'zi chaqiruv konventsiyalari uchun siz stekda kerakli argumentlarni yaratishingiz va ularsiz funktsiyani chaqirishingiz mumkin. Ammo Emscripten juda qiyin: yaratilgan kodni brauzer JS motor optimallashtiruvchisiga tanish qilish uchun ba'zi bir fokuslar qo'llaniladi. Xususan, qayta ishlash deb ataladigan narsa - qabul qilingan LLVM IR-dan foydalanib, ba'zi mavhum o'tish ko'rsatmalariga ega bo'lgan kod generatori aqlga sig'adigan if, halqa va hokazolarni qayta yaratishga harakat qiladi. Xo'sh, argumentlar funktsiyaga qanday uzatiladi? Tabiiyki, JS funktsiyalariga argumentlar sifatida, ya'ni iloji bo'lsa, stek orqali emas.

Boshida JS bilan libffi o'rniga oddiygina yozish va standart testlarni o'tkazish g'oyasi bor edi, lekin oxirida men sarlavha fayllarimni mavjud kod bilan ishlashi uchun qanday qilib yaratishni bilmay qoldim - nima qilishim mumkin? Ular aytganidek, "Vazifalar shunchalik murakkab "Biz shunchalik ahmoqmizmi?" Men libffi-ni boshqa arxitekturaga o‘tkazishim kerak edi, ta’bir joiz bo‘lsa, Emscripten-da ichki yig‘ish uchun ikkala makros ham (Javascriptda, ha – arxitektura qanday bo‘lishidan qat’iy nazar, assembler) va tezda yaratilgan kodni ishga tushirish imkoniyati mavjud. Umuman olganda, bir muncha vaqt platformaga bog'liq bo'lgan libffi fragmentlari bilan shug'ullanganimdan so'ng, men kompilyatsiya qilinadigan kodni oldim va uni birinchi duch kelgan testda ishlatdim. Ajablanarlisi shundaki, sinov muvaffaqiyatli o'tdi. Mening dahoyimdan hayratda qoldim - hazil emas, u birinchi ishga tushirilgandanoq ishladi - men hali ham ko'zlarimga ishonmay, natijada paydo bo'lgan kodga yana bir bor qaradim, keyin qayerda qazishni baholadim. Bu erda men ikkinchi marta aqldan ozdim - mening vazifam qilgan yagona narsa ffi_call - bu muvaffaqiyatli qo'ng'iroq haqida xabar berdi. Hech qanday qo'ng'iroq yo'q edi. Shunday qilib, men birinchi tortishish so'rovimni yubordim, u testdagi har qanday olimpiada o'quvchisi uchun tushunarli bo'lgan xatoni tuzatdi - haqiqiy raqamlarni taqqoslamaslik kerak a == b va hatto qanday qilib a - b < EPS - siz modulni ham eslab qolishingiz kerak, aks holda 0 1/3 ga teng bo'lib chiqadi... Umuman olganda, men eng oddiy testlardan o'tadigan va glib bilan ishlaydigan ma'lum bir libffi portini o'ylab topdim. kompilyatsiya qilingan - zarur bo'lishiga qaror qildim, keyinroq qo'shaman. Oldinga qarab, shuni aytamanki, kompilyator yakuniy kodga libffi funktsiyasini ham kiritmagan.

Ammo, yuqorida aytib o'tganimdek, ba'zi cheklovlar mavjud va turli xil noaniq xatti-harakatlardan bepul foydalanish orasida yanada noxush xususiyat yashiringan - dizayn bo'yicha JavaScript umumiy xotira bilan ko'p ish zarralarini qo'llab-quvvatlamaydi. Asosan, buni odatda yaxshi g'oya deb atash mumkin, lekin arxitekturasi C iplari bilan bog'langan kodni ko'chirish uchun emas. Umuman olganda, Firefox umumiy ishchilarni qo'llab-quvvatlash bilan tajriba o'tkazmoqda va Emscripten ular uchun pthread dasturiga ega, ammo men bunga bog'liq bo'lishni xohlamadim. Men asta-sekin Qemu kodidan multithreadingni yo'q qilishim kerak edi - ya'ni iplar qayerda ishlayotganini bilib oling, ushbu ipda ishlaydigan tsiklning tanasini alohida funktsiyaga o'tkazing va bunday funktsiyalarni asosiy tsikldan birma-bir chaqirishim kerak edi.

Ikkinchi urinib ko'ring

Bir paytlar muammo hali ham borligi va kod atrofida tasodifan qo'ltiq tayoqchalarini silkitish hech qanday yaxshilikka olib kelmasligi ayon bo'ldi. Xulosa: tayoqchalarni qo'shish jarayonini qandaydir tarzda tizimlashtirishimiz kerak. Shuning uchun, o'sha paytda yangi bo'lgan 2.4.1 versiyasi olindi (2.5.0 emas, chunki kim biladi, yangi versiyada hali qo'lga olinmagan xatolar bo'ladi va menda o'zimning xatolarim etarli. ) va birinchi narsa uni xavfsiz tarzda qayta yozish edi thread-posix.c. Xo'sh, ya'ni xavfsiz: agar kimdir blokirovkaga olib keladigan operatsiyani bajarishga harakat qilsa, darhol funktsiya chaqiriladi abort() - Albatta, bu barcha muammolarni birdaniga hal qilmadi, lekin hech bo'lmaganda bu nomuvofiq ma'lumotlarni jimgina olishdan ko'ra yoqimliroq edi.

Umuman olganda, Emscripten variantlari kodni JS ga ko'chirishda juda foydali -s ASSERTIONS=1 -s SAFE_HEAP=1 - ular aniqlanmagan xatti-harakatlarning ba'zi turlarini, masalan, tekislanmagan manzilga qo'ng'iroqlarni (bu kabi terilgan massivlar uchun kodga mutlaqo mos kelmaydi) ushlaydi. HEAP32[addr >> 2] = 1) yoki noto'g'ri argumentlar soni bilan funktsiyani chaqirish.

Aytgancha, hizalama xatolar alohida masala. Yuqorida aytib o'tganimdek, Qemu kod ishlab chiqarish uchun TCI (kichik kod tarjimoni) va Qemu'ni yangi arxitekturada qurish va ishga tushirish uchun "degenerativ" sharhlovchi backendga ega, agar omadingiz bo'lsa, C kompilyatori kifoya qiladi. "Agar omadingiz bo'lsa". Menga omad kulib boqmadi va ma'lum bo'ldiki, TCI bayt kodini tahlil qilishda tenglashtirilmagan kirishdan foydalanadi. Ya'ni, har xil turdagi ARM va boshqa arxitekturalarda, albatta, tekislangan kirishda Qemu kompilyatsiya qiladi, chunki ular mahalliy kodni yaratadigan oddiy TCG backendiga ega, ammo TCI ular ustida ishlaydimi yoki yo'qmi - bu boshqa savol. Biroq, ma'lum bo'lishicha, TCI hujjatlari shunga o'xshash narsani aniq ko'rsatgan. Natijada, Qemu-ning boshqa qismida topilgan kodga tenglashtirilmagan o'qish uchun funksiya chaqiruvlari qo'shildi.

To'pni yo'q qilish

Natijada, TCI-ga tenglashtirilmagan kirish tuzatildi, asosiy halqa yaratildi, u o'z navbatida protsessor, RCU va boshqa kichik narsalar deb ataladi. Va shuning uchun men variant bilan Qemu-ni ishga tushiraman -d exec,in_asm,out_asm, ya'ni qaysi kod bloklari bajarilayotganini, shuningdek, translyatsiya paytida mehmon kodi nima ekanligini, xost kodi qanday bo'lganini yozishingiz kerak (bu holda bayt kod). U ishga tushadi, bir nechta tarjima bloklarini bajaradi, men qoldirgan disk raskadrovka xabarini yozadi, RCU endi ishga tushadi va... ishlamay qoladi. abort() funksiya ichida free(). Funksiya bilan shug'ullanish orqali free() Ajratilgan xotiradan oldingi sakkiz baytda joylashgan yig'ma blokning sarlavhasida blok hajmi yoki shunga o'xshash narsa o'rniga axlat borligini aniqlashga muvaffaq bo'ldik.

Uyumni yo'q qilish - naqadar yoqimli ... Bunday holatda, foydali vosita bor - (agar iloji bo'lsa) bir xil manbalardan, mahalliy binarni yig'ing va Valgrind ostida boshqaring. Biroz vaqt o'tgach, ikkilik tayyor bo'ldi. Men uni bir xil variantlar bilan ishga tushiraman - u hatto ishga tushirish vaqtida ham ishlamay qoladi. Bu, albatta, yoqimsiz - aftidan, manbalar bir xil emas edi, bu ajablanarli emas, chunki konfiguratsiya biroz boshqacha variantlarni ko'rib chiqdi, lekin menda Valgrind bor - avval men bu xatoni tuzataman, keyin omadim kelsa. , asl nusxasi paydo bo'ladi. Men Valgrind ostida xuddi shu narsani ishlayapman ... Y-y-y, y-y-y, uh-uh, u boshlandi, odatdagidek ishga tushirildi va xotiraga noto'g'ri kirish haqida hech qanday ogohlantirishsiz asl xatolikdan o'tdi, qulash haqida gapirmasa ham. Hayot, ular aytganidek, meni bunga tayyorlamadi - halokatli dastur Walgrind ostida ishga tushirilganda ishdan chiqishni to'xtatadi. Bu nima bo'lganligi sir. Mening farazim shundan iboratki, ishga tushirish vaqtidagi avariyadan so'ng joriy ko'rsatma yaqinida bir marta gdb ish ko'rsatdi. memset-a ikkalasidan ham foydalangan holda to'g'ri ko'rsatgich bilan mmx, yoki xmm registrlar, keyin, ehtimol, bu qandaydir moslashtirish xatosi edi, garchi bunga ishonish hali ham qiyin.

Mayli, Valgrind bu yerda yordam bermayapti. Va bu erda eng jirkanch narsa boshlandi - hamma narsa hatto boshlanganga o'xshaydi, lekin millionlab ko'rsatmalar oldin sodir bo'lishi mumkin bo'lgan voqea tufayli mutlaqo noma'lum sabablarga ko'ra ishdan chiqadi. Uzoq vaqt davomida qanday qilib yaqinlashish ham aniq emas edi. Oxir-oqibat, men hali ham o'tirib, disk raskadrovka qilishim kerak edi. Sarlavha qayta yozilganini chop etish uning raqamga o'xshamasligini, balki qandaydir ikkilik ma'lumotlarga o'xshashligini ko'rsatdi. Va, mana, bu ikkilik satr BIOS faylida topildi - ya'ni endi bu bufer to'lib ketganligini oqilona ishonch bilan aytish mumkin edi va hatto bu buferga yozilganligi aniq. Xo'sh, shunga o'xshash narsa - Emscripten-da, xayriyatki, manzil maydonining tasodifiyligi yo'q, unda teshiklar ham yo'q, shuning uchun siz oxirgi ishga tushirishdan boshlab ko'rsatgich orqali ma'lumotlarni chiqarish uchun kodning o'rtasida biron bir joyga yozishingiz mumkin, ma'lumotlarga qarang, ko'rsatkichga qarang va agar u o'zgarmagan bo'lsa, o'ylash uchun oziq-ovqat oling. To'g'ri, har qanday o'zgarishlardan keyin ulanish uchun bir necha daqiqa kerak bo'ladi, lekin nima qila olasiz? Natijada, BIOS-ni vaqtinchalik buferdan mehmon xotirasiga ko'chiradigan ma'lum bir qator topildi - va, albatta, buferda etarli joy yo'q edi. Ushbu g'alati bufer manzilining manbasini topish funktsiyaga olib keldi qemu_anon_ram_alloc faylda oslib-posix.c - mantiq shunday edi: ba'zida manzilni 2 MB hajmdagi ulkan sahifaga tekislash foydali bo'lishi mumkin, buning uchun biz so'raymiz. mmap avval bir oz ko'proq, keyin esa yordam bilan ortiqchasini qaytaramiz munmap. Va agar bunday tekislash talab qilinmasa, biz 2 MB o'rniga natijani ko'rsatamiz getpagesize() - mmap u hali ham tekislangan manzilni beradi ... Shunday qilib, Emscripten'da mmap shunchaki qo'ng'iroq qiladi malloc, lekin, albatta, u sahifada tekislanmaydi. Umuman olganda, bir necha oy davomida meni xafa qilgan xato o'zgartirish orqali tuzatildi dvux chiziqlar.

Chaqiruv funksiyalarining xususiyatlari

Va endi protsessor nimanidir hisoblamoqda, Qemu ishdan chiqmaydi, lekin ekran yoqilmaydi va protsessor chiqishiga qarab tezda halqalarga o'tadi. -d exec,in_asm,out_asm. Gipoteza paydo bo'ldi: taymer uzilishlari (yoki umuman, barcha uzilishlar) kelmaydi. Haqiqatan ham, agar siz biron bir sababga ko'ra ishlagan mahalliy yig'ilishdagi uzilishlarni burab qo'ysangiz, xuddi shunday rasmga ega bo'lasiz. Ammo bu umuman javob emas edi: yuqoridagi variant bilan chiqarilgan izlarni taqqoslash shuni ko'rsatdiki, ijro etilish traektoriyalari juda erta farqlanadi. Bu erda shuni aytish kerakki, ishga tushirish moslamasi yordamida yozilgan narsalarni taqqoslash emrun mahalliy yig'ilishning chiqishi bilan disk raskadrovka chiqishi butunlay mexanik jarayon emas. Brauzerda ishlaydigan dastur qanday ulanishini aniq bilmayman emrun, lekin chiqishdagi ba'zi chiziqlar qayta tartibga solingan bo'lib chiqadi, shuning uchun farqdagi farq traektoriyalar ajralib chiqdi deb taxmin qilish uchun hali sabab emas. Umuman olganda, ko'rsatmalarga ko'ra aniq bo'ldi ljmpl turli manzillarga o'tish mavjud bo'lib, yaratilgan bayt-kod tubdan farq qiladi: birida yordamchi funksiyani chaqirish bo'yicha ko'rsatma mavjud, ikkinchisida esa yo'q. Ko'rsatmalarni o'rganib chiqqandan so'ng va ushbu ko'rsatmalarni tarjima qiladigan kodni o'rganib chiqqandan so'ng, birinchi navbatda, registrda ro'yxatdan o'tganligi ma'lum bo'ldi. cr0 protsessorni himoyalangan rejimga o'tkazgan, ikkinchidan, JS versiyasi hech qachon himoyalangan rejimga o'tmaganligi haqida yordamchi yordamida yozuv ham amalga oshirildi. Ammo haqiqat shundaki, Emscripten-ning yana bir xususiyati - bu ko'rsatmalarni bajarish kabi kodlarga toqat qilishni istamasligi. call TCI da, bu har qanday funktsiya ko'rsatkichi turiga olib keladi long long f(int arg0, .. int arg9) - funktsiyalar to'g'ri argumentlar soni bilan chaqirilishi kerak. Agar ushbu qoida buzilgan bo'lsa, disk raskadrovka sozlamalariga qarab, dastur buziladi (bu yaxshi) yoki umuman noto'g'ri funktsiyani chaqiradi (bu disk raskadrovka qilish achinarli bo'ladi). Uchinchi variant ham bor - argumentlarni qo'shadigan / olib tashlaydigan o'ramlarni yaratishni yoqing, lekin umuman olganda, bu o'ramlar juda ko'p joy egallaydi, garchi aslida menga yuzdan bir oz ko'proq o'ram kerak. Buning o'zi juda achinarli, ammo jiddiyroq muammo paydo bo'ldi: o'rash funktsiyalarining yaratilgan kodida argumentlar o'zgartirildi va aylantirildi, lekin ba'zida yaratilgan argumentlar bilan funktsiya chaqirilmadi - yaxshi, xuddi mening libffi dasturim. Ya'ni, ba'zi yordamchilar shunchaki qatl qilinmagan.

Yaxshiyamki, Qemu-da sarlavha fayli ko'rinishidagi mashina tomonidan o'qiladigan yordamchi ro'yxatlar mavjud.

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

Ular juda kulgili ishlatiladi: birinchidan, makrolar eng g'alati tarzda qayta ta'riflanadi DEF_HELPER_n, va keyin yoqiladi helper.h. Ibratli tuzilmani ishga tushirish va vergulga kengaytirilganda, so'ngra massiv aniqlanadi va elementlar o'rniga - #include <helper.h> Natijada, men kutubxonani ish joyida sinab ko'rish imkoniyatiga ega bo'ldim pyparsing, va skript yozilgan bo'lib, ular aynan ular uchun zarur bo'lgan funksiyalar uchun aynan o'sha o'ramlarni hosil qiladi.

Shunday qilib, shundan keyin protsessor ishlayotgandek tuyuldi. Buning sababi, memtest86+ mahalliy yig'ilishda ishlay olgan bo'lsa-da, ekran hech qachon ishga tushirilmaganga o'xshaydi. Bu erda Qemu blokining I/U kodi koroutinlarda yozilganligini aniqlashtirish kerak. Emscripten o'zining juda murakkab dasturiga ega, ammo u hali ham Qemu kodida qo'llab-quvvatlanishi kerak edi va siz hozir protsessorni disk raskadrovka qilishingiz mumkin: Qemu variantlarni qo'llab-quvvatlaydi -kernel, -initrd, -append, uning yordamida siz Linuxni yoki, masalan, memtest86+ ni blokli qurilmalardan umuman foydalanmasdan yuklashingiz mumkin. Ammo bu erda muammo: mahalliy yig'ilishda Linux yadrosi chiqishini konsolga ko'rish mumkin. -nographic, va brauzerdan u ishga tushirilgan joydan terminalga chiqmaydi emrun, kelmadi. Ya'ni, bu aniq emas: protsessor ishlamayapti yoki grafik chiqishi ishlamayapti. Keyin biroz kutish xayolimga keldi. Ma'lum bo'lishicha, "protsessor uxlamayapti, shunchaki sekin miltillaydi" va taxminan besh daqiqadan so'ng yadro konsolga bir nechta xabarlarni tashladi va osishda davom etdi. Protsessor, umuman olganda, ishlashi aniq bo'ldi va biz SDL2 bilan ishlash uchun kodni o'rganishimiz kerak. Afsuski, men bu kutubxonadan qanday foydalanishni bilmayman, shuning uchun ba'zi joylarda tasodifiy harakat qilishim kerak edi. Bir nuqtada, parallel0 chizig'i ko'k fonda ekranda miltilladi, bu ba'zi fikrlarni taklif qildi. Oxir-oqibat, muammo Qemu bir jismoniy oynada bir nechta virtual oynalarni ochishida bo'lib chiqdi, ular orasida Ctrl-Alt-n yordamida o'tish mumkin: u mahalliy tuzilmada ishlaydi, lekin Emscripten-da emas. Variantlar yordamida keraksiz oynalardan qutulganingizdan so'ng -monitor none -parallel none -serial none va har bir ramkada butun ekranni kuch bilan qayta chizish bo'yicha ko'rsatmalar, hamma narsa birdan ishladi.

Korutinlar

Shunday qilib, brauzerda emulyatsiya ishlaydi, lekin siz unda biron bir qiziqarli bitta floppi ishga tushira olmaysiz, chunki kiritish-chiqarish bloki yo'q - siz koroutinlarni qo'llab-quvvatlashni amalga oshirishingiz kerak. Qemu allaqachon bir nechta koroutin orqa tomonlarga ega, ammo JavaScript va Emscripten kod generatorining tabiati tufayli siz shunchaki steklarni jonglyor qilishni boshlay olmaysiz. Ko'rinishidan, "hamma narsa yo'qoldi, gips olib tashlandi", ammo Emscripten ishlab chiquvchilari allaqachon hamma narsaga g'amxo'rlik qilishgan. Bu juda kulgili amalga oshiriladi: keling, bunday shubhali funktsiya chaqiruvini chaqiraylik emscripten_sleep va Asyncify mexanizmidan foydalanadigan bir qancha boshqalar, shuningdek, ko'rsatgich qo'ng'iroqlari va har qanday funktsiyaga qo'ng'iroqlar, agar oldingi ikkita holatdan biri stekning pastroqda sodir bo'lishi mumkin. Va endi, har bir shubhali qo'ng'iroqdan oldin, biz asinxron kontekstni tanlaymiz va qo'ng'iroqdan so'ng darhol asinxron qo'ng'iroq sodir bo'lganligini tekshiramiz va agar mavjud bo'lsa, biz ushbu asinxron kontekstda barcha mahalliy o'zgaruvchilarni saqlaymiz, qaysi funktsiyani ko'rsatamiz. boshqaruvni bajarishni davom ettirishimiz kerak bo'lgan vaqtga o'tkazish va joriy funktsiyadan chiqish. Bu erda ta'sirni o'rganish uchun imkoniyatlar mavjud isrofgarchilik — asinxron qoʻngʻiroqdan qaytgandan soʻng kodni bajarishni davom ettirish ehtiyojlari uchun kompilyator shubhali qoʻngʻiroqdan soʻng boshlangan funktsiyaning “qoʻngʻirchoqlarini” hosil qiladi — shunga oʻxshash: agar n ta shubhali qoʻngʻiroq boʻlsa, u holda funksiya n/2 joyda kengaytiriladi. marta - bu hali ham, agar bo'lmasa ham Yodda tutingki, har bir potentsial asinxron qo'ng'iroqdan so'ng, asl funktsiyaga ba'zi mahalliy o'zgaruvchilarni saqlashni qo'shishingiz kerak. Keyinchalik, men hatto Python-da oddiy skript yozishga majbur bo'ldim, bu juda ko'p ishlatiladigan funktsiyalar to'plamiga asoslanib, go'yoki "asinxroniya o'z-o'zidan o'tib ketishiga yo'l qo'ymaydi" (ya'ni, stekni reklama qilish va men aytib o'tgan hamma narsa yo'q). ularda ishlash), ko'rsatkichlar orqali qo'ng'iroqlarni ko'rsatadi, bu funktsiyalar asinxron hisoblanmasligi uchun kompilyator tomonidan funktsiyalar e'tiborga olinmasligi kerak. Va keyin 60 MB dan kichik JS fayllari aniq haddan tashqari ko'p - aytaylik, kamida 30. Garchi men bir marta montaj skriptini o'rnatayotgan edim va tasodifan bog'lovchi variantlarini o'chirib tashladim, ular orasida -O3. Men yaratilgan kodni ishga tushiraman va Chromium xotirani iste'mol qiladi va ishdan chiqadi. Keyin tasodifan uning yuklab olmoqchi bo'lgan narsasiga qaradim... Xo'sh, nima deyman, agar mendan 500+ MB Javascriptni o'ylab o'rganishni va optimallashtirishni so'rashsa, men ham qotib qolgan bo'lardim.

Afsuski, Asyncify qo'llab-quvvatlash kutubxonasi kodidagi tekshiruvlar to'liq mos kelmadi longjmp-s virtual protsessor kodida ishlatiladi, lekin bu tekshiruvlarni o'chirib qo'yadigan va kontekstlarni kuch bilan tiklaydigan kichik tuzatishdan so'ng, kod ishladi. Va keyin g'alati narsa boshlandi: ba'zida sinxronizatsiya kodidagi tekshiruvlar ishga tushirildi - agar ijro mantig'iga ko'ra, uni blokirovka qilish kerak bo'lsa, kodni buzadiganlar - kimdir allaqachon qo'lga kiritilgan mutexni olishga harakat qilgan. Yaxshiyamki, bu ketma-ketlashtirilgan kodda mantiqiy muammo emas edi - men oddiygina Emscripten tomonidan taqdim etilgan standart asosiy tsikl funksiyasidan foydalanardim, lekin ba'zida asinxron qo'ng'iroq stekni to'liq ochadi va o'sha paytda u muvaffaqiyatsiz bo'ladi. setTimeout asosiy tsikldan - shunday qilib, kod oldingi iteratsiyadan chiqmasdan asosiy sikl iteratsiyasiga kirdi. Cheksiz tsiklda qayta yozildi va emscripten_sleep, va mutekslar bilan bog'liq muammolar to'xtadi. Kod yanada mantiqiy bo'ldi - aslida menda keyingi animatsiya ramkasini tayyorlaydigan biron bir kod yo'q - protsessor shunchaki biror narsani hisoblab chiqadi va ekran vaqti-vaqti bilan yangilanadi. Biroq, muammolar shu bilan to'xtab qolmadi: ba'zida Qemu ijrosi hech qanday istisnosiz yoki xatoliksiz jimgina tugaydi. O'sha paytda men undan voz kechdim, lekin oldinga qarab, muammo bu ekanligini aytaman: koroutin kod, aslida, foydalanmaydi. setTimeout (yoki hech bo'lmaganda siz o'ylagandek tez-tez emas): funktsiya emscripten_yield shunchaki asinxron chaqiruv bayrog'ini o'rnatadi. Gap shundaki emscripten_coroutine_next asinxron funksiya emas: ichki tomondan u bayroqni tekshiradi, uni qayta o'rnatadi va boshqaruvni kerakli joyga o'tkazadi. Ya'ni, stekning reklamasi shu erda tugaydi. Muammo shundaki, men mavjud coroutine backenddan muhim kod qatorini ko‘chirmaganim uchun korutin hovuzi o‘chirilganida paydo bo‘lgan use-after-free tufayli, funksiya qemu_in_coroutine rost qaytarildi, aslida u yolg'onni qaytarishi kerak edi. Bu qo'ng'iroqqa sabab bo'ldi emscripten_yield, yuqorida stackda hech kim yo'q edi emscripten_coroutine_next, stack eng yuqoriga ochildi, lekin yo'q setTimeout, men allaqachon aytganimdek, namoyish etilmagan.

JavaScript kodini yaratish

Va bu erda, aslida, va'da qilingan "qiyma go'shtni orqaga qaytarish". Unchalik emas. Albatta, agar biz brauzerda Qemu-ni va unda Node.js-ni ishga tushirsak, Qemu-da kod ishlab chiqarilgandan so'ng, biz mutlaqo noto'g'ri JavaScript-ga ega bo'lamiz. Ammo baribir, qandaydir teskari o'zgarish.

Birinchidan, Qemu qanday ishlashi haqida bir oz. Iltimos, meni darhol kechiring: men professional Qemu dasturchisi emasman va mening xulosalarim ba'zi joylarda noto'g'ri bo'lishi mumkin. Ular aytganidek, "talabaning fikri o'qituvchining fikriga, Peanoning aksiomatikasiga va sog'lom fikrga mos kelishi shart emas". Qemuda ma'lum miqdordagi qo'llab-quvvatlanadigan mehmon arxitekturasi mavjud va ularning har biri uchun o'xshash katalog mavjud target-i386. Qurilishda siz bir nechta mehmon arxitekturasini qo'llab-quvvatlashni belgilashingiz mumkin, ammo natijada bir nechta ikkilik bo'ladi. Mehmon arxitekturasini qo'llab-quvvatlash uchun kod, o'z navbatida, TCG (Tiny Code Generator) allaqachon xost arxitekturasi uchun mashina kodiga aylanadigan ba'zi ichki Qemu operatsiyalarini yaratadi. Tcg katalogida joylashgan readme faylida aytilganidek, bu dastlab oddiy C kompilyatorining bir qismi bo'lib, keyinchalik JIT uchun moslashtirilgan. Shuning uchun, masalan, ushbu hujjat nuqtai nazaridan maqsadli arxitektura endi mehmon arxitekturasi emas, balki xost arxitekturasidir. Bir vaqtning o'zida yana bir komponent paydo bo'ldi - Tiny Code Interpreter (TCI), u ma'lum bir xost arxitekturasi uchun kod generatori bo'lmaganda kodni (deyarli bir xil ichki operatsiyalarni) bajarishi kerak. Aslida, uning hujjatlarida ta'kidlanganidek, bu tarjimon har doim ham tezlik bo'yicha nafaqat miqdoriy, balki sifat jihatidan ham JIT kod ishlab chiqaruvchisi kabi ishlamasligi mumkin. Garchi men uning tavsifi to'liq tegishli ekanligiga ishonchim komil emas.

Avvaliga men to'liq huquqli TCG backend yaratishga harakat qildim, lekin tezda manba kodida va bayt-kod ko'rsatmalarining to'liq aniq bo'lmagan tavsifida chalkashib ketdim, shuning uchun men TCI tarjimonini o'rashga qaror qildim. Bu bir qator afzalliklarni berdi:

  • kod generatorini amalga oshirishda siz ko'rsatmalar tavsifiga emas, balki tarjimon kodiga qarashingiz mumkin
  • Siz har bir duch kelgan tarjima bloki uchun emas, balki, masalan, faqat yuzinchi bajarilgandan keyin funktsiyalarni yaratishingiz mumkin.
  • agar yaratilgan kod o'zgarsa (va bu mumkin bo'lsa, yamoq so'zini o'z ichiga olgan nomlarga ega bo'lgan funktsiyalarga ko'ra), men yaratilgan JS kodini bekor qilishim kerak, lekin hech bo'lmaganda uni qayta tiklash uchun biror narsaga ega bo'laman.

Uchinchi nuqtaga kelsak, kod birinchi marta bajarilgandan keyin tuzatish mumkinligiga ishonchim komil emas, lekin birinchi ikkita nuqta etarli.

Dastlab, kod asl bayt-kod ko'rsatmasi manzilida katta kalit ko'rinishida yaratilgan, ammo keyin Emscripten, yaratilgan JS-ni optimallashtirish va qayta ishlash haqidagi maqolani eslab, men ko'proq inson kodini yaratishga qaror qildim, ayniqsa empirik tarzda. Ma'lum bo'lishicha, tarjima blokiga yagona kirish nuqtasi uning Boshlash nuqtasidir. Bir muncha vaqt o'tgach, bizda kod generatori paydo bo'ldi, u ifs bilan kod yaratdi (garchi tsikllarsiz). Ammo omadsizlik yuz berdi, u qulab tushdi va ko'rsatmalar noto'g'ri uzunlikda ekanligi haqida xabar berdi. Bundan tashqari, ushbu rekursiya darajasidagi oxirgi ko'rsatma edi brcond. OK, men ushbu ko'rsatmani rekursiv qo'ng'iroqdan oldin va keyin hosil qilish uchun bir xil tekshiruvni qo'shaman va ... ulardan hech biri bajarilmadi, lekin tasdiqlashni o'zgartirgandan keyin ular hali ham muvaffaqiyatsiz bo'ldi. Oxir-oqibat, yaratilgan kodni o'rganib chiqqanimdan so'ng, men kommutatsiyadan so'ng joriy ko'rsatmaga ko'rsatgich stekdan qayta yuklanganligini va ehtimol yaratilgan JavaScript kodi bilan yozilishini angladim. Va shunday bo'ldi. Buferni bir megabaytdan o'nga oshirish hech narsaga olib kelmadi va kod generatori aylanalarda ishlayotgani aniq bo'ldi. Biz hozirgi sil chegarasidan tashqariga chiqmaganimizni tekshirishimiz kerak edi va agar chiqsak, keyingi TB manzilini minus belgisi bilan bering, shunda biz ijroni davom ettiramiz. Bunga qo'shimcha ravishda, bu "agar ushbu bayt-kod qismi o'zgargan bo'lsa, qaysi yaratilgan funktsiyalarni bekor qilish kerak?" Degan muammoni hal qiladi. — faqat ushbu tarjima blokiga mos keladigan funksiyani bekor qilish kerak. Aytgancha, men Chromium-da hamma narsani tuzatgan bo'lsam ham (men Firefox-dan foydalanaman va tajribalar uchun alohida brauzerdan foydalanish menga qulayroq), Firefox menga asm.js standarti bilan nomuvofiqliklarni tuzatishga yordam berdi, shundan so'ng kod tezroq ishlay boshladi. Chromium.

Yaratilgan kodga misol

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

xulosa

Shunday qilib, ish hali tugallanmagan, lekin men yashirincha bu uzoq muddatli qurilishni mukammallikka olib kelishdan charchadim. Shuning uchun men hozir bor narsamni nashr etishga qaror qildim. Kod joylarda biroz qo'rqinchli, chunki bu tajriba va nima qilish kerakligi oldindan aniq emas. Ehtimol, Qemu-ning zamonaviyroq versiyasi ustiga oddiy atom majburiyatlarini chiqarishga arziydi. Ayni paytda, Gita-da blog formatida mavzu mavjud: hech bo'lmaganda qandaydir tarzda o'tgan har bir "daraja" uchun rus tilida batafsil sharh qo'shilgan. Aslida, ushbu maqola ko'p jihatdan xulosani qayta ko'rib chiqishdir git log.

Hammasini sinab ko'rishingiz mumkin shu yerda (trafikdan ehtiyot bo'ling).

Nima allaqachon ishlaydi:

  • x86 virtual protsessor ishlaydi
  • JIT kod generatorining mashina kodidan JavaScript-ga ishlaydigan prototipi mavjud
  • Boshqa 32-bitli mehmon arxitekturalarini yig'ish uchun shablon mavjud: hozirda siz Linuxni yuklash bosqichida brauzerda muzlab qolgan MIPS arxitekturasiga qoyil qolishingiz mumkin.

Yana nima qila olasiz

  • Emulyatsiyani tezlashtiring. JIT rejimida ham u Virtual x86 dan sekinroq ishlaydi (lekin potentsial ravishda ko'plab emulyatsiya qilingan apparat va arxitekturaga ega butun Qemu mavjud)
  • Oddiy interfeys yaratish uchun - ochig'ini aytsam, men yaxshi veb-dasturchi emasman, shuning uchun men hozircha standart Emscripten qobig'ini iloji boricha qayta yaratdim.
  • Keyinchalik murakkab Qemu funktsiyalarini ishga tushirishga harakat qiling - tarmoq, VM migratsiyasi va boshqalar.
  • UPS: Qemu va boshqa loyihalarning oldingi porterlari singari siz o'zingizning bir nechta ishlanmalaringiz va xatolar haqida hisobotlarni Emscripten upstream-ga topshirishingiz kerak bo'ladi. Mening vazifamning bir qismi sifatida Emscripten-ga qo'shgan hissalarini bilvosita ishlata olganlari uchun ularga rahmat.

Manba: www.habr.com

a Izoh qo'shish