Apa yang ada di dalam file .wasm? Memperkenalkan wasm-decompile

Kami memiliki banyak kompiler dan alat lain yang dapat kami gunakan untuk membuat dan bekerja dengan file .wasm. Jumlah alat ini terus meningkat. Terkadang Anda perlu melihat file .wasm dan mencari tahu apa yang ada di dalamnya. Mungkin Anda adalah pengembang dari salah satu alat Wasm, atau mungkin Anda adalah seorang programmer yang menulis kode yang dirancang untuk dikonversi ke Wasm dan tertarik pada bagaimana tampilannya menjadi seperti apa kode yang akan berubah menjadi. Ketertarikan tersebut dapat dipicu, misalnya, dengan pertimbangan kinerja.



Masalahnya adalah bahwa file .wasm berisi kode tingkat rendah yang sangat mirip kode assembler nyata. Khususnya, tidak seperti, misalnya, JVM, semua struktur data dikompilasi ke dalam set operasi load / store, dan tidak menjadi sesuatu yang memiliki nama kelas dan bidang yang jelas. Kompiler, seperti LLVM, dapat mengubah kode input sedemikian rupa sehingga apa yang mereka dapatkan tidak terlihat dekat dengannya. 

Bagaimana dengan orang yang ingin, mengambil file .wasm, untuk mencari tahu apa yang terjadi di dalamnya?

Membongkar atau ... mendekompilasi?


Anda dapat menggunakan alat seperti wasm2wat untuk mengonversi file .wasm ke file .wat yang berisi representasi teks standar dari kode Wasm (ini adalah bagian dari toolkit WABT ). Hasil konversi ini sangat akurat, tetapi membaca kode yang dihasilkan tidak terlalu nyaman.

Di sini, misalnya, adalah fungsi sederhana yang ditulis dalam C:

typedef struct { float x, y, z; } vec3;

float dot(const vec3 *a, const vec3 *b) {
    return a->x * b->x +
           a->y * b->y +
           a->z * b->z;
}

Kode disimpan dalam file dot.c.

Kami menggunakan perintah berikut:

clang dot.c -c -target wasm32 -O2

Selanjutnya, untuk mengonversi apa yang terjadi pada file .wat, kami menerapkan perintah berikut:

wasm2wat -f dot.o

Inilah yang akan memberi kita:

(func $dot (type 0) (param i32 i32) (result f32)
  (f32.add
    (f32.add
      (f32.mul
        (f32.load
          (local.get 0))
        (f32.load
          (local.get 1)))
      (f32.mul
        (f32.load offset=4
          (local.get 0))
        (f32.load offset=4
          (local.get 1))))
    (f32.mul
      (f32.load offset=8
        (local.get 0))
      (f32.load offset=8
        (local.get 1))))))

Kode ini kecil, tetapi karena berbagai alasan, sangat sulit dibaca. Selain fakta bahwa ekspresi tidak digunakan di sini, dan fakta bahwa itu, secara keseluruhan, terlihat agak bertele-tele, tidak mudah untuk memahami struktur data yang diwakili dalam bentuk perintah untuk memuat data dari memori. Sekarang bayangkan Anda perlu menganalisis kode semacam itu dari ukuran yang jauh lebih besar. Analisis seperti itu akan menjadi tugas yang sangat sulit.

Mari kita coba, daripada menggunakan wasm2wat, jalankan perintah berikut:

wasm-decompile dot.o

Inilah yang akan dia berikan kepada kita:

function dot(a:{ a:float, b:float, c:float },
             b:{ a:float, b:float, c:float }):float {
  return a.a * b.a + a.b * b.b + a.c * b.c
}

Itu sudah terlihat jauh lebih baik. Selain menggunakan ekspresi yang mengingatkan pada bahasa pemrograman yang sudah Anda kenal, dekompiler mem-parsing perintah yang bertujuan untuk bekerja dengan memori dan mencoba untuk membuat ulang struktur data yang diwakili oleh perintah-perintah ini. Sistem kemudian membubuhi keterangan setiap variabel, yang digunakan sebagai penunjuk dengan deklarasi struktur "bawaan". Dekompiler tidak membuat deklarasi struktur bernama, karena tidak tahu apakah ada kesamaan antara struktur yang masing-masing menggunakan 3 nilai float.

Seperti yang Anda lihat, hasil dekompilasi ternyata lebih dapat dipahami daripada hasil pembongkaran.

Bahasa apa kode yang ditulis oleh decompiler yang ditulis?


Alat wasm-decompile mengeluarkan kode, mencoba membuat kode ini terlihat seperti semacam bahasa pemrograman "rata-rata". Pada saat yang sama, alat ini mencoba untuk tidak pergi terlalu jauh dari Wasm.

Tujuan pertama dari decompiler-wasm adalah membuat kode yang dapat dibaca. Yaitu - kode yang memungkinkan pembaca untuk dengan mudah memahami apa yang terjadi dalam file .wasm yang di-decompile. Tujuan kedua alat ini adalah untuk memberikan representasi paling akurat dari file .wasm, dengan menghasilkan kode yang sepenuhnya mewakili apa yang terjadi dalam file sumber. Jelas, tujuan-tujuan ini jauh dari selalu dalam persetujuan yang baik satu sama lain.

Apa output wasm-decompiler pada awalnya tidak dipahami sebagai kode yang mewakili beberapa bahasa pemrograman nyata. Saat ini tidak ada cara untuk mengkompilasi kode ini di Wasm.

Perintah untuk memuat dan menyimpan data


Seperti ditunjukkan di atas, wasm-decompile mencari perintah load dan save yang terkait dengan pointer tertentu. Jika perintah ini membentuk urutan berkelanjutan, dekompiler menampilkan salah satu deklarasi struktur data "bawaan".

Jika tidak semua "bidang" diakses, dekompiler tidak dapat secara andal membedakan struktur dari urutan operasi tertentu dengan bekerja dengan memori. Dalam kasus ini, wasm-decompile menggunakan opsi fallback, menggunakan tipe yang lebih sederhana seperti float_ptr(jika tipenya sama), atau, dalam kasus terburuk, menghasilkan kode yang menggambarkan cara bekerja dengan array, seperti o[2]:int. Kode semacam itu memberi tahu kita bahwa itu omenunjuk ke elemen-elemen tipe int, dan kita beralih ke elemen ketiga.

Situasi terakhir ini muncul jauh lebih sering daripada yang Anda kira, karena fungsi Wasm lokal lebih fokus pada penggunaan register daripada variabel. Akibatnya, dalam kode yang dioptimalkan, pointer yang sama dapat digunakan untuk bekerja dengan objek yang sama sekali tidak terkait.

Dekompiler mencari pendekatan cerdas untuk pengindeksan dan mampu mengidentifikasi pola seperti (base + (index << 2))[0]:int. Sumber pola tersebut adalah operasi pengindeksan biasa untuk C, seperti di base[index]mana ia basemenunjuk ke tipe 4-byte. Ini sangat umum dalam kode, karena Wasm, dalam memuat dan menyimpan perintah data, hanya mendukung offset yang didefinisikan sebagai konstanta. Dalam kode yang dihasilkan oleh wasm-decompile, konstruksi seperti itu dikonversikan menjadi tipe base[index]:int.

Selain itu, dekompiler tahu kapan alamat absolut menunjuk ke bagian data.

Kontrol aliran program


Jika kita berbicara tentang konstruk kontrol, yang paling terkenal di antara mereka adalah konstruk if-then Wasm, yang berubah menjadi if (cond) { A } else { B }, dengan tambahan fakta bahwa konstruk seperti itu dalam Wasm dapat mengembalikan nilai, sehingga juga dapat mewakili operator ternary, seperti cond ? A : B, yang ada di beberapa bahasa.

Struktur kontrol Wasm lainnya berbasis blok blockdan loop, serta transisi br, br_ifdan br_table. Dekompiler mencoba untuk tetap sedekat mungkin dengan struktur ini. Dia tidak berusaha untuk membuat ulang sementara / untuk / mengganti konstruksi yang bisa berfungsi sebagai dasar untuk mereka. Faktanya adalah bahwa pendekatan ini menunjukkan dirinya lebih baik ketika memproses kode yang dioptimalkan. Misalnya, desain konvensionalloop mungkin melihat kode yang dikembalikan oleh wasm-decompile seperti ini:

loop A {
  //    .
  if (cond) continue A;
}

Berikut Aadalah label yang memungkinkan Anda untuk membangun struktur bersarang satu sama lain loop. Fakta bahwa ada perintah ifdan continuedigunakan untuk mengendalikan siklus mungkin terlihat agak asing untuk sementara loop, tetapi mereka sesuai dengan konstruksi Wasm br_if.

Blok disusun dengan cara yang sama, tetapi di sini kondisinya di awal, dan bukan di akhir:

block {
  if (cond) break;
  //    .
}

Hasil dekompilasi konstruk if-then ditunjukkan di sini. Dalam versi dekompiler yang akan datang, mungkin, alih-alih kode tersebut, jika memungkinkan, konstruk if-then yang lebih akrab akan dibentuk.

Alat Wasm yang paling tidak biasa digunakan untuk mengontrol aliran suatu program adalah br_table. Alat ini adalah semacam pernyataan switch, kecuali bahwa ia menggunakan blok inline. Semua ini menyulitkan pembacaan kode. Dekompiler menyederhanakan struktur struktur seperti itu, berusaha membuat persepsi mereka sedikit lebih mudah:

br_table[A, B, C, ..D](a);
label A:
return 0;
label B:
return 1;
label C:
return 2;
label D:

Ini mengingatkan penggunaan switchuntuk analisis aketika opsi default adalah D.

Fitur menarik lainnya


Berikut adalah beberapa fitur lain dari wasm-decompile:

  • , . C++-.
  • , , , . . , .
  • .
  • Wasm-, . , wasm-decompile , , , .
  • , ( , C- ).

 


Mengurai kode Wasm adalah tugas yang jauh lebih rumit daripada, misalnya, mendekompilasi kode byte JVM.

Bytecode tidak dioptimalkan, yaitu mereproduksi struktur kode sumber dengan cukup akurat. Pada saat yang sama, terlepas dari kenyataan bahwa kode tersebut mungkin tidak mengandung nama asli, bytecode menggunakan referensi ke kelas unik, dan bukan ke area memori.

Tidak seperti bytecode JVM, kode yang masuk ke file .wasm sangat dioptimalkan oleh LLVM. Akibatnya, kode tersebut sering kehilangan sebagian besar struktur aslinya. Kode keluaran sangat berbeda dari apa yang akan ditulis oleh programmer. Ini sangat menyulitkan tugas mendekompilasi kode Wasm dengan output hasil yang dapat membawa manfaat nyata bagi programmer. Namun, ini tidak berarti bahwa kita tidak harus berusaha untuk menyelesaikan masalah ini!

Ringkasan


Jika Anda tertarik dengan topik dekompilasi kode Wasm, maka mungkin cara terbaik untuk memahami topik ini adalah dengan mengambil dan mendekompilasi proyek .wasm Anda sendiri! Selain itu, di sini Anda dapat menemukan panduan yang lebih terperinci tentang wasm-decompile. Kode dekompiler dapat ditemukan dalam file-file repositori ini , nama-nama yang dimulai dengan decompile(jika Anda mau, bergabunglah dengan pekerjaan pada dekompiler). Di sini Anda dapat menemukan tes yang menunjukkan contoh tambahan perbedaan antara file .wat dan hasil dekompilasi.

Dan dengan alat apa Anda meneliti file .wasm?

, , iPhone. , .


All Articles