JIT destekli Qemu.js: Kıymayı hâlâ geriye çevirebilirsiniz

Birkaç yıl önce Fabrice Bellard jslinux tarafından yazılmıştır JavaScript ile yazılmış bir PC emülatörüdür. Bundan sonra en azından daha fazlası vardı Sanal x86. Ancak bildiğim kadarıyla hepsi tercümandı; aynı Fabrice Bellard tarafından çok daha önce yazılan ve muhtemelen kendine saygısı olan herhangi bir modern emülatör olan Qemu, konuk kodunun ana sistem koduna JIT derlemesini kullanıyor. Bana öyle geldi ki, tarayıcıların çözdüğü görevin tersini uygulamanın zamanı gelmişti: Qemu'yu taşımanın en mantıklı göründüğü makine kodunun JIT ile JavaScript'e derlenmesi. Görünüşe göre Qemu'da neden daha basit ve kullanıcı dostu emülatörler var - örneğin aynı VirtualBox - kurulu ve çalışıyor. Ancak Qemu'nun birçok ilginç özelliği var

  • açık kaynak
  • çekirdek sürücüsü olmadan çalışabilme yeteneği
  • tercüman modunda çalışabilme yeteneği
  • çok sayıda hem ana bilgisayar hem de konuk mimarisi desteği

Üçüncü noktaya gelince, artık aslında TCI modunda yorumlananın konuk makine talimatlarının kendisi değil, onlardan elde edilen bayt kodu olduğunu, ancak bunun özü değiştirmediğini açıklayabilirim - oluşturmak ve çalıştırmak için Yeni bir mimaride Qemu, eğer şanslıysanız AC derleyicisi yeterlidir - kod oluşturucu yazmak ertelenebilir.

Ve şimdi, boş zamanlarımda Qemu kaynak koduyla iki yıl boyunca yavaş yavaş uğraştıktan sonra, örneğin Kolibri OS'yi zaten çalıştırabileceğiniz çalışan bir prototip ortaya çıktı.

Emscripten nedir?

Günümüzde pek çok derleyici ortaya çıktı ve bunların sonucunda JavaScript ortaya çıktı. Type Script gibi bazılarının başlangıçta web için yazmanın en iyi yolu olması amaçlanmıştı. Aynı zamanda Emscripten, mevcut C veya C++ kodunu alıp tarayıcı tarafından okunabilir bir biçimde derlemenin bir yoludur. Açık Bu sayfa Tanınmış programların birçok bağlantı noktasını topladık: buradaÖrneğin, PyPy'ye bakabilirsiniz - bu arada, zaten JIT'e sahip olduklarını iddia ediyorlar. Aslında her program bir tarayıcıda basitçe derlenip çalıştırılamaz; özelliklerAncak aynı sayfadaki yazıtta "Emscripten hemen hemen her şeyi derlemek için kullanılabilir" dediği için buna katlanmak zorundasınız. taşınabilir Yani, standarda göre tanımlanmamış davranışa sahip olan ancak genellikle x86 üzerinde çalışan bir dizi işlem vardır - örneğin, bazı mimarilerde genellikle yasak olan değişkenlere hizalanmamış erişim. Genel olarak , Qemu platformlar arası bir programdır ve ben buna inanmak istedim ve halihazırda pek çok tanımsız davranış içermiyor - alın ve derleyin, ardından JIT ile biraz uğraşın - ve işiniz bitti! dava...

İlk deneme

Genel olarak konuşursak, Qemu'yu JavaScript'e taşıma fikrini ortaya atan ilk kişi ben değilim. ReactOS forumunda bunun Emscripten kullanılarak mümkün olup olmadığı sorulmuştu. Daha önce de Fabrice Bellard'ın bunu bizzat yaptığına dair söylentiler vardı ama biz jslinux'tan bahsediyorduk, bildiğim kadarıyla JS'de yeterli performansı manuel olarak elde etme girişimi olan ve sıfırdan yazılmış. Daha sonra Virtual x86 yazıldı - bunun için açık kaynaklar yayınlandı ve belirtildiği gibi emülasyonun daha fazla "gerçekliği", SeaBIOS'un ürün yazılımı olarak kullanılmasını mümkün kıldı. Ayrıca, Emscripten kullanarak Qemu'yu taşımaya yönelik en az bir girişimde bulunuldu - bunu yapmaya çalıştım soket çiftiama anladığım kadarıyla gelişme donmuştu.

Öyle görünüyor ki, işte kaynaklar, işte Emscripten - alın ve derleyin. Ancak Qemu'nun bağlı olduğu kütüphaneler ve bu kütüphanelerin bağlı olduğu kütüphaneler vb. de vardır ve bunlardan biri libfi, hangi glib'e bağlıdır. İnternette Emscripten için geniş kütüphane limanları koleksiyonunda bir tane olduğuna dair söylentiler vardı, ancak buna inanmak bir şekilde zordu: birincisi, yeni bir derleyici olması amaçlanmamıştı, ikincisi, çok düşük seviyeli bir derleyiciydi. Sadece alıp JS'ye derlemek için kütüphane. Ve bu sadece montaj eklemeleri meselesi değil; muhtemelen, eğer onu çevirirseniz, bazı çağırma kuralları için yığında gerekli argümanları oluşturabilir ve onlar olmadan işlevi çağırabilirsiniz. Ancak Emscripten zor bir iştir: oluşturulan kodun tarayıcı JS motor optimizasyon aracına tanıdık gelmesini sağlamak için bazı hileler kullanılır. Özellikle, yeniden döngü olarak adlandırılan, alınan LLVM IR'yi bazı soyut geçiş talimatlarıyla kullanan bir kod oluşturucu, makul if'leri, döngüleri vb. yeniden oluşturmaya çalışır. Peki argümanlar fonksiyona nasıl aktarılıyor? Doğal olarak JS işlevlerine argüman olarak, yani mümkünse yığın aracılığıyla değil.

Başlangıçta, JS ile libffi'nin yerine yenisini yazma ve standart testleri çalıştırma fikri vardı, ancak sonunda başlık dosyalarımı mevcut kodla çalışacak şekilde nasıl yapacağım konusunda kafam karıştı - ne yapabilirim, dedikleri gibi, “Görevler bu kadar karmaşık mı?” Biz bu kadar aptal mıyız?” Tabiri caizse libffi'yi başka bir mimariye taşımak zorunda kaldım - neyse ki, Emscripten hem satır içi derleme için makrolara (Javascript'te, evet - yani mimari ne olursa olsun, yani birleştirici) ve anında oluşturulan kodu çalıştırma yeteneğine sahip. Genel olarak, platforma bağlı libffi parçalarıyla bir süre uğraştıktan sonra derlenebilir bazı kodlar aldım ve karşılaştığım ilk testte onu çalıştırdım. Testin başarılı olması beni şaşırttı. Deham karşısında hayrete düştüm - şaka değil, ilk lansmandan itibaren işe yaradı - ben, hala gözlerime inanamayarak, bir sonraki nereye kazacağımı değerlendirmek için ortaya çıkan koda tekrar bakmaya gittim. Burada ikinci kez delirdim - fonksiyonumun yaptığı tek şey ffi_call - bu başarılı bir aramayı bildirdi. Hiçbir çağrının kendisi yoktu. Bu yüzden, herhangi bir Olimpiyat öğrencisi için açık olan testteki bir hatayı düzelten ilk çekme isteğimi gönderdim - gerçek sayılar şu şekilde karşılaştırılmamalıdır: a == b ve hatta nasıl a - b < EPS - ayrıca modülü de hatırlamanız gerekir, aksi takdirde 0, 1/3'e çok eşit olacaktır... Genel olarak, en basit testleri geçen ve glib'in kullanıldığı belirli bir libffi bağlantı noktası buldum. derlenmiş - Gerekli olacağına karar verdim, daha sonra ekleyeceğim. İleriye baktığımda, derleyicinin son kodda libffi işlevini bile içermediğini söyleyeceğim.

Ancak, daha önce de söylediğim gibi, bazı sınırlamalar var ve çeşitli tanımlanmamış davranışların ücretsiz kullanımı arasında daha hoş olmayan bir özellik gizlendi - JavaScript, tasarım gereği, paylaşılan bellekle çoklu iş parçacığını desteklemiyor. Prensipte buna genellikle iyi bir fikir bile denilebilir, ancak mimarisi C iş parçacıklarına bağlı olan kodu taşımak için değil. Genel olarak konuşursak, Firefox paylaşılan çalışanları destekleme konusunda deneyler yapıyor ve Emscripten'in onlar için bir pthread uygulaması var, ancak ben buna bağlı kalmak istemedim. Çoklu iş parçacığını yavaş yavaş Qemu kodundan çıkarmak zorunda kaldım - yani iş parçacıklarının nerede çalıştığını bulmak, bu iş parçacığında çalışan döngünün gövdesini ayrı bir işleve taşımak ve bu tür işlevleri ana döngüden birer birer çağırmak zorunda kaldım.

İkinci deneme

Bir noktada sorunun hâlâ devam ettiği ve kodun etrafına gelişigüzel koltuk değneği sokmanın hiçbir işe yaramayacağı anlaşıldı. Sonuç: Koltuk değneği ekleme sürecini bir şekilde sistematikleştirmemiz gerekiyor. Bu nedenle o zamanlar taze olan 2.4.1 sürümü alındı ​​​​(2.5.0 değil, çünkü yeni sürümde henüz yakalanamayan hatalar olacağını asla bilemezsiniz ve benim de kendime yetiyorum) hatalar) ve yaptığım ilk şey onu güvenli bir şekilde yeniden yazmaktı thread-posix.c. Yani, bir o kadar da güvenli: Birisi engellemeye yol açan bir işlemi gerçekleştirmeye çalıştığında, işlev hemen çağrıldı. abort() - elbette bu, tüm sorunları aynı anda çözmedi, ancak en azından tutarsız verileri sessizce almaktan bir şekilde daha hoştu.

Genel olarak Emscripten seçenekleri kodun JS'ye taşınmasında çok faydalıdır. -s ASSERTIONS=1 -s SAFE_HEAP=1 - hizalanmamış bir adrese yapılan çağrılar gibi bazı tanımlanmamış davranış türlerini yakalarlar (bu, aşağıdaki gibi yazılan dizilerin koduyla hiç tutarlı değildir) HEAP32[addr >> 2] = 1) veya yanlış sayıda argümanla bir işlevi çağırmak.

Bu arada hizalama hataları ayrı bir konudur. Daha önce de söylediğim gibi, Qemu'nun kod üretimi TCI (küçük kod yorumlayıcısı) için "yozlaşmış" bir yorumlayıcı arka ucu vardır ve Qemu'yu yeni bir mimari üzerinde oluşturup çalıştırmak için eğer şanslıysanız bir C derleyicisi yeterlidir. "eğer şanslıysan". Şanssızdım ve TCI'ın bayt kodunu ayrıştırırken hizalanmamış erişim kullandığı ortaya çıktı. Yani, her türlü ARM ve zorunlu olarak seviyelendirilmiş erişime sahip diğer mimarilerde Qemu, yerel kod üreten normal bir TCG arka ucuna sahip oldukları için derler, ancak TCI'ın bunlar üzerinde çalışıp çalışmayacağı başka bir sorudur. Ancak ortaya çıktığı üzere, TCI belgeleri açıkça benzer bir şeye işaret ediyordu. Sonuç olarak, Qemu'nun başka bir bölümünde keşfedilen, hizalanmamış okumaya yönelik işlev çağrıları koda eklendi.

Yığın imhası

Sonuç olarak, TCI'ye uyumsuz erişim düzeltildi, işlemci, RCU ve diğer bazı küçük şeyler olarak adlandırılan bir ana döngü oluşturuldu. Ve böylece Qemu'yu seçenekle başlatıyorum -d exec,in_asm,out_asmBu, hangi kod bloklarının yürütüldüğünü söylemeniz gerektiği ve ayrıca yayın sırasında konuk kodunun ne olduğunu, ana bilgisayar kodunun (bu durumda bayt kodu) ne olduğunu yazmanız gerektiği anlamına gelir. Başlıyor, birkaç çeviri bloğunu yürütüyor, bıraktığım hata ayıklama mesajını RCU'nun şimdi başlatacağını yazıyor ve... çöküyor abort() bir fonksiyonun içinde free(). Fonksiyonla uğraşarak free() Tahsis edilen hafızadan önceki sekiz baytta yer alan yığın bloğunun başlığında, blok boyutu veya benzeri bir şey yerine çöp olduğunu bulmayı başardık.

Yığın imhası - ne kadar sevimli... Böyle bir durumda, (mümkünse) aynı kaynaklardan gelen yararlı bir çözüm vardır, yerel bir ikili dosya oluşturun ve onu Valgrind altında çalıştırın. Bir süre sonra ikili dosya hazırdı. Aynı seçeneklerle başlatıyorum - başlatma sırasında bile, yürütmeye ulaşmadan önce çöküyor. Elbette hoş olmayan bir durum - görünüşe göre kaynaklar tamamen aynı değildi, bu şaşırtıcı değil, çünkü yapılandırma biraz farklı seçenekleri araştırdı, ancak Valgrind'im var - önce bu hatayı düzelteceğim ve sonra eğer şanslıysam , orijinal olan görünecektir. Aynı şeyi Valgrind altında da çalıştırıyorum... Y-y-y, y-y-y, uh-uh, başladı, normal şekilde başlatma sürecinden geçti ve düşmeler bir yana, hatalı hafıza erişimine ilişkin tek bir uyarı olmadan orijinal hatayı geçti. Hayat, dedikleri gibi, beni buna hazırlamadı - çöken bir program, Walgrind altında başlatıldığında çökmeyi bırakıyor. Ne olduğu bir sır. Benim hipotezim, başlatma sırasındaki bir çökme sonrasında mevcut talimatın yakınında gdb'nin iş gösterdiği yönündedir. memset-a ikisini de kullanarak geçerli bir işaretçiyle mmx, ister xmm kayıtlar varsa, o zaman belki de bir tür hizalama hatasıydı, yine de buna inanmak zor.

Tamam, Valgrind'in burada faydası olmayacak gibi görünüyor. Ve burada en iğrenç şey başladı - her şey başlıyor gibi görünüyor, ancak milyonlarca talimat önce gerçekleşmiş olabilecek bir olay nedeniyle kesinlikle bilinmeyen nedenlerle çöküyor. Uzun süre nasıl yaklaşılacağı bile belli değildi. Sonunda hala oturup hata ayıklamak zorunda kaldım. Başlığın yeniden yazıldığı şeyin yazdırılması, bunun bir sayıya değil, bir tür ikili veriye benzediğini gösterdi. Ve işte, bu ikili dize BIOS dosyasında bulundu - yani artık bunun bir arabellek taşması olduğunu makul bir güvenle söylemek mümkündü ve hatta bu arabelleğe yazıldığı bile açık. O zaman şöyle bir şey - Emscripten'de, neyse ki, adres alanında rastgele bir seçim yok, içinde de delik yok, bu nedenle son başlatmadan itibaren işaretçiyle veri çıktısı almak için kodun ortasında bir yere yazabilirsiniz. verilere bakın, işaretçiye bakın ve eğer değişmediyse, üzerinde düşünmeye değer şeyler bulun. Doğru, herhangi bir değişiklikten sonra bağlantı kurmak birkaç dakika alır, ama ne yapabilirsiniz? Sonuç olarak, BIOS'u geçici arabellekten konuk belleğine kopyalayan belirli bir satır bulundu ve aslında arabellekte yeterli alan yoktu. Bu garip arabellek adresinin kaynağını bulmak bir işlevle sonuçlandı qemu_anon_ram_alloc dosyada oslib-posix.c - mantık şuydu: bazen adresi 2 MB boyutunda devasa bir sayfaya hizalamak yararlı olabilir, bunun için soracağız mmap önce biraz daha, sonra fazlalığı yardımla iade edeceğiz munmap. Ve eğer böyle bir hizalama gerekli değilse, 2 MB yerine sonucu göstereceğiz getpagesize() - mmap yine de hizalanmış bir adres verecektir... Yani Emscripten'de mmap sadece aramalar malloc, ancak elbette sayfaya hizalanmıyor. Genel olarak, birkaç aydır beni hayal kırıklığına uğratan bir hata, iki çizgiler.

Çağrı fonksiyonlarının özellikleri

Ve şimdi işlemci bir şeyler sayıyor, Qemu çökmüyor, ancak ekran açılmıyor ve işlemci çıktıya bakılırsa hızla döngülere giriyor -d exec,in_asm,out_asm. Bir hipotez ortaya çıktı: zamanlayıcı kesintileri (veya genel olarak tüm kesintiler) gelmiyor. Ve gerçekten de, bir nedenden dolayı işe yarayan yerel meclisteki kesintileri sökerseniz, benzer bir tablo elde edersiniz. Ancak yanıt kesinlikle bu değildi: Yukarıdaki seçenekle oluşturulan izlerin karşılaştırılması, yürütme yörüngelerinin çok erken farklılaştığını gösterdi. Burada başlatıcı kullanılarak kaydedilenlerin karşılaştırılması söylenmelidir. emrun çıktının yerel derlemenin çıktısıyla hata ayıklaması tamamen mekanik bir süreç değildir. Tarayıcıda çalışan bir programın tarayıcıya nasıl bağlandığını tam olarak bilmiyorum emrunancak çıktıdaki bazı satırların yeniden düzenlendiği ortaya çıkıyor, dolayısıyla farktaki fark henüz yörüngelerin farklılaştığını varsaymak için bir neden değil. Genel olarak, talimatlara göre anlaşıldı ljmpl farklı adreslere geçiş vardır ve oluşturulan bayt kodu temelde farklıdır: biri yardımcı işlevi çağırmak için bir talimat içerir, diğeri içermez. Talimatları Google'da araştırdıktan ve bu talimatları tercüme eden kodu inceledikten sonra, öncelikle kayıt defterinde hemen önce olduğu anlaşıldı. cr0 işlemciyi korumalı moda geçiren bir kayıt - yine bir yardımcı kullanılarak - yapıldı ve ikinci olarak, js sürümünün hiçbir zaman korumalı moda geçmediği. Ancak gerçek şu ki Emscripten'in bir başka özelliği de talimatların uygulanması gibi kodlara tolerans gösterme konusundaki isteksizliğidir. call TCI'da herhangi bir işlev işaretçisinin türle sonuçlandığı long long f(int arg0, .. int arg9) - işlevler doğru sayıda bağımsız değişkenle çağrılmalıdır. Bu kural ihlal edilirse, hata ayıklama ayarlarına bağlı olarak, program ya çökecektir (ki bu iyidir) ya da yanlış işlevi çağıracaktır (ki bu hata ayıklamak üzücü olacaktır). Ayrıca üçüncü bir seçenek de var - argümanları ekleyen / kaldıran sarmalayıcıların oluşturulmasını etkinleştirmek, ancak aslında yüzden fazla sarmalayıcıya ihtiyacım olmasına rağmen toplamda bu sarmalayıcılar çok fazla yer kaplıyor. Bu tek başına çok üzücü, ancak daha ciddi bir sorun olduğu ortaya çıktı: sarmalayıcı işlevlerin oluşturulan kodunda, argümanlar dönüştürüldü ve dönüştürüldü, ancak bazen oluşturulan argümanlara sahip işlev çağrılmadı - yani, tıpkı libffi uygulamam. Yani bazı yardımcılar idam edilmedi.

Neyse ki, Qemu'da aşağıdaki gibi bir başlık dosyası biçiminde makine tarafından okunabilen yardımcı listeleri var

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

Oldukça komik bir şekilde kullanılıyorlar: Öncelikle makrolar çok tuhaf bir şekilde yeniden tanımlanıyor DEF_HELPER_nve ardından açılır helper.h. Makronun bir yapı başlatıcıya ve virgül şeklinde genişletildiği ve ardından bir dizi tanımlandığı ve öğeler yerine - #include <helper.h> Sonuç olarak, nihayet işyerindeki kütüphaneyi deneme şansım oldu. ayrıştırmave tam olarak ihtiyaç duyulan işlevler için tam olarak bu sarmalayıcıları üreten bir komut dosyası yazıldı.

Ve bundan sonra işlemci çalışıyor gibi görünüyordu. Bunun nedeni, memtest86+'nın yerel derlemede çalışabilmesine rağmen ekranın hiçbir zaman başlatılmamış olması gibi görünüyor. Burada Qemu blok I/O kodunun eşyordamlar halinde yazıldığını açıklığa kavuşturmak gerekir. Emscripten'in kendine has çok zorlu bir uygulaması var, ancak yine de Qemu kodunda desteklenmesi gerekiyordu ve şimdi işlemcide hata ayıklayabilirsiniz: Qemu seçenekleri destekliyor -kernel, -initrd, -appendLinux'u veya örneğin memtest86+'yı hiçbir blok aygıt kullanmadan önyükleyebileceğiniz. Ancak sorun şu: Yerel derlemede, Linux çekirdeğinin konsola çıktısı seçeneğiyle birlikte görülebiliyordu. -nographicve tarayıcıdan başlatıldığı terminale çıktı yok emrun, gelmedi. Yani net değil: işlemci çalışmıyor veya grafik çıkışı çalışmıyor. Sonra biraz beklemek aklıma geldi. "İşlemcinin uyumadığı, sadece yavaşça yanıp söndüğü" ortaya çıktı ve yaklaşık beş dakika sonra çekirdek, konsola bir sürü mesaj attı ve takılmaya devam etti. İşlemcinin genel olarak çalıştığı ve SDL2 ile çalışmak için kodu incelememiz gerektiği ortaya çıktı. Maalesef bu kütüphaneyi nasıl kullanacağımı bilmiyorum o yüzden bazı yerlerde rastgele hareket etmek zorunda kaldım. Bir noktada ekranda mavi bir arka plan üzerinde paralel0 çizgisi yanıp söndü ve bu bazı düşünceleri akla getirdi. Sonunda sorunun, Qemu'nun tek bir fiziksel pencerede birkaç sanal pencere açması olduğu ortaya çıktı; bunlar arasında Ctrl-Alt-n kullanarak geçiş yapabilirsiniz: yerel yapıda çalışır, ancak Emscripten'de çalışmaz. Seçenekleri kullanarak gereksiz pencerelerden kurtulduktan sonra -monitor none -parallel none -serial none ve her karede ekranın tamamını zorla yeniden çizme talimatları, her şey aniden işe yaradı.

eşyordamlar

Dolayısıyla, tarayıcıdaki emülasyon çalışır, ancak içinde tek diskette ilginç hiçbir şey çalıştıramazsınız çünkü blok G/Ç yoktur - eşyordamlar için destek uygulamanız gerekir. Qemu'nun halihazırda birkaç ortak rutin arka ucu var, ancak JavaScript'in ve Emscripten kod oluşturucunun doğası gereği, yığınlarla hokkabazlık yapmaya başlayamazsınız. Görünüşe göre "her şey gitti, sıva kaldırılıyor", ancak Emscripten geliştiricileri zaten her şeyi halletmiş durumda. Bu oldukça komik bir şekilde uygulandı: buna benzer bir işlev çağrısını şüpheli olarak adlandıralım emscripten_sleep ve Asyncify mekanizmasını kullanan birkaç başkasının yanı sıra işaretçi çağrıları ve önceki iki durumdan birinin yığının daha aşağısında meydana gelebileceği herhangi bir işleve yapılan çağrılar. Ve şimdi, her şüpheli çağrıdan önce, bir eşzamansız bağlam seçeceğiz ve çağrının hemen ardından, eşzamansız bir çağrının olup olmadığını kontrol edeceğiz ve eğer varsa, tüm yerel değişkenleri bu eşzamansız bağlamda kaydedeceğiz, hangi işlevi göstereceğiz Yürütmeye devam etmemiz gerektiğinde kontrolü aktarmak ve mevcut işlevden çıkmak için. Etkiyi incelemek için kapsamın olduğu yer burasıdır israf — eşzamansız bir çağrıdan geri döndükten sonra kod yürütmeye devam etme ihtiyaçları için, derleyici şüpheli bir çağrıdan sonra başlayan işlevin "saplamalarını" oluşturur - şunun gibi: n şüpheli çağrı varsa, o zaman işlev n/2 bir yere genişletilecektir kez - bu yine de, potansiyel olarak eşzamansız her çağrıdan sonra, bazı yerel değişkenlerin kaydedilmesini orijinal işleve eklemeniz gerektiğini unutmayın. Daha sonra, Python'da, sözde "eşzamansızlığın kendi kendine geçmesine izin vermeyen" (yani, yığın yükseltme ve az önce tanımladığım her şey izin vermez) özellikle aşırı kullanılan belirli bir dizi işleve dayanan basit bir komut dosyası bile yazmak zorunda kaldım. bunlarla çalışın), bu işlevlerin eşzamansız olarak değerlendirilmemesi için işlevlerin derleyici tarafından göz ardı edilmesi gereken işaretçiler aracılığıyla yapılan çağrıları belirtir. Ve 60 MB'ın altındaki JS dosyaları açıkça çok fazla - en az 30 diyelim. Bununla birlikte, bir keresinde bir derleme komut dosyası hazırlarken yanlışlıkla bağlayıcı seçeneklerini attım; bunların arasında şunlar da vardı: -O3. Oluşturulan kodu çalıştırıyorum ve Chromium hafızayı tüketiyor ve çöküyor. Daha sonra kazara indirmeye çalıştığı şeye baktım... Peki, ne diyebilirim ki, benden 500+ MB'lık bir Javascript'i dikkatli bir şekilde incelemem ve optimize etmem istenseydi ben de donardım.

Ne yazık ki, Asyncify destek kütüphanesi kodundaki kontroller tamamen uyumlu değildi. longjmp-s, sanal işlemci kodunda kullanılır, ancak bu kontrolleri devre dışı bırakan ve sanki her şey yolundaymış gibi bağlamları zorla geri yükleyen küçük bir yamadan sonra kod çalıştı. Ve sonra garip bir şey başladı: bazen senkronizasyon kodundaki kontroller tetiklendi - yürütme mantığına göre kodun engellenmesi gerekiyorsa kodu çökertenlerin aynısı - birisi zaten yakalanmış bir muteksi yakalamaya çalıştı. Neyse ki, bunun serileştirilmiş kodda mantıksal bir sorun olmadığı ortaya çıktı - sadece Emscripten tarafından sağlanan standart ana döngü işlevini kullanıyordum, ancak bazen eşzamansız çağrı yığının paketini tamamen açıyor ve o anda başarısız oluyordu. setTimeout ana döngüden - böylece kod, önceki yinelemeden ayrılmadan ana döngü yinelemesine girdi. Sonsuz bir döngüde yeniden yazdım ve emscripten_sleepve mutekslerle ilgili sorunlar durduruldu. Kod daha da mantıklı hale geldi - sonuçta, bir sonraki animasyon çerçevesini hazırlayacak bir kodum yok - işlemci sadece bir şeyi hesaplıyor ve ekran periyodik olarak güncelleniyor. Ancak sorunlar burada bitmedi: Bazen Qemu yürütmesi herhangi bir istisna veya hata olmaksızın sessizce sona eriyordu. O anda bundan vazgeçtim ama ileriye baktığımda sorunun şu olduğunu söyleyeceğim: eşyordam kodu aslında setTimeout (veya en azından düşündüğünüz kadar sık ​​değil): işlev emscripten_yield eşzamansız çağrı bayrağını ayarlamanız yeterlidir. Bütün mesele şu ki emscripten_coroutine_next asenkron bir fonksiyon değildir: dahili olarak bayrağı kontrol eder, sıfırlar ve kontrolü ihtiyaç duyulan yere aktarır. Yani yığının tanıtımı burada bitiyor. Sorun şu ki, mevcut eşyordam arka ucundan önemli bir kod satırını kopyalamadığım için eşyordam havuzu devre dışı bırakıldığında ortaya çıkan ücretsiz kullanımdan sonra, işlev qemu_in_coroutine aslında false döndürmesi gerekirken true değerini döndürdü. Bu bir çağrıya yol açtı emscripten_yield, bunun üstünde yığında kimse yoktu emscripten_coroutine_next, yığın en üste doğru açıldı, ancak hayır setTimeoutDaha önce de söylediğim gibi sergilenmedi.

JavaScript kodu oluşturma

Ve aslında burada da vaat edilen “kıymayı geri çevirmek” var. Tam olarak değil. Elbette, tarayıcıda Qemu'yu ve içinde Node.js'yi çalıştırırsak, doğal olarak Qemu'da kod oluşturduktan sonra tamamen yanlış JavaScript elde ederiz. Ama yine de bir tür ters dönüşüm var.

Öncelikle Qemu'nun nasıl çalıştığı hakkında biraz bilgi verelim. Lütfen beni hemen affedin: Profesyonel bir Qemu geliştiricisi değilim ve sonuçlarım bazı yerlerde hatalı olabilir. Dedikleri gibi, "Öğrencinin görüşünün öğretmenin görüşüyle, Peano'nun aksiyomatikleriyle ve sağduyuyla örtüşmesi gerekmez." Qemu'nun belirli sayıda desteklenen konuk mimarisi vardır ve her biri için şöyle bir dizin vardır: target-i386. Oluştururken birden fazla konuk mimari için destek belirleyebilirsiniz, ancak sonuç yalnızca birkaç ikili dosya olacaktır. Konuk mimarisini destekleyen kod, TCG'nin (Tiny Code Generator) zaten ana bilgisayar mimarisi için makine koduna dönüştürdüğü bazı dahili Qemu işlemlerini üretir. Tcg dizininde bulunan benioku dosyasında belirtildiği gibi, bu başlangıçta normal bir C derleyicisinin parçasıydı ve daha sonra JIT'e uyarlandı. Bu nedenle, örneğin bu belge açısından hedef mimari artık bir konuk mimari değil, bir ana bilgisayar mimarisidir. Bir noktada, belirli bir ana bilgisayar mimarisi için bir kod oluşturucunun yokluğunda kodu (neredeyse aynı dahili işlemler) yürütmesi gereken başka bir bileşen ortaya çıktı - Tiny Code Interpreter (TCI). Aslında, belgelerinde belirtildiği gibi, bu yorumlayıcı her zaman bir JIT kod oluşturucu kadar iyi performans göstermeyebilir; yalnızca hız açısından değil, aynı zamanda nitelik açısından da. Ancak açıklamasının tamamen alakalı olduğundan emin değilim.

İlk başta tam teşekküllü bir TCG arka ucu oluşturmaya çalıştım, ancak kaynak kodunda ve bayt kodu talimatlarının tam olarak net olmayan bir açıklamasında hızla kafam karıştı, bu yüzden TCI yorumlayıcısını sarmaya karar verdim. Bu birkaç avantaj sağladı:

  • Bir kod oluşturucuyu uygularken talimatların açıklamasına değil, yorumlayıcı koduna bakabilirsiniz.
  • karşılaşılan her çeviri bloğu için değil, örneğin yalnızca yüzüncü yürütmeden sonra işlevler oluşturabilirsiniz.
  • oluşturulan kod değişirse (ve yama sözcüğünü içeren adlara sahip işlevlere bakılırsa bu mümkün görünüyor), oluşturulan JS kodunu geçersiz kılmam gerekecek, ancak en azından onu yeniden oluşturacak bir şeyim olacak.

Üçüncü noktaya gelince, kod ilk kez çalıştırıldıktan sonra yama yapmanın mümkün olduğundan emin değilim, ancak ilk iki nokta yeterli.

Başlangıçta kod, orijinal bayt kodu talimatının adresinde büyük bir anahtar biçiminde oluşturuldu, ancak daha sonra Emscripten hakkındaki makaleyi, oluşturulan JS'nin optimizasyonunu ve yeniden döngüyü hatırlayarak, özellikle ampirik olarak daha fazla insan kodu oluşturmaya karar verdim. çeviri bloğuna tek giriş noktasının Başlangıç ​​olduğu ortaya çıktı. Söyledikten hemen sonra, bir süre sonra ifs ile (döngüler olmasa da) kod üreten bir kod oluşturucumuz oldu. Ancak şans eseri, talimatların yanlış uzunlukta olduğu mesajını vererek çöktü. Üstelik bu özyineleme seviyesindeki son talimat şuydu: brcond. Tamam, özyinelemeli çağrıdan önce ve sonra bu talimatın oluşturulmasına aynı kontrolü ekleyeceğim ve... bunlardan biri yürütülmedi, ancak onaylama anahtarından sonra hala başarısız oldular. Sonunda, oluşturulan kodu inceledikten sonra, geçişten sonra mevcut talimatın işaretçisinin yığından yeniden yüklendiğini ve muhtemelen oluşturulan JavaScript kodunun üzerine yazıldığını fark ettim. Ve böylece ortaya çıktı. Tamponun bir megabayttan ona çıkarılması hiçbir şeye yol açmadı ve kod oluşturucunun daireler çizerek çalıştığı anlaşıldı. Mevcut TB'nin sınırlarını aşmadığımızı kontrol etmemiz gerekiyordu ve eğer aştıysak, yürütmeye devam edebilmemiz için bir sonraki TB'nin adresini eksi işaretiyle birlikte vermeliyiz. Ayrıca bu, "bu bayt kodu parçası değişirse hangi oluşturulan işlevler geçersiz kılınmalıdır?" sorununu da çözer. — yalnızca bu çeviri bloğuna karşılık gelen işlevin geçersiz kılınması gerekir. Bu arada, Chromium'da her şeyin hatalarını ayıklamama rağmen (Firefox kullandığım ve deneyler için ayrı bir tarayıcı kullanmak benim için daha kolay olduğu için), Firefox asm.js standardıyla uyumsuzlukları düzeltmeme yardımcı oldu ve ardından kod daha hızlı çalışmaya başladı. Krom.

Oluşturulan kod örneği

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

Sonuç

Yani iş hala tamamlanmadı ama ben bu uzun vadeli inşaatı gizlice mükemmele ulaştırmaktan yoruldum. Bu nedenle şimdilik elimde olanı yayınlamaya karar verdim. Kod bazı yerlerde biraz korkutucu çünkü bu bir deneme ve ne yapılması gerektiği önceden belli değil. Muhtemelen, o zaman Qemu'nun daha modern bir versiyonunun üstüne normal atomik taahhütler vermeye değer. Bu arada Gita'da blog formatında bir konu var: en azından bir şekilde geçilen her "seviye" için Rusça ayrıntılı bir yorum eklendi. Aslında bu makale büyük ölçüde sonucun yeniden anlatımıdır. git log.

Hepsini deneyebilirsiniz burada (trafiğe dikkat edin).

Zaten çalışan şey:

  • x86 sanal işlemci çalışıyor
  • Makine kodundan JavaScript'e kadar bir JIT kod oluşturucunun çalışan bir prototipi var
  • Diğer 32 bit konuk mimarilerini bir araya getirmek için bir şablon var: şu anda yükleme aşamasında tarayıcıda donan MIPS mimarisi için Linux'a hayran kalabilirsiniz

Başka ne yapabilirim

  • Emülasyonu hızlandırın. JIT modunda bile Virtual x86'dan daha yavaş çalışıyor gibi görünüyor (ancak potansiyel olarak birçok taklit donanım ve mimariye sahip bir Qemu var)
  • Normal bir arayüz oluşturmak için - açıkçası iyi bir web geliştiricisi değilim, bu yüzden şimdilik standart Emscripten kabuğunu elimden geldiğince yeniden hazırladım
  • Ağ oluşturma, sanal makine geçişi vb. gibi daha karmaşık Qemu işlevlerini başlatmayı deneyin.
  • UPD: Qemu ve diğer projelerin önceki hamallarının yaptığı gibi, birkaç geliştirmenizi ve hata raporunuzu yukarı yönde Emscripten'e göndermeniz gerekecektir. Emscripten'e olan katkılarını görevimin bir parçası olarak dolaylı olarak kullanabildikleri için onlara teşekkür ederim.

Kaynak: habr.com

Yorum ekle