Qemu.js dengan sokongan JIT: anda masih boleh memusingkan cincang ke belakang

Beberapa tahun yang lalu Fabrice Bellard ditulis oleh jslinux ialah emulator PC yang ditulis dalam JavaScript. Selepas itu terdapat sekurang-kurangnya lebih x86 maya. Tetapi semua daripada mereka, setakat yang saya tahu, adalah jurubahasa, manakala Qemu, ditulis lebih awal oleh Fabrice Bellard yang sama, dan, mungkin, mana-mana emulator moden yang menghormati diri sendiri, menggunakan kompilasi JIT kod tetamu ke dalam kod sistem hos. Nampaknya saya sudah tiba masanya untuk melaksanakan tugas yang bertentangan berhubung dengan tugas yang diselesaikan oleh penyemak imbas: JIT kompilasi kod mesin ke dalam JavaScript, yang mana ia kelihatan paling logik untuk mengalihkan Qemu. Nampaknya, mengapa Qemu, terdapat emulator yang lebih mudah dan mesra pengguna - VirtualBox yang sama, contohnya - dipasang dan berfungsi. Tetapi Qemu mempunyai beberapa ciri menarik

  • sumber terbuka
  • keupayaan untuk bekerja tanpa pemacu kernel
  • keupayaan untuk bekerja dalam mod penterjemah
  • sokongan untuk sejumlah besar seni bina hos dan tetamu

Mengenai perkara ketiga, saya kini boleh menjelaskan bahawa sebenarnya, dalam mod TCI, bukan arahan mesin tetamu sendiri yang ditafsirkan, tetapi kod byte yang diperoleh daripada mereka, tetapi ini tidak mengubah intipati - untuk membina dan menjalankan Qemu pada seni bina baharu, jika anda bernasib baik, pengkompil A C sudah cukup - menulis penjana kod boleh ditangguhkan.

Dan sekarang, selepas dua tahun bermain-main dengan santai dengan kod sumber Qemu pada masa lapang saya, prototaip yang berfungsi muncul, di mana anda sudah boleh menjalankan, sebagai contoh, Kolibri OS.

Apa itu Emscripten

Pada masa kini, banyak penyusun telah muncul, hasil akhirnya adalah JavaScript. Sesetengah, seperti Skrip Jenis, pada asalnya bertujuan untuk menjadi cara terbaik untuk menulis untuk web. Pada masa yang sama, Emscripten ialah satu cara untuk mengambil kod C atau C++ sedia ada dan menyusunnya ke dalam bentuk yang boleh dibaca pelayar. hidup halaman ini Kami telah mengumpulkan banyak pelabuhan program terkenal: di siniSebagai contoh, anda boleh melihat PyPy - dengan cara itu, mereka mendakwa sudah mempunyai JIT. Sebenarnya, bukan setiap program boleh disusun dan dijalankan dalam penyemak imbas - terdapat nombor ciri-ciri, yang perlu anda hadapi, walau bagaimanapun, kerana inskripsi pada halaman yang sama mengatakan "Emscripten boleh digunakan untuk menyusun hampir semua mudah alih Kod C/C++ kepada JavaScript". Iaitu, terdapat beberapa operasi yang tidak ditentukan kelakuan mengikut standard, tetapi biasanya berfungsi pada x86 - contohnya, akses tidak sejajar kepada pembolehubah, yang secara amnya dilarang pada sesetengah seni bina. Secara umum , Qemu ialah program merentas platform dan , saya mahu percaya, dan ia belum lagi mengandungi banyak tingkah laku yang tidak ditentukan - ambil dan susun, kemudian fikirkan sedikit dengan JIT - dan anda sudah selesai! Tetapi itu bukan kes...

Percubaan pertama

Secara umumnya, saya bukan orang pertama yang menghasilkan idea untuk mengalihkan Qemu ke JavaScript. Terdapat soalan yang ditanya pada forum ReactOS jika ini boleh dilakukan menggunakan Emscripten. Malah sebelum ini, terdapat khabar angin bahawa Fabrice Bellard melakukan ini secara peribadi, tetapi kami bercakap tentang jslinux, yang, sejauh yang saya tahu, hanyalah percubaan untuk mencapai prestasi yang mencukupi secara manual dalam JS, dan ditulis dari awal. Kemudian, Virtual x86 telah ditulis - sumber yang tidak dikelirukan telah disiarkan untuknya, dan, seperti yang dinyatakan, "realisme" emulasi yang lebih besar memungkinkan untuk menggunakan SeaBIOS sebagai firmware. Di samping itu, terdapat sekurang-kurangnya satu percubaan untuk mengalihkan Qemu menggunakan Emscripten - saya cuba melakukan ini pasangan soket, tetapi pembangunan, setakat yang saya faham, dibekukan.

Jadi, nampaknya, inilah sumbernya, inilah Emscripten - ambil dan susun. Tetapi terdapat juga perpustakaan di mana Qemu bergantung, dan perpustakaan di mana perpustakaan tersebut bergantung, dsb., dan salah satunya ialah libffi, yang bergantung pada glib. Terdapat khabar angin di Internet bahawa terdapat satu dalam koleksi besar pelabuhan perpustakaan untuk Emscripten, tetapi entah bagaimana sukar untuk dipercayai: pertama, ia tidak bertujuan untuk menjadi penyusun baharu, kedua, ia terlalu rendah tahap perpustakaan untuk hanya mengambil, dan menyusun ke JS. Dan ini bukan hanya soal sisipan pemasangan - mungkin, jika anda memutarkannya, untuk beberapa konvensyen panggilan anda boleh menjana hujah yang diperlukan pada timbunan dan memanggil fungsi tanpanya. Tetapi Emscripten adalah perkara yang rumit: untuk menjadikan kod yang dihasilkan kelihatan biasa kepada pengoptimum enjin JS penyemak imbas, beberapa helah digunakan. Khususnya, apa yang dipanggil relooping - penjana kod menggunakan IR LLVM yang diterima dengan beberapa arahan peralihan abstrak cuba mencipta semula ifs, gelung, dsb. Nah, bagaimana hujah-hujah dihantar ke fungsi? Sememangnya, sebagai hujah kepada fungsi JS, iaitu, jika boleh, bukan melalui timbunan.

Pada mulanya terdapat idea untuk hanya menulis pengganti libffi dengan JS dan menjalankan ujian standard, tetapi pada akhirnya saya keliru tentang cara membuat fail pengepala saya supaya ia berfungsi dengan kod sedia ada - apa yang boleh saya lakukan, seperti yang mereka katakan, "Adakah tugasan begitu rumit "Adakah kita begitu bodoh?" Saya terpaksa mengalihkan libffi ke seni bina lain, boleh dikatakan - mujurlah, Emscripten mempunyai kedua-dua makro untuk pemasangan sebaris (dalam Javascript, ya - baik, apa sahaja seni bina, jadi pemasang), dan keupayaan untuk menjalankan kod yang dihasilkan dengan cepat. Secara umum, selepas bermain-main dengan serpihan libffi yang bergantung pada platform untuk beberapa lama, saya mendapat beberapa kod yang boleh disusun dan menjalankannya pada ujian pertama yang saya temui. Terkejut saya, ujian itu berjaya. Terpegun dengan genius saya - tidak ada jenaka, ia berfungsi dari pelancaran pertama - saya, masih tidak mempercayai mata saya, pergi melihat kod yang dihasilkan sekali lagi, untuk menilai di mana untuk menggali seterusnya. Di sini saya menjadi gila buat kali kedua - satu-satunya perkara yang dilakukan oleh fungsi saya ialah ffi_call - ini melaporkan panggilan yang berjaya. Tiada panggilan itu sendiri. Jadi saya menghantar permintaan tarik pertama saya, yang membetulkan ralat dalam ujian yang jelas kepada mana-mana pelajar Olimpik - nombor nyata tidak boleh dibandingkan sebagai a == b dan juga bagaimana a - b < EPS - anda juga perlu mengingati modul itu, jika tidak 0 akan menjadi sangat sama dengan 1/3... Secara umum, saya menghasilkan port libffi tertentu, yang melepasi ujian paling mudah, dan dengan glib yang mana disusun - Saya memutuskan ia perlu, saya akan menambahnya kemudian. Melihat ke hadapan, saya akan mengatakan bahawa, ternyata, pengkompil tidak memasukkan fungsi libffi dalam kod akhir.

Tetapi, seperti yang telah saya katakan, terdapat beberapa batasan, dan antara penggunaan percuma pelbagai tingkah laku yang tidak ditentukan, ciri yang lebih tidak menyenangkan telah disembunyikan - JavaScript mengikut reka bentuk tidak menyokong multithreading dengan memori bersama. Pada dasarnya, ini biasanya boleh dipanggil idea yang baik, tetapi bukan untuk kod port yang seni binanya terikat pada benang C. Secara umumnya, Firefox sedang bereksperimen dengan menyokong pekerja kongsi, dan Emscripten mempunyai pelaksanaan pthread untuk mereka, tetapi saya tidak mahu bergantung padanya. Saya terpaksa menghapuskan multithreading secara perlahan-lahan daripada kod Qemu - iaitu, ketahui di mana benang berjalan, gerakkan badan gelung yang berjalan dalam benang ini ke fungsi yang berasingan, dan panggil fungsi tersebut satu demi satu dari gelung utama.

Cuba kedua

Pada satu ketika, ia menjadi jelas bahawa masalah itu masih ada, dan menolak tongkat secara sembarangan di sekeliling kod itu tidak akan membawa kepada apa-apa kebaikan. Kesimpulan: entah bagaimana kita perlu mensistemkan proses menambah tongkat. Oleh itu, versi 2.4.1, yang baru pada masa itu, telah diambil (bukan 2.5.0, kerana, siapa tahu, akan ada pepijat dalam versi baharu yang masih belum ditangkap, dan saya mempunyai cukup pepijat saya sendiri. ), dan perkara pertama ialah menulis semula dengan selamat thread-posix.c. Yaitu, selamat: jika seseorang cuba melakukan operasi yang membawa kepada penyekatan, fungsi itu segera dipanggil abort() - sudah tentu, ini tidak menyelesaikan semua masalah sekaligus, tetapi sekurang-kurangnya ia entah bagaimana lebih menyenangkan daripada menerima data yang tidak konsisten secara senyap-senyap.

Secara umum, pilihan Emscripten sangat membantu dalam mengalihkan kod ke JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - mereka menangkap beberapa jenis tingkah laku yang tidak ditentukan, seperti panggilan ke alamat yang tidak sejajar (yang sama sekali tidak konsisten dengan kod untuk tatasusunan yang ditaip seperti HEAP32[addr >> 2] = 1) atau memanggil fungsi dengan bilangan argumen yang salah.

By the way, ralat penjajaran adalah isu yang berasingan. Seperti yang telah saya katakan, Qemu mempunyai bahagian belakang tafsiran "merosot" untuk penjanaan kod TCI (jurubahasa kod kecil), dan untuk membina dan menjalankan Qemu pada seni bina baharu, jika anda bernasib baik, pengkompil C sudah mencukupi. Kata kunci "kalau awak bernasib baik". Saya tidak bernasib baik, dan ternyata TCI menggunakan akses tidak sejajar apabila menghuraikan kod baitnya. Iaitu, pada semua jenis ARM dan seni bina lain dengan akses yang diratakan semestinya, Qemu menyusun kerana mereka mempunyai bahagian belakang TCG biasa yang menjana kod asli, tetapi sama ada TCI akan berfungsi pada mereka adalah persoalan lain. Walau bagaimanapun, ternyata, dokumentasi TCI jelas menunjukkan sesuatu yang serupa. Akibatnya, panggilan fungsi untuk bacaan tidak sejajar telah ditambahkan pada kod, yang ditemui di bahagian lain Qemu.

Kemusnahan timbunan

Akibatnya, akses tidak sejajar kepada TCI telah diperbetulkan, gelung utama telah dicipta yang seterusnya dipanggil pemproses, RCU dan beberapa perkara kecil lain. Jadi saya melancarkan Qemu dengan pilihan -d exec,in_asm,out_asm, yang bermaksud bahawa anda perlu menyatakan blok kod yang sedang dilaksanakan, dan juga pada masa penyiaran untuk menulis apakah kod tetamu, kod hos menjadi (dalam kes ini, bytecode). Ia bermula, melaksanakan beberapa blok terjemahan, menulis mesej penyahpepijatan yang saya tinggalkan bahawa RCU kini akan bermula dan... ranap abort() dalam sesuatu fungsi free(). Dengan bermain-main dengan fungsi free() Kami berjaya mengetahui bahawa dalam pengepala blok timbunan, yang terletak dalam lapan bait sebelum memori yang diperuntukkan, bukannya saiz blok atau sesuatu yang serupa, terdapat sampah.

Pemusnahan timbunan - betapa comelnya... Dalam kes sedemikian, terdapat ubat yang berguna - daripada (jika boleh) sumber yang sama, kumpulkan binari asli dan jalankannya di bawah Valgrind. Selepas beberapa lama, binari telah siap. Saya melancarkannya dengan pilihan yang sama - ia ranap walaupun semasa permulaan, sebelum benar-benar mencapai pelaksanaan. Ia tidak menyenangkan, sudah tentu - nampaknya, sumbernya tidak betul-betul sama, yang tidak menghairankan, kerana konfigurasi meninjau pilihan yang sedikit berbeza, tetapi saya mempunyai Valgrind - pertama saya akan membetulkan pepijat ini, dan kemudian, jika saya bernasib baik , yang asal akan muncul. Saya menjalankan perkara yang sama di bawah Valgrind... Y-y-y, y-y-y, uh-uh, ia bermula, melalui pemula seperti biasa dan bergerak melepasi pepijat asal tanpa sebarang amaran tentang akses memori yang salah, apatah lagi tentang jatuh. Kehidupan, seperti yang mereka katakan, tidak menyediakan saya untuk ini - program ranap berhenti ranap apabila dilancarkan di bawah Walgrind. Apa itu adalah misteri. Hipotesis saya ialah apabila berada di sekitar arahan semasa selepas ranap sistem semasa pemulaan, gdb menunjukkan kerja memset-a dengan penunjuk yang sah menggunakan sama ada mmx, atau xmm mendaftar, maka mungkin ia adalah sejenis ralat penjajaran, walaupun ia masih sukar dipercayai.

Okey, Valgrind nampaknya tidak membantu di sini. Dan di sini perkara yang paling menjijikkan bermula - segala-galanya seolah-olah bermula, tetapi ranap atas sebab yang tidak diketahui kerana peristiwa yang mungkin berlaku berjuta-juta arahan yang lalu. Untuk masa yang lama, ia tidak jelas bagaimana untuk mendekati. Akhirnya, saya masih perlu duduk dan nyahpepijat. Mencetak apa yang pengepala itu ditulis semula menunjukkan bahawa ia tidak kelihatan seperti nombor, sebaliknya sejenis data binari. Dan, lihatlah, rentetan binari ini ditemui dalam fail BIOS - iaitu, sekarang adalah mungkin untuk mengatakan dengan keyakinan yang munasabah bahawa ia adalah limpahan penampan, dan bahkan jelas bahawa ia telah ditulis kepada penimbal ini. Nah, kemudian sesuatu seperti ini - dalam Emscripten, mujurlah, tidak ada rawak ruang alamat, tidak ada lubang di dalamnya, jadi anda boleh menulis di suatu tempat di tengah-tengah kod untuk mengeluarkan data dengan penunjuk dari pelancaran terakhir, lihat data, lihat penunjuk, dan, jika ia tidak berubah, dapatkan makanan untuk difikirkan. Benar, ia mengambil masa beberapa minit untuk dipautkan selepas sebarang perubahan, tetapi apakah yang boleh anda lakukan? Akibatnya, satu baris tertentu ditemui yang menyalin BIOS dari penimbal sementara ke memori tetamu - dan, sememangnya, tidak ada ruang yang mencukupi dalam penimbal. Mencari sumber alamat penimbal aneh itu menghasilkan fungsi qemu_anon_ram_alloc dalam fail oslib-posix.c - logiknya ada ini: kadangkala ia berguna untuk menyelaraskan alamat ke halaman besar bersaiz 2 MB, untuk ini kami akan bertanya mmap mula-mula sedikit lagi, dan kemudian kami akan mengembalikan lebihan itu dengan bantuan munmap. Dan jika penjajaran sedemikian tidak diperlukan, maka kami akan menunjukkan hasilnya dan bukannya 2 MB getpagesize() - mmap ia masih akan memberikan alamat sejajar... Jadi dalam Emscripten mmap panggilan sahaja malloc, tetapi sudah tentu ia tidak sejajar pada halaman. Secara umum, pepijat yang mengecewakan saya selama beberapa bulan telah diperbetulkan oleh perubahan dalam Π΄Π²ΡƒΡ… garisan.

Ciri-ciri fungsi panggilan

Dan kini pemproses mengira sesuatu, Qemu tidak ranap, tetapi skrin tidak dihidupkan, dan pemproses dengan cepat masuk ke gelung, berdasarkan output -d exec,in_asm,out_asm. Hipotesis telah muncul: gangguan pemasa (atau, secara umum, semua gangguan) tidak tiba. Dan sesungguhnya, jika anda menanggalkan gangguan dari perhimpunan asli, yang atas sebab tertentu berfungsi, anda mendapat gambaran yang serupa. Tetapi ini bukan jawapan sama sekali: perbandingan jejak yang dikeluarkan dengan pilihan di atas menunjukkan bahawa trajektori pelaksanaan menyimpang sangat awal. Di sini mesti dikatakan bahawa perbandingan apa yang direkodkan menggunakan pelancar emrun keluaran penyahpepijatan dengan keluaran pemasangan asli bukanlah proses mekanikal sepenuhnya. Saya tidak tahu dengan tepat bagaimana program yang berjalan dalam penyemak imbas disambungkan emrun, tetapi beberapa baris dalam output ternyata disusun semula, jadi perbezaan dalam perbezaan belum lagi menjadi alasan untuk menganggap bahawa trajektori telah menyimpang. Secara umum, ia menjadi jelas bahawa mengikut arahan ljmpl terdapat peralihan kepada alamat yang berbeza, dan kod bait yang dijana pada asasnya berbeza: satu mengandungi arahan untuk memanggil fungsi pembantu, satu lagi tidak. Selepas googling arahan dan mengkaji kod yang menterjemah arahan ini, menjadi jelas bahawa, pertama sekali, sejurus sebelum ia dalam daftar cr0 rakaman telah dibuat - juga menggunakan pembantu - yang menukar pemproses kepada mod dilindungi, dan kedua, bahawa versi js tidak pernah bertukar kepada mod dilindungi. Tetapi hakikatnya ialah ciri lain Emscripten ialah keengganannya untuk bertolak ansur dengan kod seperti pelaksanaan arahan call dalam TCI, yang mana-mana penunjuk fungsi menghasilkan jenis long long f(int arg0, .. int arg9) - fungsi mesti dipanggil dengan bilangan argumen yang betul. Jika peraturan ini dilanggar, bergantung pada tetapan penyahpepijatan, program sama ada ranap (yang bagus) atau memanggil fungsi yang salah sama sekali (yang akan menyedihkan untuk nyahpepijat). Terdapat juga pilihan ketiga - membolehkan penjanaan pembungkus yang menambah / membuang hujah, tetapi secara keseluruhan pembungkus ini mengambil banyak ruang, walaupun pada hakikatnya saya hanya memerlukan lebih sedikit daripada seratus pembungkus. Ini sahaja sangat menyedihkan, tetapi ternyata terdapat masalah yang lebih serius: dalam kod yang dihasilkan bagi fungsi pembalut, argumen telah ditukar dan ditukar, tetapi kadangkala fungsi dengan argumen yang dihasilkan tidak dipanggil - baik, sama seperti dalam pelaksanaan libffi saya. Iaitu, beberapa pembantu tidak dibunuh.

Nasib baik, Qemu mempunyai senarai pembantu yang boleh dibaca mesin dalam bentuk fail pengepala seperti

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

Ia digunakan agak lucu: pertama, makro ditakrifkan semula dengan cara yang paling pelik DEF_HELPER_n, dan kemudian hidupkan helper.h. Setakat yang makro dikembangkan menjadi pemula struktur dan koma, dan kemudian tatasusunan ditakrifkan, dan bukannya elemen - #include <helper.h> Hasilnya, saya akhirnya berpeluang mencuba perpustakaan di tempat kerja pyparsing, dan skrip telah ditulis yang menghasilkan pembungkus yang tepat untuk fungsi yang diperlukan.

Jadi, selepas itu pemproses seolah-olah berfungsi. Nampaknya kerana skrin tidak pernah dimulakan, walaupun memtest86+ dapat dijalankan dalam perhimpunan asli. Di sini adalah perlu untuk menjelaskan bahawa kod I/O blok Qemu ditulis dalam coroutine. Emscripten mempunyai pelaksanaannya yang sangat rumit, tetapi ia masih perlu disokong dalam kod Qemu, dan anda boleh nyahpepijat pemproses sekarang: Qemu menyokong pilihan -kernel, -initrd, -append, yang mana anda boleh boot Linux atau, sebagai contoh, memtest86+, tanpa menggunakan peranti blok sama sekali. Tetapi inilah masalahnya: dalam pemasangan asli seseorang dapat melihat output kernel Linux ke konsol dengan pilihan -nographic, dan tiada output daripada pelayar ke terminal dari mana ia dilancarkan emrun, tidak datang. Iaitu, tidak jelas: pemproses tidak berfungsi atau output grafik tidak berfungsi. Dan kemudian saya terfikir untuk menunggu sedikit. Ternyata "pemproses tidak tidur, tetapi hanya berkelip perlahan," dan selepas kira-kira lima minit kernel melemparkan sekumpulan mesej ke konsol dan terus menggantung. Ia menjadi jelas bahawa pemproses, secara umum, berfungsi, dan kita perlu menggali kod untuk bekerja dengan SDL2. Malangnya, saya tidak tahu cara menggunakan perpustakaan ini, jadi di beberapa tempat saya terpaksa bertindak secara rawak. Pada satu ketika, garisan selari0 berkelip pada skrin pada latar belakang biru, yang mencadangkan beberapa pemikiran. Akhirnya, ternyata masalahnya ialah Qemu membuka beberapa tetingkap maya dalam satu tetingkap fizikal, di antaranya anda boleh bertukar menggunakan Ctrl-Alt-n: ia berfungsi dalam binaan asli, tetapi tidak dalam Emscripten. Selepas menyingkirkan tingkap yang tidak perlu menggunakan pilihan -monitor none -parallel none -serial none dan arahan untuk melukis semula keseluruhan skrin secara paksa pada setiap bingkai, semuanya tiba-tiba berfungsi.

Coroutines

Jadi, emulasi dalam penyemak imbas berfungsi, tetapi anda tidak boleh menjalankan apa-apa liut tunggal yang menarik di dalamnya, kerana tiada blok I/O - anda perlu melaksanakan sokongan untuk coroutine. Qemu sudah mempunyai beberapa bahagian belakang coroutine, tetapi disebabkan sifat JavaScript dan penjana kod Emscripten, anda tidak boleh mula menyulap tindanan. Nampaknya "segala-galanya hilang, plaster sedang dikeluarkan," tetapi pemaju Emscripten telah menguruskan segala-galanya. Ini dilaksanakan agak lucu: mari kita panggil panggilan fungsi seperti ini mencurigakan emscripten_sleep dan beberapa yang lain menggunakan mekanisme Asyncify, serta panggilan penuding dan panggilan ke mana-mana fungsi di mana salah satu daripada dua kes sebelumnya mungkin berlaku lebih jauh di bawah timbunan. Dan sekarang, sebelum setiap panggilan yang mencurigakan, kami akan memilih konteks tak segerak, dan sejurus selepas panggilan, kami akan menyemak sama ada panggilan tak segerak telah berlaku, dan jika ada, kami akan menyimpan semua pembolehubah tempatan dalam konteks asinkron ini, menunjukkan fungsi yang mana. untuk memindahkan kawalan kepada bila kita perlu meneruskan pelaksanaan , dan keluar dari fungsi semasa. Di sinilah terdapat ruang untuk mengkaji kesannya membazir β€” untuk keperluan meneruskan pelaksanaan kod selepas kembali daripada panggilan tak segerak, pengkompil menjana β€œstub” fungsi bermula selepas panggilan yang mencurigakan β€” seperti ini: jika terdapat n panggilan yang mencurigakan, maka fungsi itu akan dikembangkan di suatu tempat n/2 kali β€” ini masih, jika tidak Perlu diingat bahawa selepas setiap panggilan yang mungkin tidak segerak, anda perlu menambah menyimpan beberapa pembolehubah setempat pada fungsi asal. Selepas itu, saya juga terpaksa menulis skrip ringkas dalam Python, yang, berdasarkan set tertentu fungsi yang digunakan secara berlebihan yang kononnya "tidak membenarkan asynchrony melalui diri mereka sendiri" (iaitu, promosi tindanan dan semua yang saya nyatakan tidak bekerja di dalamnya), menunjukkan panggilan melalui penunjuk di mana fungsi harus diabaikan oleh pengkompil supaya fungsi ini tidak dianggap tak segerak. Dan kemudian fail JS di bawah 60 MB jelas terlalu banyak - katakan sekurang-kurangnya 30. Walaupun, sebaik sahaja saya menyediakan skrip pemasangan, dan secara tidak sengaja membuang pilihan pemaut, antaranya ialah -O3. Saya menjalankan kod yang dijana dan Chromium memakan memori dan ranap sistem. Saya kemudian secara tidak sengaja melihat apa yang dia cuba muat turun... Nah, apa yang boleh saya katakan, saya juga akan beku jika saya diminta untuk mengkaji dengan teliti dan mengoptimumkan Javascript 500+ MB.

Malangnya, semakan dalam kod perpustakaan sokongan Asyncify tidak mesra sepenuhnya longjmp-s yang digunakan dalam kod pemproses maya, tetapi selepas tampalan kecil yang melumpuhkan semakan ini dan memulihkan konteks secara paksa seolah-olah semuanya baik-baik saja, kod itu berfungsi. Dan kemudian perkara pelik bermula: kadangkala semakan dalam kod penyegerakan telah dicetuskan - yang sama yang merosakkan kod jika, mengikut logik pelaksanaan, ia harus disekat - seseorang cuba merebut mutex yang telah ditangkap. Nasib baik, ini ternyata bukan masalah logik dalam kod bersiri - Saya hanya menggunakan fungsi gelung utama standard yang disediakan oleh Emscripten, tetapi kadangkala panggilan tak segerak akan membuka keseluruhan timbunan, dan pada masa itu ia akan gagal setTimeout dari gelung utama - oleh itu, kod memasuki lelaran gelung utama tanpa meninggalkan lelaran sebelumnya. Menulis semula pada gelung tak terhingga dan emscripten_sleep, dan masalah dengan mutex berhenti. Kod itu malah menjadi lebih logik - lagipun, sebenarnya, saya tidak mempunyai beberapa kod yang menyediakan bingkai animasi seterusnya - pemproses hanya mengira sesuatu dan skrin dikemas kini secara berkala. Walau bagaimanapun, masalah tidak berhenti di situ: kadangkala pelaksanaan Qemu hanya akan ditamatkan secara senyap tanpa sebarang pengecualian atau ralat. Pada masa itu saya berputus asa, tetapi, melihat ke hadapan, saya akan mengatakan bahawa masalahnya ialah ini: kod coroutine, sebenarnya, tidak menggunakan setTimeout (atau sekurang-kurangnya tidak sekerap yang anda fikirkan): fungsi emscripten_yield hanya menetapkan bendera panggilan tak segerak. Intinya ialah itu emscripten_coroutine_next bukan fungsi tak segerak: secara dalaman ia menyemak bendera, menetapkan semula dan memindahkan kawalan ke tempat yang diperlukan. Iaitu, promosi timbunan berakhir di sana. Masalahnya ialah kerana penggunaan selepas bebas, yang muncul apabila kumpulan coroutine dilumpuhkan kerana saya tidak menyalin baris kod penting dari bahagian belakang coroutine sedia ada, fungsi itu qemu_in_coroutine dikembalikan benar sedangkan sebenarnya ia sepatutnya kembali palsu. Ini membawa kepada panggilan emscripten_yield, di atasnya tiada sesiapa pun pada timbunan emscripten_coroutine_next, susunan terbentang ke bahagian paling atas, tetapi tidak setTimeout, seperti yang telah saya katakan, tidak dipamerkan.

Penjanaan kod JavaScript

Dan di sini, sebenarnya, adalah janji "membalikkan daging cincang." Tidak juga. Sudah tentu, jika kita menjalankan Qemu dalam penyemak imbas, dan Node.js di dalamnya, maka, secara semulajadi, selepas penjanaan kod dalam Qemu kita akan mendapat JavaScript yang salah sepenuhnya. Tetapi masih, beberapa jenis transformasi terbalik.

Pertama, sedikit tentang cara Qemu berfungsi. Harap maafkan saya dengan segera: Saya bukan pembangun Qemu profesional dan kesimpulan saya mungkin tersilap di sesetengah tempat. Seperti yang mereka katakan, "pendapat pelajar tidak semestinya bertepatan dengan pendapat guru, aksiomatik dan akal sehat Peano." Qemu mempunyai beberapa seni bina tetamu yang disokong dan untuk setiap satu terdapat direktori seperti target-i386. Semasa membina, anda boleh menentukan sokongan untuk beberapa seni bina tetamu, tetapi hasilnya hanya akan menjadi beberapa binari. Kod untuk menyokong seni bina tetamu, seterusnya, menjana beberapa operasi Qemu dalaman, yang TCG (Tiny Code Generator) sudah bertukar menjadi kod mesin untuk seni bina hos. Seperti yang dinyatakan dalam fail readme yang terletak dalam direktori tcg, ini pada asalnya sebahagian daripada pengkompil C biasa, yang kemudiannya disesuaikan untuk JIT. Oleh itu, sebagai contoh, seni bina sasaran dari segi dokumen ini bukan lagi seni bina tetamu, tetapi seni bina hos. Pada satu ketika, komponen lain muncul - Tiny Code Interpreter (TCI), yang sepatutnya melaksanakan kod (operasi dalaman yang hampir sama) tanpa ketiadaan penjana kod untuk seni bina hos tertentu. Malah, seperti yang dinyatakan oleh dokumentasinya, penterjemah ini mungkin tidak selalu berfungsi sebaik penjana kod JIT, bukan sahaja secara kuantitatif dari segi kelajuan, tetapi juga secara kualitatif. Walaupun saya tidak pasti penerangannya benar-benar relevan.

Pada mulanya saya cuba membuat bahagian belakang TCG yang lengkap, tetapi dengan cepat keliru dalam kod sumber dan penerangan yang tidak sepenuhnya jelas tentang arahan bytecode, jadi saya memutuskan untuk membungkus penterjemah TCI. Ini memberikan beberapa kelebihan:

  • apabila melaksanakan penjana kod, anda tidak boleh melihat pada perihalan arahan, tetapi pada kod penterjemah
  • anda boleh menjana fungsi bukan untuk setiap blok terjemahan yang ditemui, tetapi, sebagai contoh, hanya selepas pelaksanaan keseratus
  • jika kod yang dijana berubah (dan ini nampaknya mungkin, berdasarkan fungsi dengan nama yang mengandungi patch perkataan), saya perlu membatalkan kod JS yang dijana, tetapi sekurang-kurangnya saya akan mempunyai sesuatu untuk menjana semula daripadanya

Mengenai titik ketiga, saya tidak pasti bahawa penampalan boleh dilakukan selepas kod dilaksanakan buat kali pertama, tetapi dua mata pertama sudah mencukupi.

Pada mulanya, kod itu dihasilkan dalam bentuk suis besar pada alamat arahan bytecode asal, tetapi kemudian, mengingati artikel tentang Emscripten, pengoptimuman JS yang dijana dan relooping, saya memutuskan untuk menjana lebih banyak kod manusia, terutamanya kerana ia secara empirik. ternyata satu-satunya titik masuk ke dalam blok terjemahan ialah Mulanya. Tidak lama kemudian, selepas beberapa ketika kami mempunyai penjana kod yang menghasilkan kod dengan ifs (walaupun tanpa gelung). Tetapi nasib malang, ia terhempas, memberikan mesej bahawa arahan itu tidak betul. Selain itu, arahan terakhir pada tahap rekursi ini ialah brcond. Okey, saya akan menambah semakan yang sama pada penjanaan arahan ini sebelum dan selepas panggilan rekursif dan... tidak satu pun daripadanya telah dilaksanakan, tetapi selepas suis penegasan mereka masih gagal. Pada akhirnya, selepas mengkaji kod yang dijana, saya menyedari bahawa selepas suis, penunjuk kepada arahan semasa dimuat semula dari timbunan dan mungkin ditimpa oleh kod JavaScript yang dihasilkan. Dan begitulah ternyata. Meningkatkan penimbal daripada satu megabait kepada sepuluh tidak membawa kepada apa-apa, dan menjadi jelas bahawa penjana kod berjalan dalam bulatan. Kami perlu menyemak bahawa kami tidak melampaui sempadan TB semasa, dan jika kami melakukannya, maka keluarkan alamat TB seterusnya dengan tanda tolak supaya kami boleh meneruskan pelaksanaan. Di samping itu, ini menyelesaikan masalah "fungsi yang dijana harus menjadi tidak sah jika sekeping bytecode ini telah berubah?" β€” hanya fungsi yang sepadan dengan blok terjemahan ini perlu dibatalkan. Ngomong-ngomong, walaupun saya menyahpepijat semua dalam Chromium (memandangkan saya menggunakan Firefox dan lebih mudah untuk saya menggunakan penyemak imbas yang berasingan untuk percubaan), Firefox membantu saya membetulkan ketidakserasian dengan piawaian asm.js, selepas itu kod itu mula berfungsi dengan lebih pantas dalam Chromium.

Contoh kod yang dihasilkan

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

Kesimpulan

Jadi, kerja masih belum selesai, tetapi saya bosan untuk menyempurnakan pembinaan jangka panjang ini secara diam-diam. Oleh itu, saya memutuskan untuk menerbitkan apa yang saya ada buat masa ini. Kod ini agak menakutkan di beberapa tempat, kerana ini adalah percubaan, dan tidak jelas terlebih dahulu perkara yang perlu dilakukan. Mungkin, maka ia patut mengeluarkan komit atom biasa di atas beberapa versi Qemu yang lebih moden. Sementara itu, terdapat satu utas dalam Gita dalam format blog: untuk setiap "peringkat" yang telah sekurang-kurangnya entah bagaimana lulus, ulasan terperinci dalam bahasa Rusia telah ditambah. Sebenarnya, artikel ini sebahagian besarnya menceritakan semula kesimpulannya git log.

Anda boleh mencuba semuanya di sini (berhati-hati dengan lalu lintas).

Apa yang sudah berfungsi:

  • pemproses maya x86 berjalan
  • Terdapat prototaip berfungsi penjana kod JIT daripada kod mesin kepada JavaScript
  • Terdapat templat untuk memasang seni bina tetamu 32-bit yang lain: sekarang anda boleh mengagumi Linux kerana seni bina MIPS yang membeku dalam penyemak imbas pada peringkat pemuatan

Apa lagi boleh buat

  • Mempercepatkan emulasi. Walaupun dalam mod JIT nampaknya berjalan lebih perlahan daripada Virtual x86 (tetapi terdapat kemungkinan keseluruhan Qemu dengan banyak perkakasan dan seni bina yang ditiru)
  • Untuk membuat antara muka biasa - terus terang, saya bukan pembangun web yang baik, jadi buat masa ini saya telah membuat semula shell Emscripten standard sebaik mungkin
  • Cuba lancarkan fungsi Qemu yang lebih kompleks - rangkaian, migrasi VM, dsb.
  • UPD: anda perlu menyerahkan beberapa perkembangan dan laporan pepijat anda kepada Emscripten huluan, seperti yang dilakukan oleh porter Qemu dan projek lain sebelum ini. Terima kasih kepada mereka kerana dapat secara tersirat menggunakan sumbangan mereka kepada Emscripten sebagai sebahagian daripada tugas saya.

Sumber: www.habr.com

Tambah komen