Qemu.js dengan dukungan JIT: Anda masih dapat membalikkan daging cincang

Beberapa tahun yang lalu Fabrice Bellard ditulis oleh jslinux adalah emulator PC yang ditulis dalam JavaScript. Setelah itu, setidaknya masih ada lagi x86 maya. Tapi semuanya, sejauh yang saya tahu, adalah penerjemah, sedangkan Qemu, yang ditulis jauh lebih awal oleh Fabrice Bellard yang sama, dan, mungkin, emulator modern mana pun, menggunakan kompilasi JIT dari kode tamu ke dalam kode sistem host. Bagi saya, sudah waktunya untuk mengimplementasikan tugas yang berlawanan dengan tugas yang diselesaikan browser: kompilasi JIT kode mesin ke dalam JavaScript, yang tampaknya paling logis untuk mem-porting Qemu. Tampaknya mengapa Qemu, ada emulator yang lebih sederhana dan ramah pengguna - VirtualBox yang sama, misalnya - diinstal dan berfungsi. Namun Qemu memiliki beberapa fitur menarik

  • sumber terbuka
  • kemampuan untuk bekerja tanpa driver kernel
  • kemampuan untuk bekerja dalam mode juru bahasa
  • dukungan untuk sejumlah besar arsitektur host dan tamu

Mengenai poin ketiga, sekarang saya dapat menjelaskan bahwa sebenarnya dalam mode TCI, bukan instruksi mesin tamu itu sendiri yang ditafsirkan, tetapi bytecode yang diperoleh darinya, tetapi ini tidak mengubah esensi - untuk membangun dan menjalankan Qemu pada arsitektur baru, jika Anda beruntung, kompiler AC sudah cukup - penulisan pembuat kode dapat ditunda.

Dan sekarang, setelah dua tahun dengan santai mengutak-atik kode sumber Qemu di waktu luang saya, sebuah prototipe yang berfungsi muncul, di mana Anda sudah dapat menjalankan, misalnya, Kolibri OS.

Apa itu Emscripten

Saat ini sudah banyak bermunculan compiler yang hasil akhirnya adalah JavaScript. Beberapa, seperti Type Script, pada awalnya dimaksudkan sebagai cara terbaik untuk menulis untuk web. Pada saat yang sama, Emscripten adalah cara untuk mengambil kode C atau C++ yang ada dan mengkompilasinya menjadi bentuk yang dapat dibaca browser. Pada Halaman ini Kami telah mengumpulkan banyak port dari program terkenal: di siniMisalnya, Anda dapat melihat PyPy - omong-omong, mereka mengklaim sudah memiliki JIT. Faktanya, tidak semua program dapat dikompilasi dan dijalankan dengan mudah di browser - ada beberapa program fitur, yang harus Anda terima, karena tulisan di halaman yang sama mengatakan “Emscripten dapat digunakan untuk mengkompilasi hampir semua portabel Kode C/C++ ke JavaScript". Artinya, ada sejumlah operasi yang perilakunya tidak ditentukan menurut standar, tetapi biasanya berfungsi pada x86 - misalnya, akses tidak selaras ke variabel, yang umumnya dilarang pada beberapa arsitektur. Secara umum , Qemu adalah program lintas platform dan, saya ingin percaya, dan program ini belum mengandung banyak perilaku yang tidak terdefinisi - ambil dan kompilasi, lalu bermain-main sedikit dengan JIT - dan selesai! Tapi bukan itu kasus...

Percobaan pertama

Secara umum, saya bukanlah orang pertama yang memiliki ide untuk mem-porting Qemu ke JavaScript. Ada pertanyaan yang diajukan di forum ReactOS apakah ini mungkin menggunakan Emscripten. Bahkan sebelumnya, ada rumor bahwa Fabrice Bellard melakukan ini secara pribadi, tetapi kita berbicara tentang jslinux, yang sejauh yang saya tahu, hanyalah upaya untuk mencapai kinerja yang memadai di JS secara manual, dan ditulis dari awal. Kemudian, Virtual x86 ditulis - sumber yang tidak dikaburkan diposting untuk itu, dan, seperti yang dinyatakan, “realisme” emulasi yang lebih besar memungkinkan untuk menggunakan SeaBIOS sebagai firmware. Selain itu, setidaknya ada satu upaya untuk mem-porting Qemu menggunakan Emscripten - saya mencoba melakukan ini pasangan soket, tetapi pembangunan, sejauh yang saya mengerti, terhenti.

Jadi sepertinya ini sumbernya, ini Emscripten - ambil dan kompilasi. Namun ada juga perpustakaan tempat Qemu bergantung, dan perpustakaan tempat perpustakaan tersebut bergantung, dll., dan salah satunya adalah libffi, yang bergantung pada fasih. Ada desas-desus di Internet bahwa ada satu di antara banyak koleksi port perpustakaan untuk Emscripten, tetapi entah bagaimana sulit dipercaya: pertama, itu tidak dimaksudkan untuk menjadi kompiler baru, kedua, itu terlalu rendah tingkat a perpustakaan untuk mengambil, dan mengkompilasi ke JS. Dan ini bukan hanya masalah penyisipan perakitan - mungkin, jika Anda memutarnya, untuk beberapa konvensi pemanggilan Anda dapat menghasilkan argumen yang diperlukan di tumpukan dan memanggil fungsi tanpa argumen tersebut. Namun Emscripten adalah hal yang rumit: untuk membuat kode yang dihasilkan terlihat familier bagi pengoptimal mesin JS browser, beberapa trik digunakan. Secara khusus, apa yang disebut relooping - generator kode menggunakan LLVM IR yang diterima dengan beberapa instruksi transisi abstrak mencoba membuat ulang if, loop, dll yang masuk akal. Nah, bagaimana argumen diteruskan ke fungsi tersebut? Tentu saja, sebagai argumen untuk fungsi JS, jika memungkinkan, tidak melalui stack.

Pada awalnya ada ide untuk sekadar menulis pengganti libffi dengan JS dan menjalankan tes standar, namun pada akhirnya saya bingung bagaimana cara membuat file header agar dapat berfungsi dengan kode yang ada - apa yang bisa saya lakukan, seperti yang mereka katakan, “Apakah tugasnya begitu rumit, "Apakah kita begitu bodoh?" Saya harus mem-porting libffi ke arsitektur lain, bisa dikatakan - untungnya, Emscripten memiliki makro untuk perakitan inline (dalam Javascript, ya - apa pun arsitekturnya, jadi assemblernya), dan kemampuan untuk menjalankan kode yang dihasilkan dengan cepat. Secara umum, setelah mengutak-atik fragmen libffi yang bergantung pada platform selama beberapa waktu, saya mendapatkan beberapa kode yang dapat dikompilasi dan menjalankannya pada pengujian pertama yang saya temukan. Yang mengejutkan saya, tes tersebut berhasil. Terpesona oleh kejeniusan saya - bukan lelucon, ini berhasil sejak peluncuran pertama - Saya, masih tidak mempercayai mata saya, melihat kode yang dihasilkan lagi, untuk mengevaluasi di mana harus menggali selanjutnya. Di sini saya menjadi gila untuk kedua kalinya - satu-satunya hal yang dilakukan fungsi saya adalah ffi_call - ini melaporkan panggilan berhasil. Tidak ada panggilan itu sendiri. Jadi saya mengirimkan permintaan penarikan pertama saya, yang memperbaiki kesalahan dalam tes yang jelas bagi siswa Olimpiade mana pun - bilangan real tidak boleh dibandingkan dengan a == b dan bahkan bagaimana caranya a - b < EPS - Anda juga perlu mengingat modulnya, jika tidak, 0 akan menjadi sama dengan 1/3... Secara umum, saya membuat port libffi tertentu, yang lolos tes paling sederhana, dan fasih yang mana dikompilasi - Saya memutuskan itu perlu, saya akan menambahkannya nanti. Ke depan, saya akan mengatakan bahwa, ternyata, kompiler bahkan tidak menyertakan fungsi libffi dalam kode akhir.

Namun, seperti yang telah saya katakan, ada beberapa batasan, dan di antara penggunaan bebas berbagai perilaku tidak terdefinisi, ada fitur yang lebih tidak menyenangkan yang disembunyikan - JavaScript menurut desain tidak mendukung multithreading dengan memori bersama. Pada prinsipnya, ini biasanya bisa disebut sebagai ide yang bagus, tetapi tidak untuk mem-porting kode yang arsitekturnya terikat pada thread C. Secara umum, Firefox sedang bereksperimen dengan mendukung pekerja bersama, dan Emscripten memiliki implementasi pthread untuk mereka, namun saya tidak ingin bergantung padanya. Saya harus secara perlahan membasmi multithreading dari kode Qemu - yaitu, mencari tahu di mana utas berjalan, memindahkan badan loop yang berjalan di utas ini ke fungsi terpisah, dan memanggil fungsi tersebut satu per satu dari loop utama.

Percobaan kedua

Pada titik tertentu, menjadi jelas bahwa masalahnya masih ada, dan secara sembarangan memasukkan kruk ke dalam kode tidak akan membawa hasil yang baik. Kesimpulan: kita perlu mensistematisasikan proses penambahan kruk. Maka dari itu diambil versi 2.4.1 yang masih fresh saat itu (bukan 2.5.0, karena siapa tahu di versi baru akan ada bug yang belum ketahuan, dan saya punya cukup banyak bug sendiri. ), dan hal pertama adalah menulis ulang dengan aman thread-posix.c. Ya, itu aman: jika seseorang mencoba melakukan operasi yang mengarah ke pemblokiran, fungsi tersebut segera dipanggil abort() - tentu saja, ini tidak menyelesaikan semua masalah sekaligus, tapi setidaknya ini lebih menyenangkan daripada diam-diam menerima data yang tidak konsisten.

Secara umum, opsi Emscripten sangat membantu dalam mem-porting kode ke JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - mereka menangkap beberapa jenis perilaku yang tidak terdefinisi, seperti panggilan ke alamat yang tidak selaras (yang sama sekali tidak konsisten dengan kode untuk array yang diketik seperti HEAP32[addr >> 2] = 1) atau memanggil fungsi dengan jumlah argumen yang salah.

Omong-omong, kesalahan penyelarasan adalah masalah tersendiri. Seperti yang sudah saya katakan, Qemu memiliki backend interpretatif yang “menurun” untuk pembuatan kode TCI (penerjemah kode kecil), dan untuk membangun dan menjalankan Qemu pada arsitektur baru, jika Anda beruntung, kompiler C sudah cukup. "jika kamu beruntung". Saya kurang beruntung, dan ternyata TCI menggunakan akses yang tidak selaras saat mengurai bytecode-nya. Artinya, pada semua jenis ARM dan arsitektur lain dengan akses yang diratakan, Qemu mengkompilasi karena mereka memiliki backend TCG normal yang menghasilkan kode asli, tetapi apakah TCI akan berfungsi pada mereka adalah pertanyaan lain. Namun, ternyata dokumentasi TCI dengan jelas menunjukkan hal serupa. Akibatnya, pemanggilan fungsi untuk pembacaan yang tidak selaras ditambahkan ke kode, yang ditemukan di bagian lain Qemu.

Penghancuran tumpukan

Akibatnya, akses yang tidak selaras ke TCI diperbaiki, loop utama dibuat, yang pada gilirannya disebut prosesor, RCU, dan beberapa hal kecil lainnya. Jadi saya meluncurkan Qemu dengan opsi tersebut -d exec,in_asm,out_asm, yang berarti Anda perlu mengatakan blok kode mana yang sedang dieksekusi, dan juga pada saat siaran untuk menulis apa kode tamunya, apa jadinya kode host (dalam hal ini, bytecode). Itu dimulai, mengeksekusi beberapa blok terjemahan, menulis pesan debugging yang saya tinggalkan bahwa RCU sekarang akan mulai dan... macet abort() di dalam suatu fungsi free(). Dengan mengutak-atik fungsinya free() Kami berhasil menemukan bahwa di header blok heap, yang terletak di delapan byte sebelum memori yang dialokasikan, alih-alih ukuran blok atau yang serupa, terdapat sampah.

Penghancuran tumpukan - betapa lucunya... Dalam kasus seperti itu, ada solusi yang berguna - dari (jika mungkin) sumber yang sama, rakit biner asli dan jalankan di bawah Valgrind. Setelah beberapa waktu, biner sudah siap. Saya meluncurkannya dengan opsi yang sama - crash bahkan selama inisialisasi, sebelum benar-benar mencapai eksekusi. Ini tidak menyenangkan, tentu saja - rupanya, sumbernya tidak persis sama, yang tidak mengejutkan, karena konfigurasikan opsi yang sedikit berbeda, tetapi saya memiliki Valgrind - pertama saya akan memperbaiki bug ini, dan kemudian, jika saya beruntung , yang asli akan muncul. Saya menjalankan hal yang sama di Valgrind... Y-y-y, y-y-y, uh-uh, itu dimulai, melewati inisialisasi secara normal dan melewati bug asli tanpa satu peringatan pun tentang akses memori yang salah, belum lagi tentang jatuh. Kehidupan, seperti yang mereka katakan, tidak mempersiapkan saya untuk ini - program yang mogok berhenti berhenti ketika diluncurkan di bawah Walgrind. Apa itu adalah sebuah misteri. Hipotesis saya adalah bahwa ketika berada di sekitar instruksi saat ini setelah crash selama inisialisasi, gdb menunjukkan pekerjaan memset-a dengan penunjuk yang valid menggunakan salah satunya mmx, atau xmm register, mungkin itu semacam kesalahan penyelarasan, meski masih sulit dipercaya.

Oke, Valgrind sepertinya tidak membantu di sini. Dan di sini hal yang paling menjijikkan dimulai - semuanya tampak dimulai, tetapi crash karena alasan yang sama sekali tidak diketahui karena suatu peristiwa yang bisa terjadi jutaan instruksi yang lalu. Untuk waktu yang lama, bahkan tidak jelas bagaimana cara melakukan pendekatan. Pada akhirnya, saya masih harus duduk dan melakukan debug. Mencetak header yang ditulis ulang menunjukkan bahwa itu tidak terlihat seperti angka, melainkan semacam data biner. Dan, lihatlah, string biner ini ditemukan di file BIOS - yaitu, sekarang kita dapat mengatakan dengan keyakinan yang masuk akal bahwa itu adalah buffer overflow, dan bahkan jelas bahwa itu ditulis ke buffer ini. Nah, kira-kira seperti ini - di Emscripten, untungnya, tidak ada pengacakan ruang alamat, tidak ada lubang di dalamnya juga, jadi Anda dapat menulis di suatu tempat di tengah kode untuk menampilkan data dengan penunjuk dari peluncuran terakhir, lihat datanya, lihat penunjuknya, dan, jika tidak berubah, pikirkanlah. Benar, dibutuhkan beberapa menit untuk menghubungkan setelah ada perubahan, tapi apa yang dapat Anda lakukan? Akibatnya, ditemukan baris tertentu yang menyalin BIOS dari buffer sementara ke memori tamu - dan, memang, tidak ada cukup ruang di buffer. Menemukan sumber alamat buffer aneh itu menghasilkan suatu fungsi qemu_anon_ram_alloc dalam file oslib-posix.c - logikanya begini: terkadang berguna untuk menyelaraskan alamat ke halaman besar berukuran 2 MB, untuk ini kami akan bertanya mmap pertama sedikit lagi, lalu kami akan mengembalikan kelebihannya dengan bantuan munmap. Dan jika penyelarasan seperti itu tidak diperlukan, maka kami akan menunjukkan hasilnya, bukan 2 MB getpagesize() - mmap itu masih akan memberikan alamat yang selaras... Jadi di Emscripten mmap hanya menelepon malloc, tapi tentu saja tidak sejajar pada halamannya. Secara umum, bug yang membuat saya frustrasi selama beberapa bulan telah diperbaiki dengan perubahan двух garis.

Fitur fungsi panggilan

Dan sekarang prosesor sedang menghitung sesuatu, Qemu tidak crash, tetapi layar tidak menyala, dan prosesor dengan cepat berputar, dilihat dari outputnya -d exec,in_asm,out_asm. Sebuah hipotesis muncul: interupsi pengatur waktu (atau, secara umum, semua interupsi) tidak tiba. Dan memang, jika Anda melepaskan interupsi dari rakitan asli, yang karena alasan tertentu berfungsi, Anda akan mendapatkan gambaran serupa. Namun ini bukanlah jawabannya sama sekali: perbandingan jejak yang dikeluarkan dengan opsi di atas menunjukkan bahwa lintasan eksekusi berbeda sejak awal. Di sini harus dikatakan perbandingan dari apa yang direkam menggunakan peluncur emrun men-debug keluaran dengan keluaran rakitan asli bukanlah proses yang sepenuhnya mekanis. Saya tidak tahu persis bagaimana suatu program yang berjalan di browser terhubung emrun, namun beberapa baris pada keluarannya ternyata tersusun ulang, sehingga perbedaan perbedaan tersebut belum menjadi alasan untuk berasumsi bahwa lintasannya telah menyimpang. Secara umum, menjadi jelas bahwa sesuai dengan instruksi ljmpl ada transisi ke alamat yang berbeda, dan bytecode yang dihasilkan pada dasarnya berbeda: yang satu berisi instruksi untuk memanggil fungsi pembantu, yang lain tidak. Setelah mencari instruksi di Google dan mempelajari kode yang menerjemahkan instruksi ini, menjadi jelas bahwa, pertama, tepat sebelum di register cr0 rekaman dibuat - juga menggunakan pembantu - yang mengalihkan prosesor ke mode terproteksi, dan kedua, versi js tidak pernah beralih ke mode terproteksi. Namun faktanya adalah fitur lain dari Emscripten adalah keengganannya untuk mentolerir kode seperti implementasi instruksi call di TCI, yang mana hasil penunjuk fungsi apa pun akan diketik long long f(int arg0, .. int arg9) - fungsi harus dipanggil dengan jumlah argumen yang benar. Jika aturan ini dilanggar, tergantung pada pengaturan debugging, program akan crash (yang bagus) atau memanggil fungsi yang salah sama sekali (yang akan menyedihkan untuk di-debug). Ada juga opsi ketiga - aktifkan pembuatan pembungkus yang menambah/menghapus argumen, tetapi secara total pembungkus ini memakan banyak ruang, meskipun sebenarnya saya hanya membutuhkan lebih dari seratus pembungkus. Ini saja sudah sangat menyedihkan, tetapi ternyata ada masalah yang lebih serius: dalam kode fungsi pembungkus yang dihasilkan, argumen diubah dan diubah, tetapi terkadang fungsi dengan argumen yang dihasilkan tidak dipanggil - yah, seperti di implementasi libffi saya. Artinya, beberapa pembantu tidak dieksekusi.

Untungnya, Qemu memiliki daftar pembantu yang dapat dibaca mesin dalam bentuk file header seperti

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

Mereka digunakan dengan cukup lucu: pertama, makro didefinisikan ulang dengan cara yang paling aneh DEF_HELPER_n, lalu menyala helper.h. Sejauh makro diperluas menjadi penginisialisasi struktur dan koma, dan kemudian array ditentukan, dan alih-alih elemen - #include <helper.h> Alhasil, saya akhirnya berkesempatan mencoba perpustakaan di tempat kerja memparsing, dan sebuah skrip telah ditulis yang menghasilkan pembungkus tersebut dengan tepat untuk fungsi yang diperlukan.

Jadi, setelah itu prosesornya tampak berfungsi. Tampaknya karena layar tidak pernah diinisialisasi, meskipun memtest86+ dapat dijalankan di rakitan asli. Di sini perlu diperjelas bahwa kode I/O blok Qemu ditulis dalam coroutine. Emscripten memiliki implementasinya sendiri yang sangat rumit, namun masih perlu didukung dalam kode Qemu, dan Anda dapat men-debug prosesor sekarang: Qemu mendukung opsi -kernel, -initrd, -append, yang dengannya Anda dapat mem-boot Linux atau, misalnya, memtest86+, tanpa menggunakan perangkat blok sama sekali. Namun inilah masalahnya: di rakitan asli seseorang dapat melihat keluaran kernel Linux ke konsol dengan opsi tersebut -nographic, dan tidak ada keluaran dari browser ke terminal tempat peluncurannya emrun, tidak datang. Artinya, tidak jelas: prosesor tidak berfungsi atau output grafis tidak berfungsi. Dan kemudian terpikir olehku untuk menunggu sebentar. Ternyata “prosesor tidak tidur, tetapi hanya berkedip perlahan,” dan setelah sekitar lima menit, kernel melemparkan banyak pesan ke konsol dan terus hang. Menjadi jelas bahwa prosesor secara umum berfungsi, dan kita perlu menggali kode untuk bekerja dengan SDL2. Sayangnya, saya tidak tahu cara menggunakan perpustakaan ini, jadi di beberapa tempat saya harus bertindak secara acak. Pada titik tertentu, garis parallel0 muncul di layar dengan latar belakang biru, yang memunculkan beberapa pemikiran. Pada akhirnya, ternyata masalahnya adalah Qemu membuka beberapa jendela virtual dalam satu jendela fisik, di antaranya Anda dapat beralih menggunakan Ctrl-Alt-n: ini berfungsi di versi asli, tetapi tidak di Emscripten. Setelah menyingkirkan jendela yang tidak perlu menggunakan opsi -monitor none -parallel none -serial none dan instruksi untuk menggambar ulang secara paksa seluruh layar pada setiap frame, semuanya tiba-tiba berfungsi.

Coroutine

Jadi, emulasi di browser berfungsi, tetapi Anda tidak dapat menjalankan disket tunggal apa pun yang menarik di dalamnya, karena tidak ada blok I/O - Anda perlu menerapkan dukungan untuk coroutine. Qemu sudah memiliki beberapa backend coroutine, namun karena sifat JavaScript dan generator kode Emscripten, Anda tidak bisa begitu saja mulai mengatur tumpukan. Tampaknya "semuanya hilang, plesternya dilepas", tetapi pengembang Emscripten telah mengurus semuanya. Implementasinya cukup lucu: sebut saja pemanggilan fungsi seperti ini mencurigakan emscripten_sleep dan beberapa lainnya menggunakan mekanisme Asyncify, serta panggilan penunjuk dan panggilan ke fungsi apa pun di mana salah satu dari dua kasus sebelumnya mungkin terjadi di bagian bawah tumpukan. Dan sekarang, sebelum setiap panggilan yang mencurigakan, kami akan memilih konteks asinkron, dan segera setelah panggilan tersebut, kami akan memeriksa apakah panggilan asinkron telah terjadi, dan jika ya, kami akan menyimpan semua variabel lokal dalam konteks asinkron ini, tunjukkan fungsi mana untuk mentransfer kendali ke saat kita perlu melanjutkan eksekusi, dan keluar dari fungsi saat ini. Di sinilah ada ruang untuk mempelajari efeknya menyia-nyiakan — untuk kebutuhan melanjutkan eksekusi kode setelah kembali dari panggilan asinkron, kompiler menghasilkan “stub” fungsi yang dimulai setelah panggilan mencurigakan — seperti ini: jika ada n panggilan mencurigakan, maka fungsi tersebut akan diperluas di suatu tempat n/2 kali — ini tetap saja, jika tidak Perlu diingat bahwa setelah setiap panggilan yang berpotensi asinkron, Anda perlu menambahkan beberapa variabel lokal yang disimpan ke fungsi aslinya. Selanjutnya, saya bahkan harus menulis skrip sederhana dengan Python, yang, berdasarkan pada serangkaian fungsi yang terlalu sering digunakan yang seharusnya "tidak membiarkan asinkron melewati dirinya sendiri" (yaitu, promosi tumpukan dan semua yang baru saja saya jelaskan tidak bekerja di dalamnya), menunjukkan panggilan melalui pointer di mana fungsi harus diabaikan oleh kompiler sehingga fungsi-fungsi ini tidak dianggap asinkron. Dan kemudian file JS di bawah 60 MB jelas terlalu banyak - katakanlah setidaknya 30. Meskipun begitu, saya pernah menyiapkan skrip perakitan, dan secara tidak sengaja membuang opsi tautan, di antaranya adalah -O3. Saya menjalankan kode yang dihasilkan, dan Chromium memakan memori dan mogok. Saya kemudian secara tidak sengaja melihat apa yang dia coba unduh... Apa yang bisa saya katakan, saya akan membeku juga jika saya diminta untuk mempelajari dan mengoptimalkan Javascript 500+ MB dengan serius.

Sayangnya, pemeriksaan pada kode pustaka dukungan Asyncify tidak sepenuhnya ramah longjmp-s yang digunakan dalam kode prosesor virtual, tetapi setelah patch kecil yang menonaktifkan pemeriksaan ini dan secara paksa memulihkan konteks seolah-olah semuanya baik-baik saja, kode tersebut berfungsi. Dan kemudian hal yang aneh dimulai: kadang-kadang pemeriksaan dalam kode sinkronisasi dipicu - pemeriksaan yang sama membuat kode crash jika, menurut logika eksekusi, itu harus diblokir - seseorang mencoba mengambil mutex yang sudah ditangkap. Untungnya, ini ternyata bukan masalah logis dalam kode serial - Saya hanya menggunakan fungsionalitas loop utama standar yang disediakan oleh Emscripten, tetapi terkadang panggilan asinkron akan membuka tumpukan sepenuhnya, dan pada saat itu akan gagal setTimeout dari loop utama - dengan demikian, kode memasuki iterasi loop utama tanpa meninggalkan iterasi sebelumnya. Menulis ulang pada loop tak terbatas dan emscripten_sleep, dan masalah dengan mutex berhenti. Kodenya bahkan menjadi lebih logis - lagi pula, sebenarnya saya tidak memiliki kode apa pun yang menyiapkan bingkai animasi berikutnya - prosesor hanya menghitung sesuatu dan layar diperbarui secara berkala. Namun, masalahnya tidak berhenti di situ: terkadang eksekusi Qemu berhenti begitu saja tanpa pengecualian atau kesalahan apa pun. Pada saat itu saya menyerah, tetapi, ke depan, saya akan mengatakan bahwa masalahnya adalah ini: kode coroutine, pada kenyataannya, tidak menggunakan setTimeout (atau setidaknya tidak sesering yang Anda bayangkan): fungsi emscripten_yield cukup setel tanda panggilan asinkron. Intinya adalah itu emscripten_coroutine_next bukan fungsi asynchronous: secara internal ia memeriksa flag, meresetnya dan mentransfer kontrol ke tempat yang diperlukan. Artinya, promosi tumpukan berakhir di situ. Masalahnya adalah karena penggunaan-setelah-bebas, yang muncul ketika kumpulan coroutine dinonaktifkan karena saya tidak menyalin baris kode penting dari backend coroutine yang ada, fungsinya qemu_in_coroutine mengembalikan nilai benar padahal sebenarnya seharusnya mengembalikan nilai salah. Hal ini menyebabkan panggilan emscripten_yield, di atasnya tidak ada seorang pun di tumpukan emscripten_coroutine_next, tumpukannya terbuka ke paling atas, tapi tidak setTimeout, seperti yang sudah saya katakan, tidak dipamerkan.

Pembuatan kode JavaScript

Dan di sinilah sebenarnya janji “mengembalikan daging cincang”. Tidak terlalu. Tentu saja, jika kita menjalankan Qemu di browser, dan Node.js di dalamnya, tentu saja, setelah pembuatan kode di Qemu kita akan mendapatkan JavaScript yang salah sepenuhnya. Tapi tetap saja, semacam transformasi terbalik.

Pertama, sedikit tentang cara kerja Qemu. Mohon maafkan saya segera: Saya bukan pengembang Qemu profesional dan kesimpulan saya mungkin salah di beberapa tempat. Seperti yang mereka katakan, “pendapat siswa tidak harus sesuai dengan pendapat guru, aksiomatik Peano, dan akal sehat.” Qemu memiliki sejumlah arsitektur tamu yang didukung dan untuk masing-masing arsitektur terdapat direktori seperti target-i386. Saat membangun, Anda dapat menentukan dukungan untuk beberapa arsitektur tamu, namun hasilnya hanya beberapa biner. Kode untuk mendukung arsitektur tamu, pada gilirannya, menghasilkan beberapa operasi Qemu internal, yang telah diubah oleh TCG (Tiny Code Generator) menjadi kode mesin untuk arsitektur host. Sebagaimana dinyatakan dalam file readme yang terletak di direktori tcg, ini awalnya merupakan bagian dari kompiler C biasa, yang kemudian diadaptasi untuk JIT. Oleh karena itu, misalnya, arsitektur target dalam dokumen ini bukan lagi arsitektur tamu, melainkan arsitektur host. Pada titik tertentu, komponen lain muncul - Tiny Code Interpreter (TCI), yang harus mengeksekusi kode (operasi internal yang hampir sama) tanpa adanya generator kode untuk arsitektur host tertentu. Faktanya, seperti yang dinyatakan dalam dokumentasinya, penerjemah ini mungkin tidak selalu bekerja sebaik generator kode JIT, tidak hanya secara kuantitatif dalam hal kecepatan, tetapi juga secara kualitatif. Meskipun saya tidak yakin uraiannya sepenuhnya relevan.

Pada awalnya saya mencoba membuat backend TCG yang lengkap, tetapi dengan cepat menjadi bingung dengan kode sumber dan deskripsi instruksi bytecode yang tidak sepenuhnya jelas, jadi saya memutuskan untuk menggabungkan penerjemah TCI. Hal ini memberikan beberapa keuntungan:

  • saat mengimplementasikan pembuat kode, Anda tidak dapat melihat deskripsi instruksinya, tetapi pada kode juru bahasa
  • Anda dapat menghasilkan fungsi tidak untuk setiap blok terjemahan yang ditemui, tetapi, misalnya, hanya setelah eksekusi keseratus
  • jika kode yang dihasilkan berubah (dan ini tampaknya mungkin, dilihat dari fungsi dengan nama yang mengandung kata patch), saya perlu membatalkan kode JS yang dihasilkan, tetapi setidaknya saya memiliki sesuatu untuk membuatnya kembali

Mengenai poin ketiga, saya tidak yakin bahwa patching dapat dilakukan setelah kode dieksekusi untuk pertama kalinya, tetapi dua poin pertama sudah cukup.

Awalnya, kode tersebut dihasilkan dalam bentuk saklar besar di alamat instruksi bytecode asli, tetapi kemudian, mengingat artikel tentang Emscripten, optimasi JS yang dihasilkan dan pengulangan, saya memutuskan untuk menghasilkan lebih banyak kode manusia, terutama karena secara empiris itu ternyata satu-satunya titik masuk ke blok terjemahan adalah Start-nya. Tidak lama kemudian, kami memiliki generator kode yang menghasilkan kode dengan if (walaupun tanpa loop). Tapi sialnya, ia crash, memberikan pesan bahwa instruksinya salah panjangnya. Selain itu, instruksi terakhir pada tingkat rekursi ini adalah brcond. Oke, saya akan menambahkan pemeriksaan yang sama pada pembuatan instruksi ini sebelum dan sesudah panggilan rekursif dan... tidak satu pun dari mereka dieksekusi, tetapi setelah saklar penegasan mereka masih gagal. Pada akhirnya, setelah mempelajari kode yang dihasilkan, saya menyadari bahwa setelah peralihan, penunjuk ke instruksi saat ini dimuat ulang dari tumpukan dan mungkin ditimpa oleh kode JavaScript yang dihasilkan. Dan ternyata begitu. Meningkatkan buffer dari satu megabyte menjadi sepuluh tidak menghasilkan apa-apa, dan menjadi jelas bahwa pembuat kode berjalan berputar-putar. Kami harus memastikan bahwa kami tidak melampaui batas TB saat ini, dan jika kami melakukannya, berikan alamat TB berikutnya dengan tanda minus sehingga kami dapat melanjutkan eksekusi. Selain itu, ini memecahkan masalah "fungsi mana yang dihasilkan yang harus dibatalkan jika bytecode ini telah berubah?" — hanya fungsi yang sesuai dengan blok terjemahan ini yang perlu dibatalkan. Omong-omong, meskipun saya men-debug semuanya di Chromium (karena saya menggunakan Firefox dan lebih mudah bagi saya untuk menggunakan browser terpisah untuk eksperimen), Firefox membantu saya memperbaiki ketidaksesuaian dengan standar asm.js, setelah itu kode mulai bekerja lebih cepat di Kromium.

Contoh kode 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, pekerjaannya masih belum selesai, tapi saya lelah diam-diam menyempurnakan konstruksi jangka panjang ini. Oleh karena itu, saya memutuskan untuk mempublikasikan apa yang saya miliki saat ini. Kode ini agak menakutkan di beberapa tempat, karena ini adalah eksperimen, dan tidak jelas sebelumnya apa yang perlu dilakukan. Mungkin, ada baiknya mengeluarkan komitmen atom normal di atas beberapa versi Qemu yang lebih modern. Sementara itu, ada thread di Gita dalam format blog: untuk setiap “level” yang telah dilewati, komentar mendetail dalam bahasa Rusia telah ditambahkan. Sebenarnya, artikel ini sebagian besar menceritakan kembali kesimpulannya git log.

Anda bisa mencoba semuanya di sini (hati-hati terhadap lalu lintas).

Apa yang sudah berfungsi:

  • prosesor virtual x86 berjalan
  • Ada prototipe generator kode JIT yang berfungsi dari kode mesin ke JavaScript
  • Ada template untuk merakit arsitektur tamu 32-bit lainnya: saat ini Anda dapat mengagumi Linux karena arsitektur MIPS yang membeku di browser pada tahap pemuatan

Apa lagi yang bisa Anda lakukan

  • Mempercepat emulasi. Bahkan dalam mode JIT tampaknya berjalan lebih lambat daripada Virtual x86 (tetapi kemungkinan ada Qemu keseluruhan dengan banyak perangkat keras dan arsitektur yang ditiru)
  • Untuk membuat antarmuka normal - sejujurnya, saya bukan pengembang web yang baik, jadi untuk saat ini saya telah membuat ulang shell standar Emscripten sebaik mungkin
  • Coba luncurkan fungsi Qemu yang lebih kompleks - jaringan, migrasi VM, dll.
  • UPD: Anda perlu mengirimkan beberapa perkembangan dan laporan bug ke Emscripten upstream, seperti yang dilakukan oleh porter Qemu sebelumnya dan proyek lainnya. Terima kasih kepada mereka karena secara implisit dapat menggunakan kontribusi mereka pada Emscripten sebagai bagian dari tugas saya.

Sumber: www.habr.com

Tambah komentar