自 Rust 1.59 以降,在 Rust 代碼中內(nèi)聯(lián)匯編代碼的語言特性已然 stable^1。參考知乎上一篇文章^3,我用 Rust 的內(nèi)聯(lián)匯編實現(xiàn)了有棧協(xié)程^4。在此過程中學到了一些知識。
本文假設(shè)讀者對 x86 匯編有基礎(chǔ)了解。
局部內(nèi)聯(lián)匯編與自動分配寄存器
Rust 的內(nèi)聯(lián)匯編一開始是對標 GCC 的內(nèi)聯(lián)匯編設(shè)計的,長得像這樣^5:
asm!("mov $4, %eax cpuid mov %eax, $0 mov %ebx, $1 mov %ecx, $2 mov %edx, $3" : "=r"(a), "=r"(b), "=r"(c), "=r"(d) : "m"(info) : "eax", "ebx", "ecx", "edx" )
后來才變成如今富有 Rust 特色的樣子^6:
asm!( "mov edi, ebx", "cpuid", "xchg edi, ebx", in("eax") info, lateout("eax") a, out("edi") b, out("ecx") c, out("edx") d, )
與 GCC 內(nèi)聯(lián)匯編語法一樣,Rust 希望即使需要手寫匯編,程序員也能將一部分工作交給編譯器來高效完成,這部分工作就是寄存器分配,畢竟只有編譯器了解內(nèi)聯(lián)匯編前后的上下文,知道該怎么分配寄存器最合適。
asm
宏的in
,out
,inout
,lateout
,inlateout
參數(shù)就是為了讓編譯器幫助分配寄存器的。
in
表示將變量的值傳給寄存器,編譯器生成的匯編代碼會使得在內(nèi)聯(lián)匯編代碼中讀取相應的寄存器,就得到了傳入的變量的值;
out
表示將寄存器的值寫到變量中,在內(nèi)聯(lián)匯編代碼中寫入相應寄存器,編譯器在內(nèi)聯(lián)匯編之后生成的匯編代碼會使得相應變量具有寫入相應寄存器的值;
late
則是代表編譯器可以采取進一步的策略來優(yōu)化寄存器分配:默認的分配策略給每個參數(shù)分配不同的寄存器,使用lateout
或inlateout
的參數(shù)則允許編譯器復用某個in
參數(shù)的寄存器,只要內(nèi)聯(lián)匯編代碼中先讀完所有的in
寄存器,再輸出lateout
或inlateout
寄存器即可。
具體細節(jié)以及此處沒講到的option
可參考^1。
全局內(nèi)聯(lián)匯編與名稱修飾(Name Mangling)
除了需要寫在函數(shù)體中的asm
宏,還有需要寫在函數(shù)之外的global_asm
宏,其作用與獨立的匯編代碼相差不大,一切全由程序員掌控,沒有上節(jié)所述寄存器自動分配之功能,還需要手動管理參數(shù)傳遞,棧對齊等等。
用global_asm
我們可以寫出源代碼完全是匯編代碼的函數(shù),函數(shù)名就是匯編代碼中的標簽,函數(shù)參數(shù)和返回值需要按照 ABI 約定來處理^7:
use std::global_asm; extern "C" { fn my_asm_add(a: i32, b: i32) -> i32; } global_asm!{ "my_asm_add:", "mov eax, edi", "add eax, esi", "ret", } fn main() { let a = 114; let b = 514; let x = unsafe { my_asm_add(a, b) }; dbg!(x); }
這段代碼在x86_64-unknown-linux-gnu
的目標,也就是 Rust Playground 的運行環(huán)境下會通過編譯并輸出正確結(jié)果 628,但在 64 位 Windows 下則會得到錯誤的結(jié)果,因為 64 位 Windows 所用的 C ABI 和 64 位 Linux 不一樣,雖然都是通過寄存器傳遞參數(shù),但 64 位 Windows 的 C ABI 的第一二參數(shù)是用 RCX 和 RDX 傳遞,而非示例中的 RDI 和 RSI。
而在MacOS上編譯,結(jié)果是編譯不過——雖然和 64 位 Linux 一樣使用 System V AMD64 ABI,但 MacOS 進行 C 語言函數(shù)名名稱修飾時會在函數(shù)名前加一個下劃線,所以編譯器會試圖尋找_my_asm_add
符號,結(jié)果找不到。在匯編代碼中把"my_asm_add:"
改成"_my_asm_add:"
即可編譯通過。
由此可見匯編語言的不可移植性:即使是同一架構(gòu),甚至同一 ABI 約定的匯編代碼也相當不可移植。
在代碼編寫過程中,我發(fā)現(xiàn)一個技巧可以規(guī)避掉名稱修飾的影響。asm
和global_asm
宏可以接受格式為sym SYMBOL
的參數(shù)來引用符號,其中SYMBOL
是函數(shù)或者靜態(tài)變量,這種參數(shù)的目的是在匯編語言中直接引用 Rust 函數(shù)或靜態(tài)變量的符號,盡管 Rust 的名稱修飾算法尚未 stable,但代碼中可以不寫出來而由編譯器來計算。這個功能也可以用在extern
符號上,因此可以這樣寫:
global_asm!{ ".extern {0}", "{0}:", "mov eax, edi", "add eax, esi", "ret", sym my_asm_add, }
這樣在編譯 x86_64-unknown-linux-gnu 目標時生成的匯編代碼中的標簽是my_asm_add
,而對于x86_64-apple-darwin
目標,生成的標簽則是_my_asm_add
。
這樣的技巧不夠方便,更直觀的寫法是 naked function^9,這種函數(shù)從外部看來就是一個 unsafe 函數(shù),而內(nèi)部只允許有一個asm
宏調(diào)用,編譯器不生成一般函數(shù)中會有的各種上下文代碼,函數(shù)本體完全由該 asm 宏調(diào)用生成。
程序重定位與位置無關(guān)代碼
為了加載動態(tài)鏈接庫或者避免被黑客利用固定程序地址攻擊,操作系統(tǒng)加載程序時會將其載入到隨機的內(nèi)存地址,這個過程就是程序重定位。
對于 32 位 x86 程序,需要在加載時修改程序中所有的絕對地址,包括函數(shù)的和數(shù)據(jù)的。在匯編語言中可以直接將標號作為常量使用,但最好不要寫mov eax, LABEL
這樣的語句,因為這樣的語句加載器不會識別和修改。應該寫lea eax, LABEL
。
x86_64 支持相對 RIP 尋址,Rust 編譯器默認將代碼編譯為使用這項特性的位置無關(guān)可執(zhí)行程序(PIE),因此在 Rust 的內(nèi)聯(lián)匯編中取符號地址需要寫成lea rax, [rip+SYMBOL]
。
示例,x86_64 平臺下給 static 變量X
加 1 的函數(shù)用匯編語言實現(xiàn)[11]:
use std::global_asm; static mut X: usize = 0; extern "C" { fn incr_x(); } global_asm!{ "{0}:", "add dword ptr [rip+{X}], 1", "ret", sym incr_x, X = sym X, } fn main() { unsafe { incr_x(); dbg!(X); incr_x(); dbg!(X); } }
32 位 x86 代碼下則需要把匯編代碼改成:
global_asm!{ "{0}:", "lea eax, {X}", "add dword ptr[eax], 1", "ret", sym incr_x, X = sym X, }
-
代碼
+關(guān)注
關(guān)注
30文章
4728瀏覽量
68251 -
編譯器
+關(guān)注
關(guān)注
1文章
1617瀏覽量
49019 -
Rust
+關(guān)注
關(guān)注
1文章
228瀏覽量
6547
原文標題:在 Rust 中使用內(nèi)聯(lián)匯編
文章出處:【微信號:Rust語言中文社區(qū),微信公眾號:Rust語言中文社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論