JIT dəstəyi ilə Qemu.js: doldurma hələ də geri qaytarıla bilər

Bir neçə il əvvəl Fabrice Bellard jslinux tərəfindən yazılmışdır JavaScript-də yazılmış kompüter emulyatorudur. Bundan sonra ən azı daha çox oldu Virtual x86. Ancaq bildiyimə görə, onların hamısı tərcüməçi idi, eyni Fabrice Bellard tərəfindən daha əvvəl yazılmış Qemu və yəqin ki, hər hansı özünə hörmət edən müasir emulyator, qonaq kodunun JIT kompilyasiyasını host sistem koduna çevirir. Mənə elə gəldi ki, brauzerlərin həll etdiyi məsələ ilə bağlı əks tapşırığı yerinə yetirməyin vaxtı çatıb: maşın kodunun JavaScript-də JIT tərtibi, bunun üçün Qemu portu ən məntiqli görünürdü. Görünür, niyə Qemu, daha sadə və istifadəçi dostu emulyatorlar var - eyni VirtualBox, məsələn - quraşdırılıb işləyir. Lakin Qemunun bir neçə maraqlı xüsusiyyətləri var

  • açıq mənbə
  • kernel sürücüsü olmadan işləmək bacarığı
  • tərcüməçi rejimində işləmək bacarığı
  • çoxlu sayda həm ev sahibi, həm də qonaq arxitekturasına dəstək

Üçüncü nöqtəyə gəldikdə, indi izah edə bilərəm ki, əslində TCI rejimində qonaq maşın təlimatlarının özləri deyil, onlardan alınan bayt kodu şərh olunur, lakin bu mahiyyəti dəyişmir - qurmaq və işlətmək üçün Qemu yeni arxitekturada, şanslısınızsa, A C tərtibçisi kifayətdir - kod generatorunun yazılması təxirə salına bilər.

İndi, iki il boş vaxtımda Qemu mənbə kodu ilə yavaş-yavaş məşğul olduqdan sonra, məsələn, Kolibri OS-ni işlədə biləcəyiniz işləyən bir prototip ortaya çıxdı.

Emscripten nədir

Hal-hazırda çoxlu kompilyatorlar peyda olub, onların son nəticəsi JavaScript-dir. Bəziləri, Tip Skripti kimi, əvvəlcə veb üçün yazmağın ən yaxşı yolu olmaq üçün nəzərdə tutulmuşdu. Eyni zamanda, Emscripten mövcud C və ya C++ kodunu götürmək və onu brauzer tərəfindən oxuna bilən formada tərtib etmək üsuludur. Aktiv bu səhifə Biz tanınmış proqramların çoxlu portlarını topladıq: buradaMəsələn, PyPy-ə baxa bilərsiniz - yeri gəlmişkən, onlar artıq JIT-ə sahib olduqlarını iddia edirlər. Əslində, hər bir proqramı sadəcə tərtib etmək və brauzerdə işlətmək mümkün deyil - bir sıra var xüsusiyyətləri, buna dözmək məcburiyyətindəsiniz, lakin eyni səhifədəki yazıda deyilir ki, “Emscripten demək olar ki, hər hansı bir mətni tərtib etmək üçün istifadə edilə bilər. portatif JavaScript-ə C/C++ kodu". Yəni, standarta uyğun olaraq qeyri-müəyyən davranış olan, lakin adətən x86-da işləyən bir sıra əməliyyatlar var - məsələn, bəzi arxitekturalarda ümumiyyətlə qadağan edilən dəyişənlərə uyğunlaşdırılmamış giriş. Ümumiyyətlə. , Qemu platformalararası proqramdır və mən inanmaq istəyirdim və o, artıq bir çox qeyri-müəyyən davranışa malik deyil - onu götür və tərtib et, sonra JIT ilə bir az məşğul ol - və bitirdin! hal...

İlk cəhd

Ümumiyyətlə, mən Qemu-nu JavaScript-ə köçürmək ideyası ilə çıxış edən ilk şəxs deyiləm. ReactOS forumunda Emscripten istifadə edərək bunun mümkün olub-olmaması ilə bağlı bir sual verildi. Hətta əvvəllər, Fabrice Bellard-ın bunu şəxsən etdiyi barədə şayiələr var idi, lakin biz bildiyimə görə, JS-də əl ilə kifayət qədər performans əldə etmək cəhdidir və sıfırdan yazılmış jslinux-dan danışırdıq. Daha sonra Virtual x86 yazıldı - bunun üçün qeyri-müəyyən mənbələr yerləşdirildi və deyildiyi kimi, emulyasiyanın daha böyük "reallığı" SeaBIOS-dan proqram təminatı kimi istifadə etməyə imkan verdi. Bundan əlavə, Emscripten istifadə edərək Qemu portu üçün ən azı bir cəhd oldu - mən bunu etməyə çalışdım rozetka cütü, amma inkişaf, anladığım qədər, donmuşdu.

Beləliklə, görünə bilər ki, burada mənbələr, burada Emscripten var - götür və tərtib et. Amma Qemunun asılı olduğu kitabxanalar da var, o kitabxanaların da asılı olduğu kitabxanalar və s. və onlardan biri libffi, hansı glibdən asılıdır. İnternetdə Emscripten üçün kitabxanaların böyük kolleksiyasında birinin olması ilə bağlı şayiələr var idi, lakin buna inanmaq çətin idi: birincisi, o, yeni tərtibçi olmaq üçün nəzərdə tutulmamışdı, ikincisi, çox aşağı səviyyəli proqram idi. yalnız götürmək və JS-ə tərtib etmək üçün kitabxana. Və bu, sadəcə montaj əlavələri məsələsi deyil - yəqin ki, onu büksəniz, bəzi çağırış konvensiyaları üçün yığında lazımi arqumentlər yarada və onlar olmadan funksiyanı çağıra bilərsiniz. Lakin Emscripten çətin bir şeydir: yaradılan kodu brauzer JS mühərriki optimallaşdırıcısına tanış etmək üçün bəzi fəndlərdən istifadə olunur. Xüsusilə, sözdə relooping - bəzi mücərrəd keçid təlimatları ilə qəbul edilmiş LLVM IR-dən istifadə edən kod generatoru inandırıcı ifs, döngələr və s. Yaxşı, arqumentlər funksiyaya necə ötürülür? Təbii ki, JS funksiyalarına arqumentlər kimi, yəni mümkünsə yığın vasitəsilə deyil.

Başlanğıcda sadəcə olaraq JS ilə libffi-nin əvəzini yazmaq və standart testlər aparmaq fikri var idi, amma sonda başlıq fayllarımı mövcud kodla işləmək üçün necə düzəltmək barədə çaşqınlıq içində qaldım - nə edə bilərəm? necə deyərlər, “Vəzifələr bu qədər mürəkkəbdirmi “Biz belə axmaqıq?” Mən libffi-ni başqa arxitekturaya köçürməli oldum, belə desək, - xoşbəxtlikdən, Emscripten-də daxili montaj üçün hər iki makro (Javascript-də, bəli - arxitekturadan asılı olmayaraq, montajçı) və tez yaradılan kodu işlətmək imkanı var. Ümumiyyətlə, platformadan asılı libffi fraqmentləri ilə bir müddət məşğul olduqdan sonra bəzi tərtib edilə bilən kodlar əldə etdim və rast gəldiyim ilk testdə onu işlətdim. Məni təəccübləndirən odur ki, sınaq uğurlu alındı. Dahiliyimə heyrətə gəldim - zarafat deyil, ilk buraxılışdan işlədi - mən hələ də gözlərimə inanmadım, nəticədə ortaya çıxan koda yenidən baxmağa, daha sonra harada qazacağımı qiymətləndirməyə getdim. Burada ikinci dəfə dəli oldum - funksiyamın etdiyi yeganə şey oldu ffi_call - bu uğurlu zəng haqqında məlumat verdi. Özü də zəng yox idi. Beləliklə, mən ilk sorğumu göndərdim, o, testdəki hər hansı bir olimpiada şagirdi üçün aydın olan səhvi düzəltdi - real rəqəmlər müqayisə edilməməlidir. a == b və hətta necə a - b < EPS - modulu da yadda saxlamaq lazımdır, əks halda 0 çox 1/3-ə bərabər olacaq... Ümumiyyətlə, mən libffi-nin müəyyən bir portu ilə gəldim, o, ən sadə testlərdən keçir və hansı glib ilə işləyir. tərtib - lazım olacağına qərar verdim, sonra əlavə edəcəyəm. İrəliyə baxaraq deyim ki, məlum oldu ki, kompilyator hətta son koda libffi funksiyasını da daxil etməyib.

Lakin, artıq dediyim kimi, bəzi məhdudiyyətlər var və müxtəlif qeyri-müəyyən davranışların sərbəst istifadəsi arasında daha xoşagəlməz bir xüsusiyyət gizlədilib - dizaynla JavaScript paylaşılan yaddaşla çox iş parçacığını dəstəkləmir. Prinsipcə, bunu adətən hətta yaxşı ideya adlandırmaq olar, lakin arxitekturası C mövzularına bağlı olan kodu daşımaq üçün deyil. Ümumiyyətlə, Firefox paylaşılan işçiləri dəstəkləməklə sınaqdan keçirir və Emscripten-in onlar üçün pthread tətbiqi var, lakin mən ondan asılı olmaq istəmədim. Qemu kodundan yavaş-yavaş multithreading kökünü çıxarmalı oldum - yəni iplərin harada işlədiyini öyrənməli, bu ipdə işləyən döngənin gövdəsini ayrıca bir funksiyaya köçürməli və əsas döngədən belə funksiyaları bir-bir çağırmalı idim.

İkinci cəhd edin

Nə vaxtsa problemin hələ də qaldığı və kodun ətrafına təsadüfən qoltuq dəyənəklərinin gəzdirilməsinin heç bir yaxşılığa səbəb olmadığı aydın oldu. Nəticə: qoltuqaltıların əlavə edilməsi prosesini bir şəkildə sistemləşdirməliyik. Ona görə də o vaxt təzə olan 2.4.1 versiyası götürüldü (2.5.0 yox, çünki kim bilir yeni versiyada hələ tutulmamış səhvlər olacaq və mənim öz səhvlərim kifayət qədərdir. ) və ilk şey onu təhlükəsiz şəkildə yenidən yazmaq idi thread-posix.c. Yaxşı, yəni təhlükəsizdir: kimsə bloklamaya səbəb olan bir əməliyyat keçirməyə cəhd edərsə, funksiya dərhal çağırıldı abort() - əlbəttə ki, bu, bütün problemləri bir anda həll etmədi, amma heç olmasa, uyğun olmayan məlumatları sakitcə qəbul etməkdən daha xoş idi.

Ümumiyyətlə, Emscripten variantları kodu JS-ə köçürmək üçün çox faydalıdır -s ASSERTIONS=1 -s SAFE_HEAP=1 - onlar qeyri-müəyyən davranış növlərini, məsələn, sıralanmamış ünvana edilən zəngləri tuturlar (bu, tipli massivlərin kodu ilə heç də uyğun gəlmir). HEAP32[addr >> 2] = 1) və ya arqumentlərin səhv sayı ilə funksiyanı çağırmaq.

Yeri gəlmişkən, hizalanma səhvləri ayrı bir məsələdir. Artıq dediyim kimi, Qemu kod yaratmaq üçün TCI (kiçik kod tərcüməçisi) üçün “degenerativ” şərhedici arxa plana malikdir və Qemu-nu yeni arxitekturada qurmaq və işlətmək üçün şanslısınızsa, C kompilyatoru kifayətdir. "bəxtin varsa". Bəxtim gətirmədi və məlum oldu ki, TCI bayt kodunu təhlil edərkən düzlənməmiş girişdən istifadə edir. Yəni, hər cür ARM və mütləq səviyyəli girişi olan digər arxitekturalarda Qemu tərtib edir, çünki onların yerli kodu yaradan normal TCG arxa hissəsi var, lakin TCI-nin onlar üzərində işləyəcəyi başqa sualdır. Ancaq məlum oldu ki, TCI sənədləri oxşar bir şeyi açıq şəkildə göstərdi. Nəticədə, Qemunun başqa bir hissəsində aşkar edilən koda düzülməmiş oxumaq üçün funksiya çağırışları əlavə edildi.

Yığın məhvi

Nəticədə, TCI-yə uyğunlaşdırılmamış giriş düzəldildi, öz növbəsində prosessor, RCU və digər kiçik şeylər adlanan əsas dövrə yaradıldı. Və buna görə də seçimlə Qemu işə salıram -d exec,in_asm,out_asm, bu o deməkdir ki, hansı kod bloklarının icra olunduğunu, həmçinin yayım zamanı qonaq kodunun nə olduğunu, hansı host kodunun (bu halda bayt kodu) olduğunu yazmaq lazımdır. Başlayır, bir neçə tərcümə blokunu yerinə yetirir, RCU-nun indi başlayacağı barədə buraxdığım sazlama mesajını yazır və... çökür abort() funksiya daxilində free(). Funksiya ilə məşğul olaraq free() Ayrılmış yaddaşdan əvvəlki səkkiz baytda yerləşən yığın blokunun başlığında blok ölçüsü və ya buna bənzər bir şey əvəzinə zibil olduğunu öyrənə bildik.

Yığın dağıdılması - necə də yaraşıqlı... Belə bir vəziyyətdə faydalı bir vasitə var - (mümkünsə) eyni mənbələrdən, doğma binar toplayın və Valgrind altında işləyin. Bir müddət sonra binar hazır oldu. Mən onu eyni seçimlərlə işə salıram - o, hətta işə salınma zamanı, əslində icraya çatmazdan əvvəl çökür. Bu, əlbəttə ki, xoşagəlməzdir - görünür, mənbələr tam olaraq eyni deyildi, bu təəccüblü deyil, çünki konfiqurasiya bir az fərqli variantları kəşf etdi, amma məndə Valgrind var - əvvəlcə bu səhvi düzəldəcəm, sonra şanslı olsam. , orijinalı görünəcək. Valgrind altında eyni şeyi işlədirəm... Y-y-y, y-y-y, uh-uh, başladı, normal olaraq başlanğıcdan keçdi və yanlış yaddaşa giriş haqqında bir xəbərdarlıq etmədən orijinal səhvə keçdi, düşmələr haqqında danışmırıq. Həyat, necə deyərlər, məni buna hazırlamadı - çökən proqram Walgrind altında işə salındıqda çökməyi dayandırır. Bunun nə olduğu sirr olaraq qalır. Mənim fərziyyəm budur ki, işə salma zamanı qəzadan sonra cari təlimatın yaxınlığında bir dəfə gdb iş göstərdi. memsetya istifadə edərək etibarlı göstərici ilə -a mmx, və ya xmm qeyd edir, onda bəlkə də bu, bir növ uyğunlaşma xətası idi, baxmayaraq ki, hələ də inanmaq çətindir.

Tamam, Valgrind burada kömək etmir. Və burada ən iyrənc şey başladı - hər şey hətta başlayır, amma milyonlarla təlimat əvvəl baş verə biləcək bir hadisə səbəbindən tamamilə naməlum səbəblərdən çökür. Uzun müddət necə yanaşmaq lazım olduğu belə aydın deyildi. Nəhayət, mən hələ də oturub debug etməli oldum. Başlığın yenidən yazıldığını çap etmək onun rəqəmə deyil, bir növ ikili məlumatlara bənzədiyini göstərdi. Və budur, bu ikili sətir BIOS faylında tapıldı - yəni indi ağlabatan əminliklə bunun bufer daşması olduğunu söyləmək mümkün idi və hətta bu buferə yazıldığı da aydındır. Yaxşı, onda belə bir şey - Emscripten-də, xoşbəxtlikdən, ünvan sahəsinin təsadüfiləşdirilməsi yoxdur, orada da heç bir boşluq yoxdur, ona görə də kodun ortasında bir yerə yaza bilərsiniz ki, son işə salındıqdan sonra göstərici ilə məlumat çıxarın, məlumatlara baxın, göstəriciyə baxın və dəyişməyibsə, düşünmək üçün qida əldə edin. Düzdür, hər hansı bir dəyişiklikdən sonra əlaqə yaratmaq bir neçə dəqiqə çəkir, amma nə edə bilərsiniz? Nəticədə, BIOS-u müvəqqəti buferdən qonaq yaddaşına köçürən xüsusi bir xətt tapıldı - və həqiqətən də buferdə kifayət qədər yer yox idi. Həmin qəribə bufer ünvanının mənbəyini tapmaq bir funksiya ilə nəticələndi qemu_anon_ram_alloc faylda oslib-posix.c - məntiq belə idi: bəzən ünvanı 2 MB ölçüsündə böyük bir səhifəyə uyğunlaşdırmaq faydalı ola bilər, bunun üçün soruşacağıq mmap əvvəlcə bir az daha, sonra isə artıqlığı yardımla qaytaracağıq munmap. Və belə bir uyğunlaşma tələb olunmursa, onda 2 MB əvəzinə nəticəni göstərəcəyik getpagesize() - mmap o, yenə də düzlənmiş ünvan verəcək... Beləliklə, Emscripten-də mmap sadəcə zəng edir malloc, lakin əlbəttə ki, səhifədə uyğunlaşmır. Ümumiyyətlə, bir neçə ay məni əsəbiləşdirən bir səhv, dəyişikliklə düzəldildi dvux xətlər.

Çağırış funksiyalarının xüsusiyyətləri

İndi prosessor bir şeyi hesablayır, Qemu qəzaya uğramır, amma ekran açılmır və prosessor çıxışa görə tez döngələrə keçir. -d exec,in_asm,out_asm. Bir fərziyyə ortaya çıxdı: taymer kəsmələri (və ya ümumiyyətlə, bütün fasilələr) gəlmir. Həqiqətən, nədənsə işləyən yerli məclisdən kəsmələri çıxarsanız, bənzər bir şəkil alırsınız. Ancaq bu, heç də cavab deyildi: yuxarıdakı variantla buraxılan izlərin müqayisəsi, icra trayektoriyalarının çox erkən ayrıldığını göstərdi. Burada qeyd etmək lazımdır ki, başlatma cihazından istifadə edərək yazılanların müqayisəsi emrun yerli montajın çıxışı ilə çıxışın düzəldilməsi tamamilə mexaniki proses deyil. Brauzerdə işləyən proqramın necə bağlandığını dəqiq bilmirəm emrun, lakin çıxışdakı bəzi xətlər yenidən qurulur, buna görə fərqdəki fərq hələ traektoriyaların ayrıldığını güman etmək üçün bir səbəb deyil. Ümumiyyətlə, məlum oldu ki, təlimata uyğun olaraq ljmpl müxtəlif ünvanlara keçid var və yaradılan bayt kodu əsaslı şəkildə fərqlidir: birində köməkçi funksiyanı çağırmaq üçün göstəriş var, digərində yoxdur. Təlimatları araşdırdıqdan və bu təlimatları tərcümə edən kodu öyrəndikdən sonra məlum oldu ki, ilk növbədə reyestrdə ondan dərhal əvvəl cr0 Prosessoru qorunan rejimə keçirən, ikincisi, js versiyasının heç vaxt qorunan rejimə keçmədiyi bir qeyd edildi - həm də köməkçidən istifadə etməklə. Ancaq fakt budur ki, Emscripten-in başqa bir xüsusiyyəti, təlimatların icrası kimi koda dözmək istəməməsidir. call TCI-də hər hansı bir funksiya göstəricisi növü ilə nəticələnir long long f(int arg0, .. int arg9) - funksiyalar düzgün sayda arqumentlə çağırılmalıdır. Bu qayda pozularsa, sazlama parametrlərindən asılı olaraq, proqram ya çökəcək (bu yaxşıdır) və ya ümumiyyətlə səhv funksiyanı çağıracaq (bu, diskussiya üçün kədərli olacaq). Üçüncü bir seçim də var - arqumentləri əlavə edən / çıxaran sarğıların yaradılmasını aktivləşdirin, lakin ümumilikdə bu sarğılar çox yer tutur, baxmayaraq ki, əslində mənə yüzdən bir qədər çox sarğı lazımdır. Bu tək çox üzücüdür, lakin daha ciddi bir problem olduğu ortaya çıxdı: sarğı funksiyalarının yaradılan kodunda arqumentlər çevrildi və çevrildi, lakin bəzən yaradılan arqumentləri olan funksiya çağırılmırdı - yaxşı, eynilə libffi tətbiqim. Yəni, bəzi köməkçilər sadəcə olaraq edam edilməyib.

Xoşbəxtlikdən, Qemu-da başlıq faylı şəklində maşın tərəfindən oxuna bilən köməkçi siyahıları var.

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

Onlar olduqca gülməli istifadə olunur: birincisi, makrolar ən qəribə şəkildə yenidən müəyyən edilir DEF_HELPER_n, sonra açılır helper.h. Makronun strukturun başlatıcısına və vergülünə genişləndiyi və sonra bir massiv müəyyən edildiyi və elementlərin yerinə - #include <helper.h> Nəticədə, nəhayət, kitabxananı iş yerində sınamaq şansım oldu pyparsing, və lazım olan funksiyalar üçün tam olaraq həmin sarğıları yaradan skript yazılmışdır.

Və beləliklə, bundan sonra prosessor işləyirdi. Görünür, memtest86+ yerli məclisdə işləyə bilsə də, ekran heç vaxt işə salınmayıb. Burada Qemu blokunun I/O kodunun koroutinlərdə yazıldığını aydınlaşdırmaq lazımdır. Emscripten-in özünün çox çətin tətbiqi var, lakin onu hələ də Qemu kodunda dəstəkləmək lazım idi və siz indi prosessoru sazlaya bilərsiniz: Qemu seçimləri dəstəkləyir -kernel, -initrd, -append, onunla Linux və ya, məsələn, memtest86+, blok cihazlarından ümumiyyətlə istifadə etmədən yükləyə bilərsiniz. Ancaq problem budur: yerli məclisdə bir seçim ilə konsolda Linux nüvəsinin çıxışını görmək olar -nographic, və brauzerdən işə salındığı yerdən terminala çıxış yoxdur emrun, gəlmədi. Yəni aydın deyil: prosessor işləmir və ya qrafik çıxışı işləmir. Sonra bir az gözləmək ağlıma gəldi. Məlum oldu ki, "prosessor yatmır, sadəcə yavaş-yavaş yanıb-sönür" və təxminən beş dəqiqədən sonra nüvə konsola bir dəstə mesaj atdı və asmağa davam etdi. Aydın oldu ki, prosessor, ümumiyyətlə, işləyir və SDL2 ilə işləmək üçün kodu qazmaq lazımdır. Təəssüf ki, bu kitabxanadan necə istifadə edəcəyimi bilmirəm, ona görə də bəzi yerlərdə təsadüfi hərəkət etməli oldum. Bir anda, paralel0 xətti ekranda mavi fonda yanıb-söndü və bu, bəzi fikirləri təklif etdi. Sonda məlum oldu ki, problem Qemu-nun bir fiziki pəncərədə bir neçə virtual pəncərə açmasıdır, onlar arasında Ctrl-Alt-n istifadə edərək keçid edə bilərsiniz: o, yerli quruluşda işləyir, Emscripten-də deyil. Seçimlərdən istifadə edərək lazımsız pəncərələrdən qurtulduqdan sonra -monitor none -parallel none -serial none və hər bir çərçivədə bütün ekranı zorla yenidən çəkmək üçün təlimatlar, hər şey birdən işə düşdü.

Korutinlər

Beləliklə, brauzerdə emulyasiya işləyir, lakin siz orada maraqlı bir disket işlədə bilməzsiniz, çünki I/O bloku yoxdur - koroutinlər üçün dəstək tətbiq etməlisiniz. Qemu-nun artıq bir neçə korutin arxa tərəfi var, lakin JavaScript və Emscripten kod generatorunun təbiətinə görə siz sadəcə yığınlarla hoqqalara başlaya bilməzsiniz. Görünür ki, "hər şey getdi, gips sökülür", amma Emscripten tərtibatçıları artıq hər şeyə diqqət yetirdilər. Bu, olduqca gülməli şəkildə həyata keçirilir: gəlin bu kimi bir funksiya çağırışını şübhəli adlandıraq emscripten_sleep və Asyncify mexanizmindən istifadə edən bir neçə başqaları, həmçinin göstərici zəngləri və əvvəlki iki haldan birinin yığının aşağısında baş verə biləcəyi istənilən funksiyaya zənglər. İndi isə hər bir şübhəli zəngdən əvvəl biz asinxron kontekst seçəcəyik və zəngdən dərhal sonra asinxron çağırışın olub-olmadığını yoxlayacağıq və əgər varsa, bütün yerli dəyişənləri bu asinxron kontekstdə saxlayacağıq, hansı funksiyanı göstərəcəyik. icraya davam etməmiz lazım olan vaxta nəzarəti ötürmək və cari funksiyadan çıxmaq. Təsiri öyrənmək üçün imkanlar buradadır israfçılıq — asinxron zəngdən qayıtdıqdan sonra kodun icrasını davam etdirmək ehtiyacları üçün kompilyator şübhəli çağırışdan sonra başlayan funksiyanın “stub”larını yaradır — bu kimi: əgər n şübhəli zəng varsa, onda funksiya n/2 yerdə genişləndiriləcək. dəfə — bu hələ də deyilsə, unutmayın ki, hər bir potensial asinxron zəngdən sonra orijinal funksiyaya bəzi yerli dəyişənlərin saxlanmasını əlavə etməlisiniz. Sonradan, hətta Python-da sadə bir skript yazmalı oldum, bu, guya "asinxroniyanın öz-özünə keçməsinə imkan verməyən" xüsusi həddən artıq istifadə edilmiş funksiyalar toplusuna əsaslanaraq (yəni, yığının təşviqi və bayaq təsvir etdiyim hər şey deyil). onlarda işləmək), bu funksiyaların asinxron hesab edilməməsi üçün kompilyator tərəfindən funksiyaların nəzərə alınmamalı olduğu göstəricilər vasitəsilə edilən çağırışları göstərir. Və sonra 60 MB-dan aşağı olan JS faylları açıq-aydın həddən artıq çoxdur - deyək ki, ən azı 30. Baxmayaraq ki, bir dəfə montaj skripti qururdum və təsadüfən linker seçimlərini atdım, bunlar arasında -O3. Yaradılan kodu işlədirəm və Chromium yaddaşı yeyir və çökür. Sonra təsadüfən onun yükləməyə çalışdığına baxdım... Yaxşı, nə deyim, məndən 500+ MB Javascript-i düşünərək öyrənib optimallaşdırmağı tələb etsəydilər, mən də donub qalmışdım.

Təəssüf ki, Asyncify dəstək kitabxana kodundakı yoxlamalar tamamilə uyğun deyildi longjmp-s virtual prosessor kodunda istifadə olunur, lakin bu yoxlamaları söndürən və kontekstləri zorla bərpa edən kiçik bir yamadan sonra hər şey qaydasındadır, kod işlədi. Və sonra qəribə bir şey başladı: bəzən sinxronizasiya kodunda yoxlamalar işə salındı ​​- icra məntiqinə görə, bloklanmalı olduqda kodu pozan eynilər - kimsə artıq ələ keçirilmiş mutex-i tutmağa çalışdı. Xoşbəxtlikdən, seriallaşdırılmış kodda bunun məntiqi problem olmadığı ortaya çıxdı - mən sadəcə Emscripten tərəfindən təmin edilən standart əsas döngə funksionallığından istifadə edirdim, lakin bəzən asinxron zəng yığını tamamilə açır və o anda uğursuz olur. setTimeout əsas döngədən - beləliklə, kod əvvəlki iterasiyadan çıxmadan əsas döngə iterasiyasına daxil oldu. Sonsuz bir döngədə yenidən yazdı və emscripten_sleep, və mutekslərlə bağlı problemlər dayandı. Kod daha da məntiqli oldu - əslində mənim növbəti animasiya çərçivəsini hazırlayan kodum yoxdur - prosessor sadəcə nəyisə hesablayır və ekran vaxtaşırı yenilənir. Bununla belə, problemlər bununla da bitmədi: bəzən Qemu icrası heç bir istisna və ya səhv olmadan sadəcə səssizcə dayandırılırdı. O anda ondan imtina etdim, amma irəliyə baxaraq deyəcəm ki, problem bu idi: korutin kodu, əslində, istifadə etmir. setTimeout (və ya ən azı düşündüyünüz qədər tez-tez deyil): funksiya emscripten_yield sadəcə olaraq asinxron zəng bayrağını təyin edir. Bütün məsələ ondadır emscripten_coroutine_next asinxron funksiya deyil: daxili olaraq bayrağı yoxlayır, onu sıfırlayır və nəzarəti lazım olan yerə ötürür. Yəni yığının təbliğatı orada bitir. Problem onda idi ki, mövcud coroutine backendindən vacib kod sətirini köçürmədiyim üçün koroutin hovuzu söndürüldükdə ortaya çıxan pulsuz istifadədən sonra funksiya qemu_in_coroutine həqiqəti geri qaytardı, əslində yalanı qaytarmalı idi. Bu, zəngə səbəb oldu emscripten_yield, yuxarıda yığında heç kim yox idi emscripten_coroutine_next, yığın yuxarıya doğru açıldı, amma yox setTimeout, artıq dediyim kimi, sərgilənmədi.

JavaScript kodu yaratmaq

Və burada, əslində, vəd edilmiş "qiyməni geri çevirmək" var. Həqiqətən yox. Əlbəttə ki, əgər biz brauzerdə Qemu, orada isə Node.js işlətsək, təbii ki, Qemu-da kod yaratdıqdan sonra tamamilə səhv JavaScript əldə edəcəyik. Ancaq yenə də bir növ tərs çevrilmə.

Əvvəlcə Qemunun necə işlədiyi haqqında bir az. Xahiş edirəm məni dərhal bağışlayın: mən peşəkar Qemu tərtibatçısı deyiləm və nəticələrim bəzi yerlərdə səhv ola bilər. Necə deyərlər, “şagirdin fikri müəllimin fikri, Peanonun aksiomatikası və sağlam düşüncəsi ilə üst-üstə düşməməlidir”. Qemu müəyyən sayda dəstəklənən qonaq arxitekturasına malikdir və hər biri üçün bənzər bir kataloq var target-i386. Qurarkən bir neçə qonaq arxitekturasına dəstək təyin edə bilərsiniz, lakin nəticə yalnız bir neçə binar olacaq. Qonaq arxitekturasını dəstəkləmək üçün kod, öz növbəsində, TCG (Tiny Code Generator) artıq host arxitekturasının maşın koduna çevrildiyi bəzi daxili Qemu əməliyyatlarını yaradır. Tcg qovluğunda yerləşən readme faylında qeyd edildiyi kimi, bu, əvvəlcə JIT üçün uyğunlaşdırılmış adi C kompilyatorunun bir hissəsi idi. Buna görə də, məsələn, bu sənəd baxımından hədəf arxitektura artıq qonaq arxitekturası deyil, ev sahibi arxitekturasıdır. Bir anda başqa bir komponent ortaya çıxdı - müəyyən bir host arxitekturası üçün kod generatoru olmadıqda kodu (demək olar ki, eyni daxili əməliyyatları) yerinə yetirməli olan Tiny Code Interpreter (TCI). Əslində, onun sənədlərində deyildiyi kimi, bu tərcüməçi yalnız sürət baxımından deyil, həm də keyfiyyətcə JIT kod generatoru kimi yaxşı işləməyə bilər. Baxmayaraq ki, onun təsvirinin tam uyğun olduğuna əmin deyiləm.

Əvvəlcə tam hüquqlu bir TCG backend yaratmağa çalışdım, lakin tez mənbə kodunda və bayt kodu təlimatlarının tam aydın olmayan təsvirində çaşqın oldum, buna görə də TCI tərcüməçisini bağlamaq qərarına gəldim. Bu, bir sıra üstünlüklər verdi:

  • kod generatorunu həyata keçirərkən, təlimatların təsvirinə deyil, tərcüməçi koduna baxa bilərsiniz
  • rast gələn hər tərcümə bloku üçün deyil, məsələn, yalnız yüzüncü icradan sonra funksiyalar yarada bilərsiniz.
  • yaradılan kod dəyişərsə (və bu, söz yamağı olan adları olan funksiyalara əsasən mümkün görünür), mən yaradılan JS kodunu etibarsız etməliyəm, lakin heç olmasa onu bərpa etmək üçün bir şeyim olacaq.

Üçüncü nöqtəyə gəldikdə, kodun ilk dəfə icrasından sonra yamağın mümkün olduğuna əmin deyiləm, lakin ilk iki nöqtə kifayətdir.

Əvvəlcə kod orijinal bayt kodu təlimatının ünvanında böyük bir keçid şəklində yaradıldı, lakin sonra Emscripten, yaradılan JS-nin optimallaşdırılması və təkrar dövriyyə haqqında məqaləni xatırlayaraq, daha çox insan kodu yaratmağa qərar verdim, xüsusən də empirik olaraq Məlum oldu ki, tərcümə blokuna yeganə giriş nöqtəsi onun Başlanmasıdır. Bir müddət sonra biz ifs ilə kod yaradan kod generatorumuz oldu (döngülər olmadan). Ancaq uğursuzluq, təlimatların yanlış uzunluqda olduğu mesajını verərək qəzaya uğradı. Üstəlik, bu rekursiya səviyyəsində son təlimat idi brcond. Yaxşı, mən rekursiv çağırışdan əvvəl və sonra bu təlimatın yaradılmasına eyni çek əlavə edəcəyəm və... onlardan heç biri yerinə yetirilmədi, lakin təsdiq keçidindən sonra yenə də uğursuz oldu. Sonda yaradılan kodu öyrəndikdən sonra başa düşdüm ki, keçiddən sonra cari təlimatın göstəricisi stekdən yenidən yüklənir və yəqin ki, yaradılan JavaScript kodunun üzərinə yazılır. Və belə çıxdı. Buferin bir meqabaytdan ona qədər artırılması heç nəyə gətirib çıxarmadı və kod generatorunun dairələrdə işlədiyi aydın oldu. Biz yoxlamalı idik ki, biz indiki vərəmin hüdudlarından kənara çıxmamışıq və əgər çıxırıqsa, onda növbəti vərəmin ünvanını mənfi işarə ilə verin ki, icranı davam etdirək. Bundan əlavə, bu, "bu bayt kodunun bir hissəsi dəyişərsə, hansı yaradılan funksiyalar etibarsız olmalıdır?" problemini həll edir. — yalnız bu tərcümə blokuna uyğun gələn funksiya ləğv edilməlidir. Yeri gəlmişkən, Chromium-da hər şeyi sazlasam da (Firefox-dan istifadə etdiyim üçün və təcrübələr üçün ayrıca brauzerdən istifadə etmək mənim üçün daha asandır), Firefox mənə asm.js standartı ilə uyğunsuzluqları düzəltməyə kömək etdi, bundan sonra kod daha sürətli işləməyə başladı. Xrom.

Yaradılmış kodun nümunəsi

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

Nəticə

Beləliklə, iş hələ də tamamlanmayıb, amma bu uzunmüddətli tikintini gizli şəkildə mükəmməlliyə çatdırmaqdan yoruldum. Buna görə də, indiyə qədər əlimdə olanı dərc etmək qərarına gəldim. Kod yerlərdə bir az qorxuludur, çünki bu bir təcrübədir və nə etmək lazım olduğu əvvəlcədən aydın deyil. Yəqin ki, o zaman Qemu-nun daha müasir versiyasının üstünə normal atom öhdəlikləri verməyə dəyər. Bu vaxt Gita-da bir blog formatında bir mövzu var: ən azı bir şəkildə keçən hər bir "səviyyə" üçün rus dilində ətraflı şərh əlavə edildi. Əslində, bu məqalə böyük ölçüdə nəticənin təkrarıdır git log.

Hamısını sınaya bilərsiniz burada (trafikdən ehtiyatlı olun).

Artıq nə işləyir:

  • x86 virtual prosessor işləyir
  • Maşın kodundan JavaScript-ə qədər JIT kod generatorunun işləyən prototipi mövcuddur
  • Digər 32-bit qonaq arxitekturasının yığılması üçün şablon var: hazırda yükləmə mərhələsində brauzerdə donan MIPS arxitekturasına görə Linux-a heyran ola bilərsiniz.

Başqa nə edə bilərsən

  • Emulyasiyanı sürətləndirin. Hətta JIT rejimində o, Virtual x86-dan daha yavaş işləyir (lakin potensial olaraq çoxlu təqlid edilmiş aparat və arxitekturaya malik bütöv Qemu var)
  • Normal bir interfeys yaratmaq üçün - düzünü desəm, mən yaxşı bir veb tərtibatçı deyiləm, buna görə də indi standart Emscripten qabığını bacardığım qədər yenidən düzəltdim.
  • Daha mürəkkəb Qemu funksiyalarını işə salmağa çalışın - şəbəkə, VM miqrasiyası və s.
  • UPS: Qemu və digər layihələrin əvvəlki daşıyıcılarının etdiyi kimi, bir neçə inkişafınızı və səhv hesabatlarınızı Emscripten-ə təqdim etməlisiniz. Tapşırığımın bir hissəsi kimi Emscripten-ə verdiyi töhfələrdən dolayı istifadə edə bildikləri üçün onlara təşəkkür edirəm.

Mənbə: www.habr.com

Добавить комментарий