QEMU.js: kini serius dan dengan WASM

Pada suatu masa dahulu saya memutuskan untuk berseronok membuktikan kebolehbalikan proses tersebut dan pelajari cara menjana JavaScript (lebih tepat lagi, Asm.js) daripada kod mesin. QEMU telah dipilih untuk percubaan, dan beberapa lama kemudian sebuah artikel telah ditulis mengenai Habr. Dalam komen saya dinasihatkan untuk membuat semula projek itu dalam WebAssembly, dan juga berhenti sendiri hampir siap Saya entah bagaimana tidak mahu projek itu... Kerja sedang berjalan, tetapi sangat perlahan, dan kini, baru-baru ini dalam artikel itu muncul ulasan mengenai topik "Jadi bagaimana semuanya berakhir?" Sebagai tindak balas kepada jawapan terperinci saya, saya mendengar "Ini kedengaran seperti artikel." Nah, jika anda boleh, akan ada artikel. Mungkin seseorang akan mendapati ia berguna. Daripadanya pembaca akan mempelajari beberapa fakta tentang reka bentuk bahagian belakang penjanaan kod QEMU, serta cara menulis pengkompil Just-in-Time untuk aplikasi web.

tugas-tugas

Memandangkan saya telah pun belajar cara "entah bagaimana" mengalihkan QEMU ke JavaScript, kali ini diputuskan untuk melakukannya dengan bijak dan tidak mengulangi kesilapan lama.

Ralat nombor satu: cawangan dari keluaran titik

Kesilapan pertama saya adalah untuk menggantikan versi saya daripada versi huluan 2.4.1. Kemudian saya rasa idea yang baik: jika pelepasan titik wujud, maka ia mungkin lebih stabil daripada 2.4 mudah, dan lebih-lebih lagi cawangan master. Dan memandangkan saya bercadang untuk menambah sejumlah besar pepijat saya sendiri, saya tidak memerlukan orang lain sama sekali. Mungkin begitulah keadaannya. Tetapi inilah perkaranya: QEMU tidak berdiam diri, dan pada satu ketika mereka juga mengumumkan pengoptimuman kod yang dijana sebanyak 10 peratus. "Ya, sekarang saya akan membeku," saya fikir dan putus asa. Di sini kita perlu membuat penyimpangan: disebabkan sifat satu-benang QEMU.js dan fakta bahawa QEMU asal tidak membayangkan ketiadaan berbilang-benang (iaitu, keupayaan untuk mengendalikan beberapa laluan kod yang tidak berkaitan secara serentak, dan bukan sahaja "menggunakan semua kernel") adalah penting untuknya, fungsi utama utas saya terpaksa "mematikan" untuk dapat memanggil dari luar. Ini menimbulkan beberapa masalah semula jadi semasa penggabungan. Walau bagaimanapun, hakikat bahawa beberapa perubahan daripada cawangan master, yang mana saya cuba menggabungkan kod saya, juga telah dipilih ceri dalam keluaran titik (dan oleh itu dalam cawangan saya) juga mungkin tidak akan menambah kemudahan.

Secara umum, saya memutuskan bahawa masih masuk akal untuk membuang prototaip, membukanya untuk bahagian dan membina versi baharu dari awal berdasarkan sesuatu yang lebih segar dan sekarang dari master.

Kesilapan nombor dua: Metodologi TLP

Pada dasarnya, ini bukan satu kesilapan, secara amnya, ia hanya satu ciri untuk mencipta projek dalam keadaan salah faham yang lengkap tentang kedua-dua "di mana dan bagaimana untuk bergerak?" dan secara umum "adakah kita akan sampai ke sana?" Dalam keadaan ini pengaturcaraan yang kekok adalah pilihan yang wajar, tetapi, secara semula jadi, saya tidak mahu mengulanginya tanpa perlu. Kali ini saya mahu melakukannya dengan bijak: komitmen atom, perubahan kod sedar (dan bukan "mengikat aksara rawak bersama-sama sehingga ia disusun (dengan amaran)", seperti yang pernah dikatakan oleh Linus Torvalds tentang seseorang, menurut Wikiquote), dsb.

Kesilapan nombor tiga: masuk ke dalam air tanpa mengetahui ford

Saya masih belum sepenuhnya menyingkirkan perkara ini, tetapi sekarang saya telah memutuskan untuk tidak mengikuti jalan yang paling kurang rintangan, dan melakukannya dengan "cara dewasa", iaitu, tulis bahagian belakang TCG saya dari awal, supaya tidak perlu berkata kemudian, "Ya, ini sudah tentu, perlahan-lahan, tetapi saya tidak dapat mengawal segala-galanya - begitulah cara TCI ditulis..." Lebih-lebih lagi, ini pada mulanya kelihatan seperti penyelesaian yang jelas, kerana Saya menjana kod binari. Seperti yang mereka katakan, "Ghent berkumpulΡƒ, tetapi bukan yang itu”: kod itu, sudah tentu, binari, tetapi kawalan tidak boleh dipindahkan begitu sahaja - ia mesti ditolak secara eksplisit ke dalam penyemak imbas untuk penyusunan, menghasilkan objek tertentu dari dunia JS, yang masih perlu diselamatkan di suatu tempat. Walau bagaimanapun, pada seni bina RISC biasa, setakat yang saya faham, situasi biasa ialah keperluan untuk menetapkan semula cache arahan secara eksplisit untuk kod yang dijana semula - jika ini bukan yang kita perlukan, maka, dalam apa jua keadaan, ia hampir. Di samping itu, daripada percubaan terakhir saya, saya mengetahui bahawa kawalan nampaknya tidak dipindahkan ke bahagian tengah blok terjemahan, jadi kami tidak benar-benar memerlukan bytecode yang ditafsirkan daripada sebarang offset, dan kami hanya boleh menjananya daripada fungsi pada TB .

Mereka datang dan menendang

Walaupun saya mula menulis semula kod itu pada bulan Julai, tendangan ajaib merayap tanpa disedari: biasanya surat daripada GitHub tiba sebagai pemberitahuan tentang respons kepada permintaan Isu dan Tarik, tetapi di sini, tiba-tiba sebut dalam benang Binaryen sebagai backend qemu dalam konteks, "Dia melakukan sesuatu seperti itu, mungkin dia akan mengatakan sesuatu." Kami bercakap tentang menggunakan perpustakaan berkaitan Emscripten binaryen untuk mencipta WASM JIT. Baiklah, saya katakan bahawa anda mempunyai lesen Apache 2.0 di sana, dan QEMU secara keseluruhan diedarkan di bawah GPLv2, dan ia tidak begitu serasi. Tiba-tiba ternyata lesen boleh betulkan entah bagaimana (Saya tidak tahu: mungkin mengubahnya, mungkin dwi pelesenan, mungkin sesuatu yang lain...). Ini, tentu saja, membuat saya gembira, kerana pada masa itu saya sudah melihat dengan teliti format binari WebAssembly, dan saya entah bagaimana sedih dan tidak dapat difahami. Terdapat juga perpustakaan yang akan memakan blok asas dengan graf peralihan, menghasilkan bytecode, dan juga menjalankannya dalam penterjemah itu sendiri, jika perlu.

Kemudian ada lagi satu surat dalam senarai mel QEMU, tetapi ini lebih kepada soalan, "Siapa pula yang memerlukannya?" Dan ia adalah tiba-tiba, ternyata ia perlu. Sekurang-kurangnya, anda boleh mengikis bersama kemungkinan penggunaan berikut, jika ia berfungsi lebih atau kurang dengan cepat:

  • melancarkan sesuatu yang mendidik tanpa sebarang pemasangan sama sekali
  • virtualisasi pada iOS, di mana, menurut khabar angin, satu-satunya aplikasi yang mempunyai hak untuk menjana kod dengan cepat ialah enjin JS (adakah ini benar?)
  • demonstrasi mini-OS - liut tunggal, terbina dalam, semua jenis perisian tegar, dll...

Ciri Masa Jalan Pelayar

Seperti yang telah saya katakan, QEMU terikat dengan multithreading, tetapi pelayar tidak memilikinya. Ya, itu, tidak... Pada mulanya ia tidak wujud sama sekali, kemudian WebWorkers muncul - setakat yang saya faham, ini adalah multithreading berdasarkan penghantaran mesej tanpa pembolehubah yang dikongsi. Sememangnya, ini menimbulkan masalah besar apabila mengalihkan kod sedia ada berdasarkan model memori yang dikongsi. Kemudian, di bawah tekanan orang ramai, ia juga dilaksanakan di bawah nama SharedArrayBuffers. Ia diperkenalkan secara beransur-ansur, mereka meraikan pelancarannya dalam pelayar yang berbeza, kemudian mereka merayakan Tahun Baru, dan kemudian Meltdown... Selepas itu mereka sampai pada kesimpulan bahawa ukuran masa yang kasar atau kasar, tetapi dengan bantuan memori bersama dan benang menambah kaunter, semuanya sama ia akan berfungsi dengan cukup tepat. Jadi kami melumpuhkan multithreading dengan memori yang dikongsi. Nampaknya mereka kemudian menghidupkannya semula, tetapi, kerana ia menjadi jelas dari percubaan pertama, ada kehidupan tanpanya, dan jika ya, kami akan cuba melakukannya tanpa bergantung pada multithreading.

Ciri kedua ialah kemustahilan manipulasi peringkat rendah dengan tindanan: anda tidak boleh begitu sahaja mengambil, menyimpan konteks semasa dan beralih kepada yang baharu dengan tindanan baharu. Timbunan panggilan diuruskan oleh mesin maya JS. Nampaknya, apakah masalahnya, kerana kami masih memutuskan untuk menguruskan aliran terdahulu sepenuhnya secara manual? Hakikatnya ialah blok I/O dalam QEMU dilaksanakan melalui coroutine, dan di sinilah manipulasi tindanan peringkat rendah akan berguna. Nasib baik, Emscipten sudah mengandungi mekanisme untuk operasi tak segerak, walaupun dua: Asyncify ΠΈ Maharaja. Yang pertama berfungsi melalui pembahagian yang ketara dalam kod JavaScript yang dijana dan tidak lagi disokong. Yang kedua ialah "cara yang betul" semasa dan berfungsi melalui penjanaan bytecode untuk penterjemah asli. Ia berfungsi, sudah tentu, perlahan-lahan, tetapi ia tidak mengembang kod. Benar, sokongan untuk coroutine untuk mekanisme ini perlu disumbangkan secara bebas (sudah ada coroutine yang ditulis untuk Asyncify dan terdapat pelaksanaan kira-kira API yang sama untuk Emterpreter, anda hanya perlu menyambungkannya).

Pada masa ini, saya masih belum berjaya membahagikan kod kepada satu yang disusun dalam WASM dan ditafsirkan menggunakan Emterpreter, jadi peranti sekat masih belum berfungsi (lihat dalam siri seterusnya, seperti yang mereka katakan...). Iaitu, pada akhirnya anda harus mendapat sesuatu seperti perkara berlapis lucu ini:

  • blok I/O yang ditafsirkan. Nah, adakah anda benar-benar mengharapkan NVMe yang dicontohi dengan prestasi asli? πŸ™‚
  • kod QEMU utama yang disusun secara statik (penterjemah, peranti lain yang dicontohi, dsb.)
  • kod tetamu yang disusun secara dinamik ke dalam WASM

Ciri-ciri sumber QEMU

Seperti yang anda mungkin sudah meneka, kod untuk meniru seni bina tetamu dan kod untuk menjana arahan mesin hos dipisahkan dalam QEMU. Malah, ia lebih rumit:

  • terdapat seni bina tetamu
  • ada pemecut, iaitu, KVM untuk virtualisasi perkakasan pada Linux (untuk sistem tetamu dan hos yang serasi antara satu sama lain), TCG untuk penjanaan kod JIT di mana-mana sahaja. Bermula dengan QEMU 2.9, sokongan untuk standard virtualisasi perkakasan HAXM pada Windows muncul (butirannya)
  • jika TCG digunakan dan bukannya virtualisasi perkakasan, maka ia mempunyai sokongan penjanaan kod yang berasingan untuk setiap seni bina hos, serta untuk penterjemah universal
  • ... dan di sekeliling semua ini - peranti persisian yang dicontohi, antara muka pengguna, penghijrahan, main semula rekod, dsb.

By the way, adakah anda tahu: QEMU boleh meniru bukan sahaja keseluruhan komputer, tetapi juga pemproses untuk proses pengguna yang berasingan dalam kernel hos, yang digunakan, sebagai contoh, oleh fuzzer AFL untuk instrumentasi binari. Mungkin seseorang ingin mengalihkan mod operasi QEMU ini kepada JS? πŸ˜‰

Seperti kebanyakan perisian percuma yang telah lama wujud, QEMU dibina melalui panggilan configure ΠΈ make. Katakan anda memutuskan untuk menambah sesuatu: hujung belakang TCG, pelaksanaan utas, sesuatu yang lain. Jangan tergesa-gesa untuk gembira/seram (gariskan mengikut kesesuaian) pada prospek berkomunikasi dengan Autoconf - sebenarnya, configure QEMU nampaknya ditulis sendiri dan tidak dihasilkan daripada apa-apa.

webassembly

Jadi apakah perkara ini yang dipanggil WebAssembly (aka WASM)? Ini adalah pengganti untuk Asm.js, tidak lagi berpura-pura sebagai kod JavaScript yang sah. Sebaliknya, ia adalah binari semata-mata dan dioptimumkan, malah hanya menulis integer ke dalamnya tidak begitu mudah: untuk kekompakan, ia disimpan dalam format LEB128.

Anda mungkin pernah mendengar tentang algoritma pengulangan semula untuk Asm.js - ini ialah pemulihan arahan kawalan aliran "peringkat tinggi" (iaitu, jika-maka-lain, gelung, dll.), yang mana enjin JS direka, daripada IR LLVM peringkat rendah, lebih dekat dengan kod mesin yang dilaksanakan oleh pemproses. Sememangnya, perwakilan perantaraan QEMU lebih hampir kepada yang kedua. Nampaknya di sinilah, bytecode, penghujung siksaan... Dan kemudian terdapat blok, if-then-else dan gelung!..

Dan ini adalah satu lagi sebab mengapa Binaryen berguna: ia secara semula jadi boleh menerima blok peringkat tinggi yang hampir dengan apa yang akan disimpan dalam WASM. Tetapi ia juga boleh menghasilkan kod daripada graf blok asas dan peralihan antara mereka. Baiklah, saya telah mengatakan bahawa ia menyembunyikan format storan WebAssembly di sebalik API C/C++ yang mudah.

TCG (Penjana Kod Kecil)

GTC pada asalnya bahagian belakang untuk pengkompil C. Kemudian, nampaknya, ia tidak dapat menahan persaingan dengan GCC, tetapi pada akhirnya ia mendapat tempatnya dalam QEMU sebagai mekanisme penjanaan kod untuk platform hos. Terdapat juga bahagian belakang TCG yang menghasilkan beberapa kod bait abstrak, yang segera dilaksanakan oleh jurubahasa, tetapi saya memutuskan untuk mengelak daripada menggunakannya kali ini. Walau bagaimanapun, hakikat bahawa dalam QEMU sudah mungkin untuk membolehkan peralihan kepada TB yang dijana melalui fungsi tersebut tcg_qemu_tb_exec, ternyata ia sangat berguna untuk saya.

Untuk menambah bahagian belakang TCG baharu pada QEMU, anda perlu mencipta subdirektori tcg/<имя Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Ρ‹> (dalam kes ini, tcg/binaryen), dan ia mengandungi dua fail: tcg-target.h ΠΈ tcg-target.inc.c ΠΈ menetapkan itu semua tentang configure. Anda boleh meletakkan fail lain di sana, tetapi, seperti yang anda boleh meneka dari nama kedua-dua ini, kedua-duanya akan disertakan di suatu tempat: satu sebagai fail pengepala biasa (ia termasuk dalam tcg/tcg.h, dan yang itu sudah ada dalam fail lain dalam direktori tcg, accel dan bukan sahaja), yang lain - hanya sebagai coretan kod dalam tcg/tcg.c, tetapi ia mempunyai akses kepada fungsi statiknya.

Memutuskan bahawa saya akan menghabiskan terlalu banyak masa untuk penyiasatan terperinci tentang cara ia berfungsi, saya hanya menyalin "rangka" kedua-dua fail ini daripada pelaksanaan bahagian belakang yang lain, dengan jujur ​​menunjukkan perkara ini dalam pengepala lesen.

fail tcg-target.h mengandungi terutamanya tetapan dalam borang #define-s:

  • berapa banyak daftar dan berapa lebar yang ada pada seni bina sasaran (kita mempunyai seberapa banyak yang kita mahu, seberapa banyak yang kita mahu - persoalannya lebih lanjut mengenai perkara yang akan dijana menjadi kod yang lebih cekap oleh penyemak imbas pada seni bina "sasaran sepenuhnya" ...)
  • penjajaran arahan hos: pada x86, dan walaupun dalam TCI, arahan tidak diselaraskan sama sekali, tetapi saya akan meletakkan penampan kod bukan arahan sama sekali, tetapi penunjuk kepada struktur perpustakaan Binaryen, jadi saya akan berkata: 4 bait
  • apakah arahan pilihan yang boleh dihasilkan oleh bahagian belakang - kami menyertakan semua yang kami temui dalam Binaryen, biarkan pemecut memecahkan yang lain menjadi yang lebih mudah sendiri
  • Apakah anggaran saiz cache TLB yang diminta oleh bahagian belakang. Hakikatnya ialah dalam QEMU segala-galanya adalah serius: walaupun terdapat fungsi pembantu yang melaksanakan beban/simpan dengan mengambil kira MMU tetamu (di mana kita akan berada tanpanya sekarang?), mereka menyimpan cache terjemahan mereka dalam bentuk struktur, pemprosesan yang mudah untuk dibenamkan terus ke dalam blok penyiaran. Persoalannya, apakah offset dalam struktur ini yang paling cekap diproses oleh urutan arahan yang kecil dan pantas?
  • di sini anda boleh mengubah tujuan satu atau dua daftar simpanan, dayakan panggilan TB melalui fungsi dan secara pilihan menerangkan beberapa inline-berfungsi seperti flush_icache_range (tetapi ini bukan kes kami)

fail tcg-target.inc.c, sudah tentu, biasanya bersaiz lebih besar dan mengandungi beberapa fungsi wajib:

  • permulaan, termasuk sekatan ke atas arahan yang boleh beroperasi pada operan mana. Disalin secara terang-terangan oleh saya dari bahagian belakang yang lain
  • fungsi yang mengambil satu arahan bytecode dalaman
  • Anda juga boleh meletakkan fungsi tambahan di sini, dan anda juga boleh menggunakan fungsi statik daripada tcg/tcg.c

Untuk diri saya sendiri, saya memilih strategi berikut: dalam perkataan pertama blok terjemahan seterusnya, saya menulis empat petunjuk: tanda permulaan (nilai tertentu di sekitar 0xFFFFFFFF, yang menentukan keadaan semasa TB), konteks, modul yang dijana dan nombor ajaib untuk penyahpepijatan. Pada mulanya tanda itu diletakkan 0xFFFFFFFF - nJika n - nombor positif yang kecil, dan setiap kali ia dilaksanakan melalui penterjemah ia meningkat sebanyak 1. Apabila ia mencapai 0xFFFFFFFE, kompilasi berlaku, modul telah disimpan dalam jadual fungsi, diimport ke dalam "pelancar" kecil, di mana pelaksanaan pergi dari tcg_qemu_tb_exec, dan modul telah dialih keluar daripada memori QEMU.

Untuk menghuraikan klasik, "Crutch, berapa banyak yang terjalin dalam bunyi ini untuk jantung proger ...". Namun, ingatan itu bocor entah ke mana. Lebih-lebih lagi, ia adalah memori yang diuruskan oleh QEMU! Saya mempunyai kod yang, semasa menulis arahan seterusnya (iaitu, penunjuk), memadamkan kod yang pautannya berada di tempat ini sebelum ini, tetapi ini tidak membantu. Sebenarnya, dalam kes paling mudah, QEMU memperuntukkan memori pada permulaan dan menulis kod yang dihasilkan di sana. Apabila penimbal habis, kod itu dibuang dan yang seterusnya mula ditulis di tempatnya.

Selepas mengkaji kod itu, saya menyedari bahawa helah dengan nombor ajaib membolehkan saya tidak gagal pada pemusnahan timbunan dengan membebaskan sesuatu yang salah pada penimbal yang tidak dimulakan pada hantaran pertama. Tetapi siapa yang menulis semula penimbal untuk memintas fungsi saya nanti? Seperti yang dinasihatkan oleh pembangun Emscripten, apabila saya menghadapi masalah, saya mengalihkan kod yang terhasil kembali ke aplikasi asli, menetapkan Mozilla Record-Replay padanya... Secara umum, pada akhirnya saya menyedari satu perkara yang mudah: untuk setiap blok, a struct TranslationBlock dengan penerangannya. Teka di mana... Betul, sebelum blok betul-betul dalam penimbal. Menyedari perkara ini, saya memutuskan untuk berhenti menggunakan tongkat (sekurang-kurangnya beberapa), dan hanya membuang nombor ajaib, dan memindahkan perkataan yang tinggal ke struct TranslationBlock, mencipta senarai pautan tunggal yang boleh dilalui dengan cepat apabila cache terjemahan ditetapkan semula dan mengosongkan memori.

Beberapa tongkat kekal: sebagai contoh, penunjuk bertanda dalam penimbal kod - sebahagian daripadanya adalah ringkas BinaryenExpressionRef, iaitu, mereka melihat ungkapan yang perlu dimasukkan secara linear ke dalam blok asas yang dijana, sebahagiannya adalah syarat untuk peralihan antara BB, sebahagian adalah ke mana hendak pergi. Nah, sudah ada blok yang disediakan untuk Relooper yang perlu disambungkan mengikut syarat. Untuk membezakannya, andaian digunakan bahawa kesemuanya diselaraskan dengan sekurang-kurangnya empat bait, jadi anda boleh menggunakan dua bit yang paling kurang penting untuk label, anda hanya perlu ingat untuk mengalih keluarnya jika perlu. Ngomong-ngomong, label sedemikian sudah digunakan dalam QEMU untuk menunjukkan sebab untuk keluar dari gelung TCG.

Menggunakan Binaryen

Modul dalam WebAssembly mengandungi fungsi, setiap satunya mengandungi badan, yang merupakan ungkapan. Ungkapan ialah operasi unari dan binari, blok yang terdiri daripada senarai ungkapan lain, aliran kawalan, dsb. Seperti yang telah saya katakan, aliran kawalan di sini disusun dengan tepat sebagai cawangan peringkat tinggi, gelung, panggilan fungsi, dll. Argumen kepada fungsi tidak dihantar pada timbunan, tetapi secara eksplisit, sama seperti dalam JS. Terdapat juga pembolehubah global, tetapi saya belum menggunakannya, jadi saya tidak akan memberitahu anda tentangnya.

Fungsi juga mempunyai pembolehubah tempatan, bernombor dari sifar, jenis: int32 / int64 / float / double. Dalam kes ini, n pembolehubah tempatan pertama ialah hujah yang dihantar kepada fungsi. Sila ambil perhatian bahawa walaupun segala-galanya di sini bukan tahap rendah sepenuhnya dari segi aliran kawalan, integer masih tidak membawa atribut "ditandatangani/tidak ditandatangani": bagaimana nombor itu bertindak bergantung pada kod operasi.

Secara umumnya, Binaryen menyediakan C-API mudah: anda membuat modul, dalam dirinya cipta ungkapan - unari, binari, blok daripada ungkapan lain, aliran kawalan, dsb. Kemudian anda mencipta fungsi dengan ungkapan sebagai badannya. Jika anda, seperti saya, mempunyai graf peralihan tahap rendah, komponen relooper akan membantu anda. Setakat yang saya faham, adalah mungkin untuk menggunakan kawalan peringkat tinggi aliran pelaksanaan dalam blok, selagi ia tidak melampaui sempadan blok - iaitu, adalah mungkin untuk membuat laluan pantas dalaman / perlahan laluan bercabang di dalam kod pemprosesan cache TLB terbina dalam, tetapi tidak mengganggu aliran kawalan "luaran" . Apabila anda membebaskan relooper, bloknya dibebaskan; apabila anda membebaskan modul, ungkapan, fungsi, dsb. yang diperuntukkan kepadanya hilang arena.

Walau bagaimanapun, jika anda ingin mentafsir kod dengan cepat tanpa penciptaan dan pemadaman contoh penterjemah yang tidak perlu, mungkin masuk akal untuk meletakkan logik ini ke dalam fail C++, dan dari sana terus menguruskan keseluruhan C++ API perpustakaan, memintas sedia- dibuat pembalut.

Jadi untuk menjana kod yang anda perlukan

// Π½Π°ΡΡ‚Ρ€ΠΎΠΈΡ‚ΡŒ Π³Π»ΠΎΠ±Π°Π»ΡŒΠ½Ρ‹Π΅ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ (ΠΌΠΎΠΆΠ½ΠΎ ΠΏΠΎΠΌΠ΅Π½ΡΡ‚ΡŒ ΠΏΠΎΡ‚ΠΎΠΌ)
BinaryenSetAPITracing(0);

BinaryenSetOptimizeLevel(3);
BinaryenSetShrinkLevel(2);

// ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ ΠΌΠΎΠ΄ΡƒΠ»ΡŒ
BinaryenModuleRef MODULE = BinaryenModuleCreate();

// ΠΎΠΏΠΈΡΠ°Ρ‚ΡŒ Ρ‚ΠΈΠΏΡ‹ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΉ (ΠΊΠ°ΠΊ создаваСмых, Ρ‚Π°ΠΊ ΠΈ Π²Ρ‹Π·Ρ‹Π²Π°Π΅ΠΌΡ‹Ρ…)
helper_type  BinaryenAddFunctionType(MODULE, "helper-func", BinaryenTypeInt32(), int32_helper_args, ARRAY_SIZE(int32_helper_args));
// (int23_helper_args ΠΏΡ€ΠΈΠΎΠ±^WΡΠΎΠ·Π΄Π°ΡŽΡ‚ΡΡ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎ)

// ΡΠΊΠΎΠ½ΡΡ‚Ρ€ΡƒΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ супСр-ΠΌΠ΅Π³Π° Π²Ρ‹Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅
// ... Π½Ρƒ Ρ‚ΡƒΡ‚ ΡƒΠΆ Π²Ρ‹ ΠΊΠ°ΠΊ-Π½ΠΈΠ±ΡƒΠ΄ΡŒ сами :)

// ΠΏΠΎΡ‚ΠΎΠΌ ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ
BinaryenAddFunction(MODULE, "tb_fun", tb_func_type, func_locals, FUNC_LOCALS_COUNT, expr);
BinaryenAddFunctionExport(MODULE, "tb_fun", "tb_fun");
...
BinaryenSetMemory(MODULE, (1 << 15) - 1, -1, NULL, NULL, NULL, NULL, NULL, 0, 0);
BinaryenAddMemoryImport(MODULE, NULL, "env", "memory", 0);
BinaryenAddTableImport(MODULE, NULL, "env", "tb_funcs");

// Π·Π°ΠΏΡ€ΠΎΡΠΈΡ‚ΡŒ Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΡŽ ΠΈ ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΡŽ ΠΏΡ€ΠΈ ΠΆΠ΅Π»Π°Π½ΠΈΠΈ
assert (BinaryenModuleValidate(MODULE));
BinaryenModuleOptimize(MODULE);

... jika saya terlupa apa-apa, maaf, ini hanya untuk mewakili skala, dan butirannya ada dalam dokumentasi.

Dan kini crack-fex-pex bermula, seperti ini:

static char buf[1 << 20];
BinaryenModuleOptimize(MODULE);
BinaryenSetMemory(MODULE, 0, -1, NULL, NULL, NULL, NULL, NULL, 0, 0);
int sz = BinaryenModuleWrite(MODULE, buf, sizeof(buf));
BinaryenModuleDispose(MODULE);
EM_ASM({
  var module = new WebAssembly.Module(new Uint8Array(wasmMemory.buffer, $0, $1));
  var fptr = $2;
  var instance = new WebAssembly.Instance(module, {
      'env': {
          'memory': wasmMemory,
          // ...
      }
  );
  // ΠΈ Π²ΠΎΡ‚ ΡƒΠΆΠ΅ Ρƒ вас Π΅ΡΡ‚ΡŒ instance!
}, buf, sz);

Untuk entah bagaimana menyambungkan dunia QEMU dan JS dan pada masa yang sama mengakses fungsi yang disusun dengan cepat, tatasusunan telah dibuat (jadual fungsi untuk diimport ke dalam pelancar), dan fungsi yang dijana diletakkan di sana. Untuk mengira indeks dengan cepat, indeks blok terjemahan perkataan sifar pada mulanya digunakan seperti itu, tetapi kemudian indeks yang dikira menggunakan formula ini mula sesuai dengan medan dalam struct TranslationBlock.

By the way, demo (kini dengan lesen keruh) hanya berfungsi dengan baik dalam Firefox. Pembangun Chrome adalah entah bagaimana tidak bersedia kepada fakta bahawa seseorang ingin mencipta lebih daripada seribu contoh modul WebAssembly, jadi mereka hanya memperuntukkan satu gigabait ruang alamat maya untuk setiap...

Itu sahaja buat masa ini. Mungkin akan ada artikel lain jika ada yang berminat. Iaitu, masih ada sekurang-kurangnya hanya menjadikan peranti blok berfungsi. Ia juga mungkin masuk akal untuk menjadikan penyusunan modul WebAssembly tidak segerak, seperti kebiasaan dalam dunia JS, kerana masih terdapat jurubahasa yang boleh melakukan semua ini sehingga modul asli sedia.

Akhirnya teka-teki: anda telah menyusun binari pada seni bina 32-bit, tetapi kod itu, melalui operasi memori, naik dari Binaryen, di suatu tempat pada timbunan, atau di tempat lain dalam 2 GB atas ruang alamat 32-bit. Masalahnya ialah dari sudut pandangan Binaryen ini mengakses alamat yang terhasil terlalu besar. Bagaimana untuk mengatasi ini?

Dengan cara admin

Saya tidak akhirnya menguji ini, tetapi pemikiran pertama saya ialah "Bagaimana jika saya memasang Linux 32-bit?" Kemudian bahagian atas ruang alamat akan diduduki oleh kernel. Satu-satunya soalan ialah berapa banyak yang akan diduduki: 1 atau 2 Gb.

Dengan cara pengaturcara (pilihan untuk pengamal)

Mari tiup gelembung di bahagian atas ruang alamat. Saya sendiri tidak faham mengapa ia berfungsi - di sana sudah mesti ada timbunan. Tetapi "kami adalah pengamal: semuanya berfungsi untuk kami, tetapi tiada siapa yang tahu mengapa..."

// 2gbubble.c
// Usage: LD_PRELOAD=2gbubble.so <program>

#include <sys/mman.h>
#include <assert.h>

void __attribute__((constructor)) constr(void)
{
  assert(MAP_FAILED != mmap(1u >> 31, (1u >> 31) - (1u >> 20), PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0));
}

... memang benar bahawa ia tidak serasi dengan Valgrind, tetapi, mujurlah, Valgrind sendiri sangat berkesan mendorong semua orang keluar dari sana :)

Mungkin seseorang akan memberikan penjelasan yang lebih baik tentang cara kod saya ini berfungsi...

Sumber: www.habr.com

Tambah komen