深入學習 Go 語言的設計與實現之前要準備以下工作:
- 克隆 Go 倉庫源代碼並編譯它。
- 了解 Plan 9 彙編,知道 Go 的棧結構並能分析代碼的執行過程。
彙編者,二進制代碼的文本形式也,其最大的特點就是不可移植。Plan 9 彙編是貝爾實驗室的九號計劃的產物,目前被用於 Go 程序編譯的中間代碼,因爲 Go 的作者 Rob Pike,同時也是 Plan 9 彙編的作者。
Plan 9 彙編指令與 Intel 等彙編等的不同在於:
一般情況下,命令的源操作數在先,目的操作數在後。 如同樣是將十六進制的 10 傳送到 AX寄存器,在 Plan 9 中是
MOVQ $0x10, AX
,而在 Intel 彙編中是mov rax, 0x10
。棧的調整通過硬件 SP 寄存器進行加減運算實現。而 Intel 彙編中通過 push 和 pop 命令實現。
操作的數據長度取決於命令的後綴。而 Intel 彙編取決於寄存器。
// plan 9 彙編
MOVB $1, DI // 1 byte
MOVW $0x10, BX // 2 bytes
MOVD $1, DX // 4 bytes
MOVQ $-10, AX // 8 bytes
// intel 彙編
mov rax, 0x1 // 8 bytes
mov eax, 0x100 // 4 bytes
mov ax, 0x22 // 2 bytes
mov ah, 0x33 // 1 byte
mov al, 0x44 // 1 byte
通過分析 Plan 9 彙編代碼我們可以繪製出如下的棧結構:
1.1 調試 Go 語言
1.1.1 克隆 go 倉庫並查看代碼行數
cloc (Count Lines of Code) 工具可以計算源代碼行數。執行後可知 Go 源代碼超過 140 萬行。
$ git clone https://github.com/golang/go.git |
1.1.2 編譯源碼
在源代碼中找到 fmt 包,修改 Println 函數如下:
func Println(a ...interface{}) (n int, err error) { |
隨後執行 ./src/make.bash
編譯源代碼,成功之後用編譯出來的 Go 二進制文件來執行你的代碼:
$ cat main.go |
1.2 Plan 9 彙編
1.2.1 彙編
在正式學習 Plan 9 彙編之前,建議先看下阮一峰老師的《汇编语言入门教程》以理解以下結論:
匯編語言是二進制指令的文本形式。
較之CPU 緩存,CPU 訪問寄存器速度更快且不需要尋址。
寄存器依靠名稱而不是地址來區分數據。
內存中的堆自下而上分配,棧自上而下分配。
1.2.2 Plan 9
Plan 9,貝爾實驗室九號計劃,是一個基於 Unix 的分布式操作系統,該系統開源但並未商業化使用。Go 語言的作者 Rob Pike 是其帶領者之一。
Go 使用了 Plan 9 彙編作爲中間代碼,最後再編譯成二進制代碼。了解 Plan 9 彙編有助於分析 Go 語言的底層實現和排查問題。
彙編語言具有不可移植性,各個平台上的平台指令集和寄存器都不一樣。學習時我們以 Linux amd64 爲例。
1.2.3 寄存器
amd64 上的寄存器有:
- AX:累加寄存器(AccumulatorRegister),用於存放數據,包括算術、操作數、結果和臨時存放地址。
- BX:基址寄存器(BaseRegister),用於存放訪問存儲器時的地址。
- CX:計數寄存器(CountRegister),用於保存計算值,用作計數器。
- DX:數據寄存器(DataRegister),用於數據傳遞,在寄存器間接尋址中的I/O指令中存放I/O端口的地址。
- DI:目的寄存器(DestinationIndex),用於存放目的操作數的偏移地址。
- SI:源變址寄存器(SourceIndex),用於存放源操作數的偏移地址。
- BP:棧基指針(BasePointer),保存在進入函數前的棧頂基址。
- SP:棧指針(StackPointer),指向當前棧幀的局部變量的開始位置。如果是symbol+offset(SP)的形式表示偽寄存器,offset 的合法取值是 [-framesize, 0),注意是个左闭右开的区间;如果是offset(SP)的形式表示硬件寄存器。(注意:對於編譯輸出(
go tool compile -S / go tool objdump
)的代碼來講,目前所有的 SP 都是硬件寄存器 SP,無論是否帶 symbol) - FP:棧幀指針(FramePointer),偽寄存器。指向引用函數的輸入參數,形式是symbol+offset(FP),例如 arg0+0(FP)。
- SB:靜態基指針(StaticBasePointer),偽寄存器。一般用來聲明函數或全局變量。
- PC:程序計數器(ProgramCounter),偽寄存器。存放下一條指令的地址。
- R8
- R9
- R10
偽寄存器(pseudo register):僞寄存器與平台無關,只在生成目標代碼時才與平台上的硬件寄存器對應起來。Plan 9 彙編有四個偽寄存器,FP、SP、SB、PC。FP 存放引用函數的輸入參數,SP 存放當前棧幀的局部變量的開始位置,SB 聲明函數和全局變量,PC 存放下一條指令的地址。
1.2.4 基本指令
棧調整
通過對 SP 寄存器進行操作實現,這與 intle 或 AT&T 匯編不同,他們是使用 push、pop 指令實現。
SUBQ $0x18, SP // 對 SP 做減法,為函數分配函數棧幀
... // 省略無用代碼
ADDQ $0x18, SP // 對 SP 做加法,清除函數棧幀注意:SP 一開始處於棧頂且棧是自上而下分配的,所以要通過減法分配棧幀,通過加法清除棧幀。示意圖如下:
數據搬運
將源操作數複製到目的操作數。Plan 9 中用
$num
形式表示常數,默認爲十進制,也可用$0x123
形式表示十六進制。搬運的長度由MOV
的後綴決定。格式:
MOVB source destination
// move byte
// 搬運一個字節長
MOVB $1, DI
// move word = 2 bytes
// 搬運一個字長
MOVW $0x10, BX
// move double word = 4 bytes
// 搬運兩倍字長
MOVD $1, DX
// move quad word = 8 bytes
// 搬運四倍字長
MOVQ $-10, AX地址傳送
將源操作數的地址複製到目的操作數。
lea is an abbreviation of “load effective address”。— Stack Overflow
// 把AX內容的地址傳送到BX中
LEAQ AX, BX計算
// add quad word from AX to BX
// BX += AX
ADDQ AX, BX
// subtract quad word from AX to BX
// BX -= AX
SUBQ AX, BX
// 無符號乘法 IMUL
// BX *= AX
IMULQ AX, BX
// 無符號除法 IDIV
// 除數是CX,被除數是AX,結果存儲到AX中
IDIVQ CX
// 比較SI和CX的大小。與SUBQ類似,只是不賦值
// 其結果會存放到寄存器中
CMPQ SI CX跳轉
// -- 無條件跳轉 --
// 跳轉到地址,地址可為代碼中的地址,不過實際上手寫不會出現這種東西
JMP addr
// 跳轉到標籤,可以跳轉到同一函數內的標籤位置
JMP label
// 以當前指令為基礎,向前跳轉 x 行
JMP 2(PC)
// 以當前指令為基礎,向後跳轉 x 行
JMP -2(PC)
// -- 有條件跳轉 --
// jump target if zero
// 如果 zero flag 被 set 過,則跳轉
JZ target
// jump target if less
// 上一行的比較CMP結果,左邊小於右邊則執行跳到0x0185地址處(十進制389轉換成十六進制0x0185)
JLS 389指令集
見源代碼的 arch 部分。
1.2.5 案例分析:函數調用
編寫以下代碼於 call.go 文件:
package main |
執行以下命令可查看彙編代碼:
// -S 輸出彙編代碼到控制台 Print assembly listing to standard output (code only). |
輸出結果爲(有刪減):
"".Callee STEXT nosplit size=52 args=0x18 locals=0x10 |
分析可得棧結構如下:
1.3 參考
- Go 语言设计与实现:第一章 准备工作 — draveness
- 汇编语言入门教程 — 阮一峰的网络日志
- Go 系列文章3 :plan9 汇编入门 — 曹春晖@No Headback
- 深入Go的底层,带你走近一群有追求的人 — Stefno@博客园
- go plan9汇编入门 — yuchanns@Go 语言中文网
- go编译工具的使用之汇编 — yuchanns@github.com
- Go Assembly by Example
- A Quick Guide to Go’s Assembler — golang.org
- A Manual for the Plan 9 assembler — Rob Pike