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
vec
andstr
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 theCString
type in thestd::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>
的
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. 結論
除非你每天都想要有驚喜或是有領薪水,不然沒事不要逆向編碼的方式或是在這上面建構程式!