rust 的 wasm ABI
應該有不少人知道我最近在逆向 rust 的 wasmabi,解析它會回傳什麼,以下是一些紀錄
1. 字串、 Vec 與結構
這三者的邏輯都是結構的邏輯,通常是按順序來,除非有 padding 或是
align。比如說 2022-11-02 版的 Rust 會把字串跟 Vec 都變成一個 i32
三元組 (i32, i32, i32) ,且三個欄位的用途分別是
- 位址
- capability
- length
問題是這不是可以相信的內容,因為 Rust 從來都不保證二進位的相容性。在 2022-12-06 版中,雖然依然是一個三元組,但語意變成
- capability
- 位址
- length
有趣的是 Vec 跟 String 共用這樣的結構,在
FFI:
interoperability with foreign code 中可以找到答案
Vectors and strings share the same basic memory layout, and utilities are available in the
vecandstrmodules 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 theCStringtype in thestd::ffimodule.
特別談論這點,是因為他們是內建的,其他結構大可以採用 #[repr(C)]
迴避沒有穩定二進位介面的問題。但對我來說這恰恰就是破壞使用者體驗的部分,雖然
CString
是穩定的,卻不是對使用者來說好用的型別。可以直接想到的方案基本上都需要額外的型別轉換,只為了讓介面穩定
1.1. Vec
Vec 還因為是間接的,解開第一層三元組得到資料( u8
的序列)之後還要再轉換一次內部資料(根據是 u8
的幾倍對這個序列分割操作),弄得非常麻煩
2. enum
enum 可以說是整件事最麻煩也最難迴避的部分。當然就像前面說過的,用
#[repr(C, u8)] 可以處理自訂的型別,問題是很多重要的型別如
Result<T, E> 、 Option<T> 根本就不是你訂的啊!enum
原始的編碼其實還蠻簡單的,就是用數字標記是第幾個建構子,舉例來說
Option<i32> 的
Some(3)會被表示成(1, 3)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> 呢?你可能會覺得這個問題很瞎,難道不是根據 i32 比 String (在 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. 結論
除非你每天都想要有驚喜或是有領薪水,不然沒事不要逆向編碼的方式或是在這上面建構程式!