- 原文地址:Anatomy of a function call in Go
- 原文作者:Phil Pearl
- 譯文出自:掘金翻譯計劃
- 譯者:xiaoyusilen
- 校對者:1992chenlu,Zheaoli
讓我們來看一些簡單的 Go 的函式,然後看看我們能否明白函式呼叫是怎麼回事。我們將通過分析 Go 編譯器根據函式生成的彙編來完成這件事。對於一個小小的部落格來講,這樣的目標可能有點不切實際,但是別擔心,組合語言很簡單。哪怕是 CPU 都能讀懂。
圖片來自 Rob Baines github.com/telecoda/in…
這是我們的第一個函式。對,我們只是讓兩個數相加。
func add(a, b int) int {
return a + b
}複製程式碼
我們編譯的時候需要關閉優化,這樣方便我們去理解生成的彙編程式碼。我們用 go build -gcflags 'N -l'
這個命令來完成上述操作。然後我們可以用 go tool objdump -s main.add func
輸出我們函式的具體細節(這裡的 func 是我們的包名,也就是我們剛剛用 go build 編譯出的可執行檔案)。
如果你之前沒有學過彙編,那麼恭喜你,你將接觸到一個全新的事物。另外我會在 Mac 上完成這篇部落格的程式碼,因此所生成的是 Intel 64-bit 彙編。
main.go:20 0x22c0 48c744241800000000 MOVQ $0x0, 0x18(SP)
main.go:21 0x22c9 488b442408 MOVQ 0x8(SP), AX
main.go:21 0x22ce 488b4c2410 MOVQ 0x10(SP), CX
main.go:21 0x22d3 4801c8 ADDQ CX, AX
main.go:21 0x22d6 4889442418 MOVQ AX, 0x18(SP)
main.go:21 0x22db c3 RET複製程式碼
現在我們看到了什麼?如下所示,每一行被分為了4部分:
- 原始檔的名稱和行號(main.go:15)。這行的原始碼會被轉換為標有程式碼行號的說明。Go 的一行可能被轉換成多行程式集。
- 目標檔案中的偏移量(例如 0x22C0)。
- 機器碼(例如 48c744241800000000)。這是 CPU 實際執行的二進位制機器碼。我們不需要看這個,幾乎沒有人看這玩意。
- 機器碼的彙編表示形式,這也是我們想要理解的部分。
讓我們將注意力集中在最後一部分,組合語言。
- MOVQ,ADDQ 和 RET 是指令。它們告訴 CPU 需要執行的操作。後面的引數告訴 CPU 對什麼執行該操作。
- SP,AX 和 CX 是 CPU 暫存器。暫存器是 CPU 用於儲存值的地方,CPU 有多個暫存器可以使用。
- SP 是一個專用暫存器,用於儲存當前堆疊指標。堆疊是記錄區域性變數,引數和函式呼叫的暫存器。每個 goroutine 都有一個堆疊。當一個函式呼叫另一個函式,然後另一個函式再呼叫其他函式,每個函式在堆疊上獲得自己的儲存區域。在函式呼叫期間建立儲存區域,將 SP 的大小中減去所需的儲存大小。
- 0x8(SP)是指超過 SP 指向的儲存單元的 8 個位元組的儲存單元。
因此,我們的工作的內容包含儲存單元,CPU 暫存器,用於在儲存器和暫存器之間移動值的指令以及暫存器上的操作。 這幾乎就是一個 CPU 所完成的事情了。
現在讓我們從第一條指令開始看每一條內容。別忘了我們需要從記憶體中載入兩個引數 a
和 b
,把它們相加,然後返回至呼叫函式。
MOVQ $0x0, 0x18(SP)
將 0 置於儲存單元 SP+0x18 中。 這句程式碼看起來有點抽象。MOVQ 0x8(SP), AX
將儲存單元 SP+0x8 中的內容放到 CPU 暫存器 AX 中。也許這就是從記憶體中載入的我們所使用的引數之一?MOVQ 0x10(SP), CX
將儲存單元 SP+0x10 的內容置於 CPU 暫存器 CX 中。 這可能就是我們所需的另一個引數。ADDQ CX, AX
將 CX 與 AX 相加,將結果存到 AX 中。好,現在已經把兩個引數相加了。MOVQ AX, 0x18(sp)
將暫存器 AX 的內容儲存在儲存單元 SP+0x18 中。這就是在儲存相加的結果。RET
將結果返回至呼叫函式。
記住我們的函式有兩個引數 a
和 b
,它計算了 a+b
並且返回了結果。MOVQ 0x8(SP), AX
將引數 a
移到 AX 中,在 SP+0x8 的堆疊中 a
將被傳給函式。MOVQ 0x10(SP), CX
將引數 b
移到 CX 中,在 SP+0x10 的堆疊中 b
將被傳給函式。ADDQ CX, AX
使 a
和 b
相加。MOVQ AX, 0x18(SP)
將結果儲存到 SP+0x18 中。 現在相加的結果被儲存在 SP+0x18 的堆疊中,當函式返回撥用函式時,可以從棧中讀取結果。
我假設 a
是第一個引數,b
是第二個引數。我不確定是不是這樣。我們需要花一點時間來完成這件事,但是這篇文章已經很長了。
那麼有點神祕的第一行程式碼究竟是做什麼用的?MOVQ $0X0, 0X18(SP)
將 0 儲存至 SP+0x18 中,而 SP+0x18 是我們儲存相加結果的地方。我們可以猜測,這是因為 Go 把沒有初始化的值設定為 0 ,我們已經關閉了優化,即使沒有必要,編譯器也會執行這個操作。
所以我們從中明白了什麼:
- 好,看起來引數都存在堆疊中,第一個引數儲存在 SP+0x8 中,另一個在更高編號的地址中。
- 並且看上去返回的結果儲存在引數後邊,一個更高編號的地址中。
現在讓我們看另一個函式。這個函式有一個區域性變數,不過我們依然會讓它看起來很簡單。
func add3(a int) int {
b := 3
return a + b
}複製程式碼
我們用和剛才一樣的過程來獲取程式集列表。
TEXT main.add3(SB)
/Users/phil/go/src/github.com/philpearl/func/main.go
main.go:15 0x2280 4883ec10 SUBQ $0x10, SP
main.go:15 0x2284 48896c2408 MOVQ BP, 0x8(SP)
main.go:15 0x2289 488d6c2408 LEAQ 0x8(SP), BP
main.go:15 0x228e 48c744242000000000 MOVQ $0x0, 0x20(SP)
main.go:16 0x2297 48c7042403000000 MOVQ $0x3, 0(SP)
main.go:17 0x229f 488b442418 MOVQ 0x18(SP), AX
main.go:17 0x22a4 4883c003 ADDQ $0x3, AX
main.go:17 0x22a8 4889442420 MOVQ AX, 0x20(SP)
main.go:17 0x22ad 488b6c2408 MOVQ 0x8(SP), BP
main.go:17 0x22b2 4883c410 ADDQ $0x10, SP
main.go:17 0x22b6 c3 RET複製程式碼
喔!看起來有點複雜。讓我們來試試。
前4條指令是根據原始碼中的第15行列出的。這行程式碼是這樣的:
func add3(a int) int {複製程式碼
這一行程式碼似乎沒有做什麼。所以這可能是一種宣告函式的方法。讓我們分析一下。
SUBQ $0x10, SP
從 SP 減去 0x10=16。這個操作為我們釋放了 16 位元組的堆疊空間MOVQ BP, 0x8(SP)
將暫存器 BP 中的值儲存至 SP+8 中,然後LEAQ 0x8(SP), BP
將地址 SP+8 中的內容載入到 BP 中。現在我們已經有空間可以儲存 BP 中之前所存的內容,然後將 BP 中的內容儲存至剛剛分配的儲存空間中,這有助於建立堆疊區域鏈(或者堆疊框架)。這有點神祕,不過在這篇文章中我們恐怕不會解決這個問題。- 在這一部分的最後是
MOVQ $ 0x0, 0x20 (SP)
,它和我們剛剛分析的最後一句類似,就是將返回值初始化為0。
下一行對應的是原始碼中的 b := 3
,MOVQ $03x, 0(SP)
把 3 放到 SP+0 中。這解決了我們的一個疑惑。當我們從 SP 中減去 0x10 = 16 時,我們得到了可以儲存兩個 8 位元組值的空間:我們的區域性變數 b
儲存在 SP+0 中,而 BP 之前的值儲存在 SP+0x08 中。
接下來的 6 行程式集對應於 return a + b
。這需要從記憶體中載入 a
和 b
,然後將它們相加,並且返回結果。讓我們依次看看每一行。
MOVQ 0x18(SP), AX
將儲存在 SP+0x18 的引數a
移動到暫存器 AX 中ADDQ $0x3, AX
將 3 加到 AX(由於某些原因,它不使用我們儲存在 SP+0 的區域性變數b
,儘管編譯時優化被關閉了)MOVQ AX, 0x20(SP)
將a+b
的結果儲存到 SP+0x20 中,也就是我們返回結果所存的地方。- 接下來我們得到的是
MOVQ 0x8(SP), BP
以及ADDQ $0x10, SP
,這些將恢復BP的舊值,然後將 0x10 新增到 SP,將其設定為該函式開始時的值。 - 最後我們得到了
RET
,將要返回給呼叫函式的。
所以我們從中學到了什麼呢?
- 呼叫函式在堆疊中為返回值和引數分配空間。返回值的儲存地址比引數的儲存地址高。
- 如果被呼叫函式有區域性變數,則通過減少堆疊指標 SP 的值為它們分配空間。它也和暫存器 BP 做了一些神祕的事情。
- 當函式返回任何對 SP 和 BP 的操作都會相反。
讓我們看看堆疊在 add3() 方法中如何使用:
SP+0x20: the return value
SP+0x18: the parameter a
SP+0x10: ??
SP+0x08: the old value of BP
SP+0x0: the local variable b複製程式碼
如果你覺得文章中沒有提到 SP+0x10,所以不知道這是幹什麼用的。我可以告訴你,這是儲存返回地址的地方。這是為了讓 RET
指令知道返回到哪裡去。
這篇文章已經足夠了。 希望如果以前你不知道這些東西如何工作,但是現在你覺得你已經有了一些瞭解,或者如果你被彙編嚇倒了,那麼也許它不那麼晦澀難懂了。 如果你想了解有關彙編的更多資訊,請在評論中告訴我,我會考慮在之後的文章中寫出來。
既然你已經看到這兒了,如果喜歡我的這篇文章或者可以從中學到一點什麼的話,那麼請給我點個贊這樣這篇文章就可以被更多人看到了。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃。