這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。在 Go 的源碼中包含大量彙編語句,最優秀的範例程式碼位於 `math/big`, `runtime` 和 `crypto` 這些庫中,但是從這裡入門的話實在太過於痛苦,這些樣本都是著力於系統操作和效能的運行代碼。對於沒有經驗的 Go 語言愛好者來說,這樣會使通過庫代碼的學習過程遇到很大困難 。這也是撰寫本文的原因所在。Go ASM ( 譯者註:ASM 是彙編的簡寫 ) 是一種被 Go 編譯器使用的特殊形式的組合語言,而且它基於 Plan 9 (譯者註:來自貝爾實驗室的概念[網路作業系統 ](https://baike.baidu.com/item/%E7%BD%91%E7%BB%9C%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F))輸入風格,所以先從 [文檔](https://9p.io/sys/doc/asm.pdf) 開始是一個不錯的選擇。注意:本文的內容是基於 x86_64 架構,但大多數樣本也能相容 x86 架構。一些例子是從原始文檔中選取出來的,主要目的是建立一個綜合的統一標準摘要,涵蓋那些最重要/有用的主題。## 第一步Go ASM 和標準的彙編文法( NASM 或 YASM )不太一樣,首先你會發現它是架構獨立的,沒有所謂的 32 或 64 位元寄存器,如所示:| NASM x86 | NASM x64 | Go ASM || -------- | -------- | ------ || eax | rax | AX || ebx | rbx | BX || ecx | rcx | CX || … | … | … |大部分寄存器符號都依賴於架構。另外, Go ASM 還有四個預定義的符號作為偽寄存器。它們不是真正意義上的寄存器,而是被工具鏈維持出來的虛擬寄存器,這些符號在所有架構上都完全一樣:- `FP`: 幀指標 –參數和局部變數–- `PC`: 程式計數器 –跳轉和分支–- `SB`: 靜態基址指標 –全域符號–- `SP`: 棧指標 –棧的頂端–.這些虛擬寄存器在 Go ASM 中佔有了重要地位,並且被廣泛使用,其中最重要的就要屬 SB 和 FP了。偽寄存器 SB 可以看作是記憶體的起始地址,所以 foo(SB) 就是 foo 在記憶體中的地址。文法中有兩種修飾符,<> 和 +N (N是一個整數)。第一種情況 foo<>(SB) 代表了一個私人元素,只有在同一個源檔案中才可以訪問,類似於 Go 裡面的小寫命名。第二種屬於對相對位址加上一個位移量後得到的地址,所以 foo+8(SB) 就指向 foo 之後 8 個位元組處的地址。偽寄存器 FP 是一個虛擬幀指標,被用來引用過程參數,這些引用由編譯器負責維護,它們將指向從偽寄存器處位移的棧中參數。在一台 64 位元電腦上, 0(FP) 是第一個參數, 8(FP) 就是第二個參數。為了引用這些參數,編譯器會強制它們的命名使用,這是出於清晰和可讀性的考慮。所以 MOVL foo+0(FP), CX 會把虛擬 FP 寄存器中的第一個參數放入到物理上的 CX 寄存器,以及 MOVL bar+8(FP), DX 會把第二個參數放入到 DX 寄存器中。讀者可能已經注意到這種 ASM 文法類似 AT&T 風格,但不完全一致:| Intel | AT&T | Go || ------------------ | -------------------- | ------------------ || `mov eax, 1` | `movl $1, %eax` | `MOVQ $1, AX` || `mov rbx, 0ffh` | `movl $0xff, %rbx` | `MOVQ $(0xff), BX` || `mov ecx, [ebx+3]` | `movl 3(%ebx), %ecx` | `MOVQ 2(BX), CX` |另一處顯著的差異就是全域源碼檔案結構, NASM 中的代碼結構是用 section 清晰的定義出來:```asmglobal startsection .bss…section .data…section .textstart:mov rax, 0x2000001mov rdi, 0x00syscall```而在 Go 彙編中則是靠預定義的 section 類型符號:```asmDATA myInt<>+0x00(SB)/8, $42GLOBL myInt<>(SB), RODATA, $8// func foo()TEXT ·foo(SB), NOSPLIT, $0MOVQ $0, DXLEAQ myInt<>(SB), DXRET```這種文法使得我們能夠儘可能的在最適合的地方定義符號。## 在 Go 中調用彙編代碼可以從介紹中發現,Go 中的彙編代碼主要用於最佳化和與底層系統互動,這使得 Go ASM 並不會像其它的經典彙編代碼那樣獨立運行。Go ASM 必須在 Go 代碼中調用。hello.go```gopackage mainfunc neg(x uint64) int64func main() {println(neg(42))}```hello_amd64.s```asmTEXT ·neg(SB), NOSPLIT, $0MOVQ x+0(FP), AXNEGQ AXMOVQ AX, ret+8(FP)RET```運行這份代碼將會在終端列印出 -42 。注意子過程符號開始處的 unicode 中間點 `·` ,這是為了包名分隔,沒有首碼的 `·foo` 等價於 `main·foo`。過程中的 `TEXT ·neg(SB), NOSPLIT, $0` 意味著:- `TEXT`: 這個符號位於 `text` section。- `·neg`: 該過程的包符號和符號。- `(SB)`: 詞法分析器會用到。- `NOSPLIT`: 使得沒有必要定義參數大小。–可以省略不寫–- `$0`: 參數的大小, 如果定義了`NOSPLIT` 就是 `$0` 。build的步驟仍舊和往常一樣,使用 `go build` 命令, Go 編譯器會根據檔案名稱–`amd64`–自動連結`.s` 檔案。還有一份資源可以協助學習 Go 檔案的編譯過程,我們可以看下 `go tool build -S <file.go>` 產生的 Go ASM 。一些類似 `NOSPLIT` 和 `RODATA` 的符號都是在 `textflax` 標頭檔中定義,因此用`#include textflag.h` 包含 該檔案可以有利於完成一次沒有報錯的完美編譯。## MacOS 中的系統調用MacOS 中的系統調用需要在加上調用號 `0x2000000` 後才能被調用,舉個例子,exit 系統調用就是 `0x2000001` 。調用號開始處的 `2` 是因為有多個種類的調用被定義在了重疊的調用號範圍,這些類型都是定義在 [這裡](https://opensource.apple.com/source/xnu/xnu-792.10.96/osfmk/mach/i386/syscall_sw.h) :```c#define SYSCALL_CLASS_NONE0/* Invalid */#define SYSCALL_CLASS_MACH1/* Mach */#define SYSCALL_CLASS_UNIX2/* Unix/BSD */#define SYSCALL_CLASS_MDEP3/* Machine-dependent */#define SYSCALL_CLASS_DIAG4/* Diagnostics */```所有的 MacOS 系統調用號列表可以在 [這裡](https://opensource.apple.com/source/xnu/xnu-1504.3.12/bsd/kern/syscalls.master) 找到.參數是通過這些寄存器 `DI`, `SI`, `DX`, `R10`, `R8` 和`R9` 傳遞給系統調用, 系統調用代碼存放在 `AX` 中。NASM 中的寫法類似這樣:```asmmov rax, 0x2000004 ; 寫系統調用mov rdi, 1 ; 參數 1 fd (stdout)mov rsi, rcx ; 參數 2 bufmov rdx, 16 ; 參數 3 countsyscall```與之相反,Go ASM 中類似的例子則是像這樣:```asmMOVL $1, DI // 參數 1 fd (stdout)LEAQ CX, SI // 參數 2 bufMOVL $16, DX // 參數 3 countMOVL $(0x2000000+4), AX // 寫系統調用SYSCALL```同樣,系統調用代碼被放置在 `SYSCALL` 指令之前,這僅僅是通用寫法,你可以像在 NASM 中那樣直接把寫系統調用放在最前面,編譯後不會報任何錯誤。## 使用字串現在我相信你已經能夠寫一些基本的彙編代碼並運行了,例如經典的 hello world 。我們知道如何把一個參數傳遞給子過程,如何傳回值和如果在資料 section 裡面定義符號。你試過定義一個字串嗎?幾天前我在編寫一些彙編代碼的時候遇到了這個問題,而我最關心的問題是,我該如何做才能去定義一個操蛋的字串?嗯,NASM 中可以像這樣來定義字串:```asmsection data:foo: db "My random string", 0x00```可這在 Go 中不行,在我深入研究了我能從網上找到的所有 go ASM 項目後,我還是沒能找到一個定義簡單字串的樣本。最後我在 Plan9 組合語言文檔中找到了一個例子,它可以說明怎樣讓目標實現。Go 和 Plan9 唯一的不同之處是使用雙引號而非單引號,並且添加了一個`RODATA` 符號:```asmDATA foo<>+0x00(SB)/8, $"My rando"DATA foo<>+0x08(SB)/8, $"m string"DATA foo<>+0x16(SB)/1, $0x0aGLOBL foo<>(SB), RODATA, $24TEXT ·helloWorld(SB), NOSPLIT, $0MOVL $(0x2000000+4), AX // syscall writeMOVQ $1, DI // arg 1 fdLEAQ foo<>(SB), SI // arg 2 bufMOVL $24, DX // arg 3 countSYSCALLRET```注意,定義字串時不能放在一起,需要把它們定義在 8 位元組( 64 位元)的塊中。現在你可以深入 Go ASM 世界中寫下你自己的超級快速和極端最佳化的代碼l,並請記住,去讀那些操蛋的手冊(微笑臉)。## 在安全領域使用?使用彙編除了最佳化你的 Go 代碼外,也可以很方便的避免觸發常見簽名從而規避防毒軟體,以及使用一些反編譯技術規避沙箱來搜尋異常行為,或者只是讓分析師哀嚎。如果你對此有興趣,我會在該主題的下一篇文章中介紹,敬請關注!## 附錄- https://golang.org/doc/asm- https://9p.io/sys/doc/asm.pdf- https://goroutines.com/asm- https://blog.sgmansfield.com/2017/04/a-foray-into-go-assembly-programming/
via: https://blog.hackercat.ninja/post/quick_intro_to_go_assembly/
作者:hcn 譯者:sunzhaohao 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
1427 次點擊 ∙ 1 贊