UP | HOME

rust 的 wasm ABI

應該有不少人知道我最近在逆向 rust 的 wasmabi,解析它會回傳什麼,以下是一些紀錄

1. 字串、 Vec 與結構

這三者的邏輯都是結構的邏輯,通常是按順序來,除非有 padding 或是 align。比如說 2022-11-02 版的 Rust 會把字串跟 Vec 都變成一個 i32 三元組 (i32, i32, i32) ,且三個欄位的用途分別是

  1. 位址
  2. capability
  3. length

問題是這不是可以相信的內容,因為 Rust 從來都不保證二進位的相容性。在 2022-12-06 版中,雖然依然是一個三元組,但語意變成

  1. capability
  2. 位址
  3. length

有趣的是 VecString 共用這樣的結構,在 FFI: interoperability with foreign code 中可以找到答案

Vectors and strings share the same basic memory layout, and utilities are available in the vec and str modules for working with C APIs. However, strings are not terminated with \0. If you need a NUL-terminated string for interoperability with C, you should use the CString type in the std::ffi module.

特別談論這點,是因為他們是內建的,其他結構大可以採用 #[repr(C)] 迴避沒有穩定二進位介面的問題。但對我來說這恰恰就是破壞使用者體驗的部分,雖然 CString 是穩定的,卻不是對使用者來說好用的型別。可以直接想到的方案基本上都需要額外的型別轉換,只為了讓介面穩定

1.1. Vec

Vec 還因為是間接的,解開第一層三元組得到資料( u8 的序列)之後還要再轉換一次內部資料(根據是 u8 的幾倍對這個序列分割操作),弄得非常麻煩

2. enum

enum 可以說是整件事最麻煩也最難迴避的部分。當然就像前面說過的,用 #[repr(C, u8)] 可以處理自訂的型別,問題是很多重要的型別如 Result<T, E>Option<T> 根本就不是你訂的啊!enum 原始的編碼其實還蠻簡單的,就是用數字標記是第幾個建構子,舉例來說 Option<i32>

  1. Some(3) 會被表示成 (1, 3)
  2. None 會被表示成 (0, _) ,這裡 _ 會是一個隨意的記憶體值

而這是因為 Option<T> 的定義是

enum Option<T> {
    None,
    Some(T)
}

你可能會想,按照這個邏輯,因為 Result<T, E> 的定義如下

enum Result<T, E> {
    Ok(T), // 0, T
    Err(E)
}

Ok(T) 就是 (0, T) ,而 Err(E) 就是 (1, E) 了吧!這件事半對半不對,要是你說的是 Result<i32, i32> ,上面的說法是對的。

但要是目標是是 Result<i32, String> 呢?你可能會覺得這個問題很瞎,難道不是根據 i32String (在 wasmabi 中是 (i32, i32, i32) )小,所以應該是 i32 的標記加上 (i32, i32, i32) 得到 (i32, i32, i32, i32) 嗎?然而你會拿到 (i32, i32, i32)

怎麼會這樣?這是因為 rust 覺得位址不會是 0 啊各位,根據這個假設它會把 (i32, i32, i32, i32) 簡化成 (i32, i32, i32) !一但你弄懂這個不穩定的根源,你就可以猜到那些有更多 case 的 enum,編碼會更複雜。

3. 結論

除非你每天都想要有驚喜或是有領薪水,不然沒事不要逆向編碼的方式或是在這上面建構程式!

Date: 2022-12-08 Thu 00:00
Author: Lîm Tsú-thuàn