[譯] 解析 Go 中的函數調用

讓咱們來看一些簡單的 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

  • 源文件的名稱和行號(main.go:15)。這行的源代碼會被轉換爲標有代碼行號的說明。Go 的一行可能被轉換成多行程序集。
  • 目標文件中的偏移量(例如 0x22C0)。
  • 機器碼(例如 48c744241800000000)。這是 CPU 實際執行的二進制機器碼。咱們不須要看這個,幾乎沒有人看這玩意。
  • 機器碼的彙編表示形式,這也是咱們想要理解的部分。

讓咱們將注意力集中在最後一部分,彙編語言。c#

  • MOVQ,ADDQ 和 RET 是指令。它們告訴 CPU 須要執行的操做。後面的參數告訴 CPU 對什麼執行該操做。
  • SP,AX 和 CX 是 CPU 寄存器。寄存器是 CPU 用於存儲值的地方,CPU 有多個寄存器可使用。
  • SP 是一個專用寄存器,用於存儲當前堆棧指針。堆棧是記錄局部變量,參數和函數調用的寄存器。每一個 goroutine 都有一個堆棧。當一個函數調用另外一個函數,而後另外一個函數再調用其餘函數,每一個函數在堆棧上得到本身的存儲區域。在函數調用期間建立存儲區域,將 SP 的大小中減去所需的存儲大小。
  • 0x8(SP)是指超過 SP 指向的存儲單元的 8 個字節的存儲單元。

所以,咱們的工做的內容包含存儲單元,CPU 寄存器,用於在存儲器和寄存器之間移動值的指令以及寄存器上的操做。 這幾乎就是一個 CPU 所完成的事情了。後端

如今讓咱們從第一條指令開始看每一條內容。別忘了咱們須要從內存中加載兩個參數 ab,把它們相加,而後返回至調用函數。markdown

  1. MOVQ $0x0, 0x18(SP) 將 0 置於存儲單元 SP+0x18 中。 這句代碼看起來有點抽象。
  2. MOVQ 0x8(SP), AX 將存儲單元 SP+0x8 中的內容放到 CPU 寄存器 AX 中。也許這就是從內存中加載的咱們所使用的參數之一?
  3. MOVQ 0x10(SP), CX 將存儲單元 SP+0x10 的內容置於 CPU 寄存器 CX 中。 這可能就是咱們所需的另外一個參數。
  4. ADDQ CX, AX 將 CX 與 AX 相加,將結果存到 AX 中。好,如今已經把兩個參數相加了。
  5. MOVQ AX, 0x18(sp) 將寄存器 AX 的內容存儲在存儲單元 SP+0x18 中。這就是在存儲相加的結果。
  6. RET 將結果返回至調用函數。

記住咱們的函數有兩個參數 ab,它計算了 a+b 而且返回告終果。MOVQ 0x8(SP), AX 將參數 a 移到 AX 中,在 SP+0x8 的堆棧中 a 將被傳給函數。MOVQ 0x10(SP), CX 將參數 b 移到 CX 中,在 SP+0x10 的堆棧中 b 將被傳給函數。ADDQ CX, AX 使 ab 相加。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 := 3MOVQ $03x, 0(SP) 把 3 放到 SP+0 中。這解決了咱們的一個疑惑。當咱們從 SP 中減去 0x10 = 16 時,咱們獲得了能夠存儲兩個 8 字節值的空間:咱們的局部變量 b 存儲在 SP+0 中,而 BP 以前的值存儲在 SP+0x08 中。

接下來的 6 行程序集對應於 return a + b。這須要從內存中加載 ab,而後將它們相加,而且返回結果。讓咱們依次看看每一行。

  • 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 指令知道返回到哪裏去。

這篇文章已經足夠了。 但願若是之前你不知道這些東西如何工做,可是如今你以爲你已經有了一些瞭解,或者若是你被彙編嚇倒了,那麼也許它不那麼晦澀難懂了。 若是你想了解有關彙編的更多信息,請在評論中告訴我,我會考慮在以後的文章中寫出來。

既然你已經看到這兒了,若是喜歡個人這篇文章或者能夠從中學到一點什麼的話,那麼請給我點個贊這樣這篇文章就能夠被更多人看到了。

掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃

相關文章
相關標籤/搜索