QEMU.js: sekarang serius dan dengan WASM

Suatu ketika saya memutuskan untuk bersenang-senang membuktikan reversibilitas proses dan pelajari cara membuat JavaScript (lebih tepatnya, Asm.js) dari kode mesin. QEMU dipilih untuk percobaan tersebut, dan beberapa waktu kemudian sebuah artikel ditulis di Habr. Dalam komentar saya disarankan untuk membuat ulang proyek di WebAssembly, dan bahkan berhenti sendiri hampir selesai Saya entah bagaimana tidak menginginkan proyek itu... Pekerjaan itu berlangsung, tetapi sangat lambat, dan sekarang, baru-baru ini artikel itu muncul komentar pada topik “Jadi, bagaimana semuanya berakhir?” Menanggapi jawaban rinci saya, saya mendengar "Ini terdengar seperti sebuah artikel." Nah, kalau bisa, akan ada artikelnya. Mungkin seseorang akan merasakan manfaatnya. Dari situ pembaca akan mempelajari beberapa fakta tentang desain backend pembuatan kode QEMU, serta cara menulis kompiler Just-in-Time untuk aplikasi web.

tugas

Karena saya telah mempelajari cara “entah bagaimana” mem-porting QEMU ke JavaScript, kali ini diputuskan untuk melakukannya dengan bijak dan tidak mengulangi kesalahan lama.

Kesalahan nomor satu: cabang dari titik pelepasan

Kesalahan pertama saya adalah mem-fork versi saya dari versi upstream 2.4.1. Bagi saya itu ide yang bagus: jika rilis titik ada, maka mungkin lebih stabil daripada 2.4 sederhana, dan terlebih lagi cabangnya master. Dan karena saya berencana menambahkan cukup banyak bug saya sendiri, saya tidak membutuhkan bug orang lain sama sekali. Mungkin itulah yang terjadi. Tapi ada satu hal: QEMU tidak tinggal diam, dan pada titik tertentu mereka bahkan mengumumkan optimasi kode yang dihasilkan sebesar 10 persen. “Ya, sekarang saya akan membeku,” pikir saya dan putus asa. Di sini kita perlu membuat penyimpangan: karena sifat QEMU.js berulir tunggal dan fakta bahwa QEMU asli tidak berarti tidak adanya multi-threading (yaitu, kemampuan untuk mengoperasikan beberapa jalur kode yang tidak terkait secara bersamaan, dan bukan hanya "gunakan semua kernel") yang penting untuk itu, fungsi utama thread saya harus "mematikannya" agar dapat memanggil dari luar. Hal ini menimbulkan beberapa masalah alami selama merger. Namun faktanya ada beberapa perubahan dari cabang master, yang dengannya saya mencoba menggabungkan kode saya, juga dipilih pada rilis titik (dan karena itu di cabang saya) juga mungkin tidak akan menambah kenyamanan.

Secara umum, saya memutuskan bahwa masih masuk akal untuk membuang prototipe, membongkarnya menjadi beberapa bagian, dan membuat versi baru dari awal berdasarkan sesuatu yang lebih segar dan sekarang dari master.

Kesalahan nomor dua: metodologi TLP

Intinya, ini bukan kesalahan, secara umum, ini hanya fitur pembuatan proyek dalam kondisi kesalahpahaman total baik tentang “ke mana dan bagaimana cara pindah?” dan secara umum “akankah kita sampai di sana?” Dalam kondisi ini pemrograman yang kikuk adalah pilihan yang bisa dibenarkan, tapi, tentu saja, saya tidak ingin mengulanginya jika tidak perlu. Kali ini saya ingin melakukannya dengan bijak: komitmen atom, perubahan kode secara sadar (dan bukan "merangkai karakter acak hingga dikompilasi (dengan peringatan)", seperti yang pernah dikatakan Linus Torvalds tentang seseorang, menurut Wikiquote), dll.

Kesalahan nomor tiga: masuk ke dalam air tanpa mengetahui arungannya

Saya masih belum sepenuhnya menghilangkan hal ini, tetapi sekarang saya telah memutuskan untuk tidak mengikuti jalan yang paling sedikit perlawanannya sama sekali, dan melakukannya “sebagai orang dewasa”, yaitu, menulis backend TCG saya dari awal, agar tidak harus mengatakan nanti, "Ya, ini tentu saja, perlahan, tapi saya tidak bisa mengendalikan semuanya - begitulah cara TCI ditulis..." Terlebih lagi, ini awalnya tampak seperti solusi yang jelas Saya menghasilkan kode biner. Seperti yang mereka katakan, “Ghent berkumpulу, tapi bukan yang itu”: kodenya, tentu saja, biner, tetapi kontrol tidak bisa begitu saja ditransfer ke kode tersebut - kode tersebut harus secara eksplisit dimasukkan ke dalam browser untuk dikompilasi, menghasilkan objek tertentu dari dunia JS, yang masih perlu untuk disimpan di suatu tempat. Namun, pada arsitektur RISC normal, sejauh yang saya pahami, situasi yang umum adalah kebutuhan untuk secara eksplisit mengatur ulang cache instruksi untuk kode yang dibuat ulang - jika ini bukan yang kita perlukan, maka, bagaimanapun, itu sudah dekat. Selain itu, dari upaya terakhir saya, saya mengetahui bahwa kontrol tampaknya tidak ditransfer ke tengah blok terjemahan, jadi kita tidak benar-benar memerlukan bytecode yang diinterpretasikan dari offset apa pun, dan kita cukup membuatnya dari fungsi di TB .

Mereka datang dan menendang

Meskipun saya mulai menulis ulang kodenya pada bulan Juli, sebuah keajaiban muncul tanpa disadari: biasanya surat dari GitHub tiba sebagai pemberitahuan tentang respons terhadap Permintaan Masalah dan Tarik, namun di sini, tiba-tiba sebutkan di thread Binaryen sebagai backend qemu dalam konteksnya, “Dia melakukan hal seperti itu, mungkin dia akan mengatakan sesuatu.” Kami berbicara tentang penggunaan perpustakaan terkait Emscripten biner untuk membuat WASM JIT. Ya, saya katakan Anda memiliki lisensi Apache 2.0 di sana, dan QEMU secara keseluruhan didistribusikan di bawah GPLv2, dan keduanya tidak terlalu kompatibel. Tiba-tiba ternyata lisensinya bisa memperbaikinya entah bagaimana (Saya tidak tahu: mungkin mengubahnya, mungkin lisensi ganda, mungkin yang lain...). Hal ini tentu saja membuat saya senang, karena saat itu saya sudah mencermatinya format biner WebAssembly, dan saya merasa sedih dan tidak bisa dimengerti. Ada juga perpustakaan yang akan menggunakan blok dasar dengan grafik transisi, menghasilkan bytecode, dan bahkan menjalankannya di interpreter itu sendiri, jika perlu.

Lalu masih ada lagi sebuah surat di milis QEMU, namun ini lebih pada pertanyaan, “Lagipula, siapa yang membutuhkannya?” Dan itu benar tiba-tiba, ternyata itu perlu. Minimal, Anda dapat mengumpulkan kemungkinan penggunaan berikut, jika ini bekerja lebih atau kurang cepat:

  • meluncurkan sesuatu yang mendidik tanpa instalasi sama sekali
  • virtualisasi di iOS, yang menurut rumor, satu-satunya aplikasi yang berhak membuat kode dengan cepat adalah mesin JS (apakah ini benar?)
  • demonstrasi mini-OS - disket tunggal, bawaan, semua jenis firmware, dll...

Fitur Waktu Proses Browser

Seperti yang saya katakan, QEMU terkait dengan multithreading, tetapi browser tidak memilikinya. Ya, tidak... Awalnya tidak ada sama sekali, kemudian muncul WebWorkers - sejauh yang saya mengerti, ini adalah multithreading berdasarkan penyampaian pesan tanpa variabel bersama. Tentu saja, hal ini menimbulkan masalah yang signifikan saat mem-porting kode yang ada berdasarkan model memori bersama. Kemudian, atas tekanan masyarakat, hal itu juga dilaksanakan atas nama SharedArrayBuffers. Itu diperkenalkan secara bertahap, mereka merayakan peluncurannya di browser yang berbeda, lalu mereka merayakan Tahun Baru, dan kemudian Meltdown... Setelah itu mereka sampai pada kesimpulan bahwa pengukuran waktu kasar atau kasar, tetapi dengan bantuan memori bersama dan a thread menambah penghitung, semuanya sama saja itu akan berhasil dengan cukup akurat. Jadi kami menonaktifkan multithreading dengan memori bersama. Tampaknya mereka kemudian mengaktifkannya kembali, tetapi, seperti yang terlihat dari percobaan pertama, ada kehidupan tanpanya, dan jika demikian, kami akan mencoba melakukannya tanpa bergantung pada multithreading.

Fitur kedua adalah ketidakmungkinan manipulasi tingkat rendah dengan tumpukan: Anda tidak bisa begitu saja mengambil, menyimpan konteks saat ini dan beralih ke yang baru dengan tumpukan baru. Tumpukan panggilan dikelola oleh mesin virtual JS. Tampaknya, apa masalahnya, karena kami masih memutuskan untuk mengelola aliran sebelumnya sepenuhnya secara manual? Faktanya adalah blok I/O di QEMU diimplementasikan melalui coroutine, dan di sinilah manipulasi tumpukan tingkat rendah akan berguna. Untungnya, Emscipten sudah berisi mekanisme untuk operasi asynchronous, bahkan ada dua: Asinkronisasi и juru bahasa. Yang pertama berfungsi melalui pembengkakan yang signifikan pada kode JavaScript yang dihasilkan dan tidak lagi didukung. Yang kedua adalah "cara yang benar" saat ini dan bekerja melalui pembuatan bytecode untuk penerjemah asli. Tentu saja, ini bekerja dengan lambat, tetapi kodenya tidak membengkak. Benar, dukungan coroutine untuk mekanisme ini harus dikontribusikan secara independen (sudah ada coroutine yang ditulis untuk Asyncify dan ada implementasi API yang kurang lebih sama untuk Emterpreter, Anda hanya perlu menghubungkannya).

Saat ini, saya belum berhasil membagi kode menjadi satu yang dikompilasi di WASM dan diinterpretasikan menggunakan Emterpreter, jadi perangkat blok belum berfungsi (lihat seri berikutnya, seperti yang mereka katakan...). Artinya, pada akhirnya Anda akan mendapatkan sesuatu yang lucu dan berlapis-lapis:

  • menafsirkan blok I/O. Nah, apakah Anda benar-benar mengharapkan NVMe yang ditiru dengan performa asli? 🙂
  • kode QEMU utama yang dikompilasi secara statis (penerjemah, perangkat emulasi lainnya, dll.)
  • kode tamu yang dikompilasi secara dinamis ke dalam WASM

Fitur sumber QEMU

Seperti yang mungkin sudah Anda duga, kode untuk meniru arsitektur tamu dan kode untuk menghasilkan instruksi mesin host dipisahkan di QEMU. Faktanya, ini sedikit lebih rumit:

  • ada arsitektur tamu
  • ada akselerator, yaitu KVM untuk virtualisasi perangkat keras di Linux (untuk sistem tamu dan host yang kompatibel satu sama lain), TCG untuk pembuatan kode JIT di mana saja. Dimulai dengan QEMU 2.9, dukungan untuk standar virtualisasi perangkat keras HAXM di Windows muncul (detailnya)
  • jika TCG digunakan dan bukan virtualisasi perangkat keras, maka TCG memiliki dukungan pembuatan kode terpisah untuk setiap arsitektur host, serta untuk penerjemah universal
  • ... dan seputar semua ini - periferal yang ditiru, antarmuka pengguna, migrasi, pemutaran ulang rekaman, dll.

Ngomong-ngomong, tahukah Anda: QEMU tidak hanya dapat meniru seluruh komputer, tetapi juga prosesor untuk proses pengguna terpisah di kernel host, yang digunakan, misalnya, oleh fuzzer AFL untuk instrumentasi biner. Mungkin seseorang ingin mem-porting mode operasi QEMU ini ke JS? 😉

Seperti kebanyakan perangkat lunak gratis yang sudah lama ada, QEMU dibangun melalui panggilan configure и make. Misalkan Anda memutuskan untuk menambahkan sesuatu: backend TCG, implementasi thread, dan hal lainnya. Jangan terburu-buru merasa senang/ngeri (garis bawahi seperlunya) saat akan berkomunikasi dengan Autoconf - faktanya, configure QEMU tampaknya ditulis sendiri dan tidak dihasilkan dari apa pun.

WebAssembly

Jadi apa yang disebut WebAssembly (alias WASM)? Ini adalah pengganti Asm.js, tidak lagi berpura-pura menjadi kode JavaScript yang valid. Sebaliknya, ini murni biner dan dioptimalkan, dan bahkan menulis bilangan bulat ke dalamnya tidaklah mudah: untuk kekompakan, ia disimpan dalam format LEB128.

Anda mungkin pernah mendengar tentang algoritme perulangan ulang untuk Asm.js - ini adalah pemulihan instruksi kontrol aliran "tingkat tinggi" (yaitu, perulangan jika-maka-lainnya, dll.), yang merupakan mesin JS yang dirancang, dari LLVM IR tingkat rendah, lebih dekat dengan kode mesin yang dieksekusi oleh prosesor. Tentu saja, representasi perantara QEMU lebih dekat ke yang kedua. Tampaknya ini dia, bytecode, akhir dari siksaan... Dan kemudian ada blok, if-then-else dan loop!..

Dan inilah alasan lain mengapa Binaryen berguna: ia secara alami dapat menerima blok tingkat tinggi yang mendekati apa yang akan disimpan di WASM. Namun ia juga dapat menghasilkan kode dari grafik blok-blok dasar dan transisi di antara blok-blok tersebut. Ya, saya sudah mengatakan bahwa itu menyembunyikan format penyimpanan WebAssembly di belakang C/C++ API yang nyaman.

TCG (Pembuat Kode Kecil)

TCG awalnya backend untuk compiler C. Kemudian, tampaknya, ia tidak dapat bertahan dalam persaingan dengan GCC, namun pada akhirnya ia menemukan tempatnya di QEMU sebagai mekanisme pembuatan kode untuk platform host. Ada juga backend TCG yang menghasilkan beberapa bytecode abstrak yang langsung dieksekusi oleh penerjemah, tetapi saya memutuskan untuk tidak menggunakannya kali ini. Namun, faktanya di QEMU sudah dimungkinkan untuk mengaktifkan transisi ke TB yang dihasilkan melalui fungsi tersebut tcg_qemu_tb_exec, ternyata sangat bermanfaat bagi saya.

Untuk menambahkan backend TCG baru ke QEMU, Anda perlu membuat subdirektori tcg/<имя архитектуры> (pada kasus ini, tcg/binaryen), dan berisi dua file: tcg-target.h и tcg-target.inc.c и menentukan semua tentang configure. Anda dapat meletakkan file lain di sana, tetapi, seperti yang dapat Anda tebak dari nama keduanya, keduanya akan disertakan di suatu tempat: satu sebagai file header biasa (termasuk dalam tcg/tcg.h, dan file itu sudah ada di file lain di direktori tcg, accel dan tidak hanya), yang lainnya - hanya sebagai cuplikan kode tcg/tcg.c, tetapi ia memiliki akses ke fungsi statisnya.

Memutuskan bahwa saya akan menghabiskan terlalu banyak waktu untuk menyelidiki secara mendetail cara kerjanya, saya hanya menyalin “kerangka” kedua file ini dari implementasi backend lain, dan dengan jujur ​​​​menunjukkannya di header lisensi.

berkas tcg-target.h terutama berisi pengaturan dalam formulir #define-S:

  • berapa banyak register dan berapa lebar yang ada pada arsitektur target (kami memiliki sebanyak yang kami inginkan, sebanyak yang kami inginkan - pertanyaannya lebih lanjut tentang apa yang akan dihasilkan menjadi kode yang lebih efisien oleh browser pada arsitektur “target sepenuhnya” ...)
  • penyelarasan instruksi host: pada x86, dan bahkan di TCI, instruksi tidak selaras sama sekali, tapi saya tidak akan memasukkan instruksi ke dalam buffer kode sama sekali, tetapi pointer ke struktur perpustakaan Binaryen, jadi saya akan mengatakan: 4 byte
  • instruksi opsional apa yang dapat dihasilkan oleh backend - kami menyertakan semua yang kami temukan di Binaryen, biarkan akselerator memecah sisanya menjadi lebih sederhana
  • Berapa perkiraan ukuran cache TLB yang diminta oleh backend. Faktanya adalah bahwa di QEMU semuanya serius: meskipun ada fungsi pembantu yang melakukan pemuatan/penyimpanan dengan mempertimbangkan MMU tamu (di mana kita tanpanya sekarang?), mereka menyimpan cache terjemahannya dalam bentuk struktur, pemrosesan yang mudah untuk ditanamkan langsung ke blok siaran. Pertanyaannya adalah offset apa dalam struktur ini yang paling efisien diproses oleh serangkaian perintah kecil dan cepat?
  • di sini Anda dapat mengubah tujuan dari satu atau dua register yang dicadangkan, mengaktifkan panggilan TB melalui suatu fungsi dan secara opsional menjelaskan beberapa register kecil inline-fungsi seperti flush_icache_range (tapi ini bukan kasus kami)

berkas tcg-target.inc.c, tentu saja, biasanya berukuran jauh lebih besar dan berisi beberapa fungsi wajib:

  • inisialisasi, termasuk pembatasan instruksi mana yang dapat beroperasi pada operan mana. Secara terang-terangan disalin oleh saya dari backend lain
  • fungsi yang mengambil satu instruksi bytecode internal
  • Anda juga dapat meletakkan fungsi tambahan di sini, dan Anda juga dapat menggunakan fungsi statis dari tcg/tcg.c

Bagi saya sendiri, saya memilih strategi berikut: di kata pertama dari blok terjemahan berikutnya, saya menuliskan empat petunjuk: tanda awal (nilai tertentu di sekitarnya 0xFFFFFFFF, yang menentukan status TB saat ini), konteks, modul yang dihasilkan, dan angka ajaib untuk debugging. Mula-mula tanda itu dipasang 0xFFFFFFFF - nDimana n - angka positif kecil, dan setiap kali dieksekusi melalui penerjemah, angka itu bertambah 1. Ketika sudah tercapai 0xFFFFFFFE, kompilasi berlangsung, modul disimpan dalam tabel fungsi, diimpor ke "peluncur" kecil, tempat eksekusi dimulai tcg_qemu_tb_exec, dan modul telah dihapus dari memori QEMU.

Untuk memparafrasekan lagu klasik, “Kruk, seberapa banyak yang terjalin dalam suara ini untuk hati orang yang proger…”. Namun, ingatannya bocor entah ke mana. Selain itu, memorinya dikelola oleh QEMU! Saya memiliki kode yang, ketika menulis instruksi berikutnya (yaitu, sebuah penunjuk), menghapus kode yang tautannya ada di tempat ini sebelumnya, tetapi ini tidak membantu. Sebenarnya, dalam kasus paling sederhana, QEMU mengalokasikan memori saat startup dan menulis kode yang dihasilkan di sana. Ketika buffer habis, kode akan dibuang dan kode berikutnya mulai ditulis sebagai gantinya.

Setelah mempelajari kodenya, saya menyadari bahwa trik dengan angka ajaib memungkinkan saya untuk tidak gagal dalam penghancuran heap dengan membebaskan sesuatu yang salah pada buffer yang tidak diinisialisasi pada lintasan pertama. Tapi siapa yang menulis ulang buffer untuk melewati fungsi saya nanti? Seperti yang disarankan oleh pengembang Emscripten, ketika saya mengalami masalah, saya mem-porting kode yang dihasilkan kembali ke aplikasi asli, mengatur Mozilla Record-Replay di atasnya... Secara umum, pada akhirnya saya menyadari hal sederhana: untuk setiap blok, A struct TranslationBlock dengan uraiannya. Coba tebak... Betul, tepat sebelum blok tepat di buffer. Menyadari hal ini, saya memutuskan untuk berhenti menggunakan kruk (setidaknya beberapa), dan membuang angka ajaibnya, dan memindahkan kata-kata yang tersisa ke struct TranslationBlock, membuat daftar tertaut tunggal yang dapat dilintasi dengan cepat ketika cache terjemahan disetel ulang, dan mengosongkan memori.

Beberapa kruk tetap ada: misalnya, penunjuk yang ditandai di buffer kode - beberapa di antaranya sederhana BinaryenExpressionRef, yaitu, mereka melihat ekspresi yang perlu dimasukkan secara linier ke dalam blok dasar yang dihasilkan, sebagian adalah kondisi transisi antar BB, sebagian adalah ke mana harus pergi. Nah, untuk Relooper sudah disiapkan blok-blok yang perlu disambungkan sesuai kondisi. Untuk membedakannya, digunakan asumsi bahwa semuanya disejajarkan setidaknya empat byte, sehingga Anda dapat dengan aman menggunakan dua bit paling tidak signifikan untuk label, Anda hanya perlu ingat untuk menghapusnya jika perlu. Omong-omong, label tersebut sudah digunakan di QEMU untuk menunjukkan alasan keluarnya loop TCG.

Menggunakan Bineren

Modul di WebAssembly berisi fungsi, yang masing-masing berisi badan, yang merupakan ekspresi. Ekspresi adalah operasi uner dan biner, blok yang terdiri dari daftar ekspresi lain, aliran kontrol, dll. Seperti yang sudah saya katakan, aliran kontrol di sini diatur persis sebagai cabang tingkat tinggi, loop, pemanggilan fungsi, dll. Argumen ke fungsi tidak diteruskan di stack, tetapi secara eksplisit, seperti di JS. Ada juga variabel global, tapi saya belum menggunakannya, jadi saya tidak akan memberi tahu Anda tentangnya.

Fungsi juga memiliki variabel lokal, diberi nomor dari nol, dengan tipe: int32/int64/float/double. Dalam hal ini, n variabel lokal pertama adalah argumen yang diteruskan ke fungsi tersebut. Harap dicatat bahwa meskipun semua yang ada di sini tidak sepenuhnya tingkat rendah dalam hal aliran kontrol, bilangan bulat tetap tidak membawa atribut “signed/unsigned”: bagaimana perilaku bilangan bergantung pada kode operasi.

Secara umum, Binaryen menyediakan C-API sederhana: Anda membuat modul, dalam dirinya membuat ekspresi - unary, biner, blok dari ekspresi lain, aliran kontrol, dll. Kemudian Anda membuat fungsi dengan ekspresi sebagai tubuhnya. Jika Anda, seperti saya, memiliki grafik transisi tingkat rendah, komponen relooper akan membantu Anda. Sejauh yang saya pahami, adalah mungkin untuk menggunakan kontrol tingkat tinggi atas aliran eksekusi dalam sebuah blok, selama tidak melampaui batas-batas blok - yaitu, dimungkinkan untuk membuat jalur cepat / lambat internal jalur bercabang di dalam kode pemrosesan cache TLB bawaan, tetapi tidak mengganggu aliran kontrol "eksternal". Ketika Anda membebaskan relooper, blok-bloknya dibebaskan; ketika Anda membebaskan sebuah modul, ekspresi, fungsi, dll. yang dialokasikan padanya menghilang arena.

Namun, jika Anda ingin menafsirkan kode dengan cepat tanpa membuat dan menghapus instance penerjemah yang tidak perlu, mungkin masuk akal untuk memasukkan logika ini ke dalam file C++, dan dari sana langsung mengelola seluruh C++ API perpustakaan, melewati siap- membuat pembungkus.

Jadi untuk menghasilkan kode yang Anda butuhkan

// настроить глобальные параметры (можно поменять потом)
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 lupa, maaf, ini hanya untuk mewakili skala, dan detailnya ada di dokumentasi.

Dan sekarang crack-fex-pex dimulai, kira-kira 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 menghubungkan dunia QEMU dan JS dan pada saat yang sama mengakses fungsi yang dikompilasi dengan cepat, sebuah array dibuat (tabel fungsi untuk diimpor ke peluncur), dan fungsi yang dihasilkan ditempatkan di sana. Untuk menghitung indeks dengan cepat, indeks blok terjemahan nol kata pada awalnya digunakan, tetapi kemudian indeks yang dihitung menggunakan rumus ini mulai dimasukkan ke dalam bidang di struct TranslationBlock.

Kebetulan, demonstrasi (saat ini dengan lisensi yang suram) hanya berfungsi dengan baik di Firefox. Pengembang Chrome adalah entah kenapa belum siap fakta bahwa seseorang ingin membuat lebih dari seribu contoh modul WebAssembly, jadi mereka hanya mengalokasikan satu gigabyte ruang alamat virtual untuk masing-masing...

Itu saja untuk saat ini. Mungkin akan ada artikel lain jika ada yang berminat. Yaitu, setidaknya masih ada hanya membuat perangkat blok berfungsi. Mungkin juga masuk akal untuk membuat kompilasi modul WebAssembly secara asinkron, seperti biasa di dunia JS, karena masih ada juru bahasa yang dapat melakukan semua ini hingga modul asli siap.

Akhirnya sebuah teka-teki: Anda telah mengkompilasi biner pada arsitektur 32-bit, tetapi kode tersebut, melalui operasi memori, naik dari Binaryen, di suatu tempat di tumpukan, atau di tempat lain di 2 GB atas ruang alamat 32-bit. Masalahnya adalah dari sudut pandang Binaryen, pengaksesan alamat yang dihasilkan terlalu besar. Bagaimana cara menyiasatinya?

Dengan cara admin

Saya tidak mengujinya, tetapi pikiran pertama saya adalah “Bagaimana jika saya menginstal Linux 32-bit?” Kemudian bagian atas ruang alamat akan ditempati oleh kernel. Satu-satunya pertanyaan adalah berapa banyak yang akan ditempati: 1 atau 2 Gb.

Dengan cara seorang programmer (pilihan untuk praktisi)

Mari kita tiup gelembung di bagian atas ruang alamat. Saya sendiri tidak mengerti mengapa ini berhasil - di sana sudah pasti ada tumpukan. Namun “kami adalah praktisi: segala sesuatunya berhasil bagi kami, namun tidak seorang pun mengetahui alasannya…”

// 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 itu tidak kompatibel dengan Valgrind, tapi untungnya, Valgrind sendiri sangat efektif mendorong semua orang keluar dari sana :)

Mungkin seseorang akan memberikan penjelasan yang lebih baik tentang cara kerja kode saya ini...

Sumber: www.habr.com

Tambah komentar