- 原文地址:Anatomy of a function call in Go
- 原文做者:Phil Pearl
- 譯文出自:掘金翻譯計劃
- 譯者:xiaoyusilen
- 校對者:1992chenlu,Zheaoli
讓咱們來看一些簡單的 Go 的函數,而後看看咱們可否明白函數調用是怎麼回事。咱們將經過分析 Go 編譯器根據函數生成的彙編來完成這件事。對於一個小小的博客來說,這樣的目標可能有點不切實際,可是別擔憂,彙編語言很簡單。哪怕是 CPU 都能讀懂。前端
圖片來自 Rob Baines github.com/telecoda/in…react
這是咱們的第一個函數。對,咱們只是讓兩個數相加。android
func add(a, b int) int { return a + b }複製代碼
咱們編譯的時候須要關閉優化,這樣方便咱們去理解生成的彙編代碼。咱們用 go build -gcflags 'N -l'
這個命令來完成上述操做。而後咱們能夠用 go tool objdump -s main.add func
輸出咱們函數的具體細節(這裏的 func 是咱們的包名,也就是咱們剛剛用 go build 編譯出的可執行文件)。ios
若是你以前沒有學過彙編,那麼恭喜你,你將接觸到一個全新的事物。另外我會在 Mac 上完成這篇博客的代碼,所以所生成的是 Intel 64-bit 彙編。git
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部分:github
讓咱們將注意力集中在最後一部分,彙編語言。c#
所以,咱們的工做的內容包含存儲單元,CPU 寄存器,用於在存儲器和寄存器之間移動值的指令以及寄存器上的操做。 這幾乎就是一個 CPU 所完成的事情了。後端
如今讓咱們從第一條指令開始看每一條內容。別忘了咱們須要從內存中加載兩個參數 a
和 b
,把它們相加,而後返回至調用函數。markdown
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 ,咱們已經關閉了優化,即便沒有必要,編譯器也會執行這個操做。
因此咱們從中明白了什麼:
如今讓咱們看另外一個函數。這個函數有一個局部變量,不過咱們依然會讓它看起來很簡單。
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
,將要返回給調用函數的。因此咱們從中學到了什麼呢?
讓咱們看看堆棧在 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、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃。