這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。讓我們來看幾個 go 函數調用的簡單例子。通過研究 go 編譯器為這些函數產生的彙編代碼,我們來看看函數調用是如何工作的。這個課題對於一篇小小的文章來講有點費勁,但是別擔心,組合語言是非常簡單的,連 CPU 都能理解它。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/anatomy-of-a-function/1_CKK4XrLm3ylzsQzNbOaroQ.png)*作者:Rob Baines https://github.com/telecoda/inktober-2016*來看看我們的第一個函數,對,我們簡單的將兩個數相加。```gofunc add(a, b int) int {return a + b}```通過 `go build -gcflags '-N -l'`,我們禁用了編譯最佳化,以使產生的彙編代碼更加容易讀懂。然後我們就可以用 go 工具 `objdump -s main.add func` (func是我們用的包名,也是 go build 產生的可執行檔的名稱),將這個函數對應的彙編代碼匯出來。如果你以前從來沒有接觸過組合語言,那麼恭喜,現在它對你來說是個新的東西。我在 mac 電腦上做的實驗,所以彙編代碼是英特爾 64位 的。```main.go:20 0x22c0 48c744241800000000 MOVQ $0x0, 0x18(SP)main.go:21 0x22c9 488b442408 MOVQ 0x8(SP), AXmain.go:21 0x22ce 488b4c2410 MOVQ 0x10(SP), CXmain.go:21 0x22d3 4801c8 ADDQ CX, AXmain.go:21 0x22d6 4889442418 MOVQ AX, 0x18(SP)main.go:21 0x22db c3 RET```在這裡我們應該看什麼呢?每一行都分成如下四個部分:- 源檔案名稱和行號 (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`,需要從記憶體載入,相加,然後返回。1. `MOVQ $0x0, 0x18(SP)` 在記憶體位址 SP+0x18 處放入 0。這好像有點玄妙。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` 返回到調用函數。還記得我們的函數有兩個參數 `a` 和 `b`,它計算 `a+b` 並且返回結果。`MOVQ 0x8(SP), AX` 是將參數 `a` 移動到 AX。`a` 通過棧的 SP+0x8 位置傳進函數。`MOVQ 0x10(SP), CX` 將參數 `b` 移動到 CX。`b` 通過棧的 SP+0x10 位置傳進函數。`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,另一個位於緊接著的更高地址的位置。- 傳回值看起來也是通過棧儲存的,在比參數更高地址的位置。現在我們來看另一個函數。這個函數有一個局部變數,但我們還是讓它盡量保持簡單。```gofunc add3(a int) int {b := 3return 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```啊,看起來比上一個要複雜一些。讓我們試著理解它。前四條指令對應的是第 15 行的原始碼。這行是:```gofunc 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 中。這協助我們建立了棧空間(棧幀) 的鏈。這有點玄妙,但恐怕這篇文章不會對此做解釋了。- 這一段的最後是 `MOVQ $0x0, 0x20(SP)`。這和我們剛討論的上一個函數很類似,是將傳回值初始化為 0。彙編的下一行對應於源碼的 `b := 3`。這個命令 `MOVQ $0x3, 0(SP)` 將 3 放入記憶體 SP+0 處。這個解決了我們的疑問。當我們把 SP 的值減去 0x10=16,我們空出了能容納 2 個 8位元組 變數的空間:局部變數 `b` 儲存於 SP+0,而 BP 的值儲存於 SP+0x8。後面的 6 行對應於 `return a + b`。這包括從記憶體載入 `a` 和 `b`, 將它們相加,以及返回計算結果。讓我們按順序來看每一行。- `MOVQ 0x18(SP), AX` 將儲存於 SP+0x18 處的參數 `a` 移動到寄存器 AX。- `ADDQ $0x3, AX` 將 AX 的值加 3(儘管我們關閉了最佳化選項, 但由於某種原因這裡還是沒有用到儲存於 SP+0 的局部變數 `b`)。- `MOVQ AX, 0x20(SP)` 將 `a+b` 的結果儲存於 SP+0x20,這裡即是我們的傳回值儲存的位置。- 接下來是 `MOVQ 0x8(SP), BP` 和 `ADDQ $0x10, SP`。首先恢複 BP 的值,然後將 SP 的值增加 0x10,這樣就恢複到了函數剛開始時 SP 的值。- 最後是 `RET`,返回到調用函數。那麼我們學到了什嗎?- 調用者函數為傳回值和參數在棧上申請空間。傳回值在棧上的地址高於參數。- 如果被調用函數有局部變數,它將通過減小棧指標 SP 的值來申請空間。這與寄存器 BP 也有著一些奇妙的關係。- 當函數返回時,一切對 SP 和 BP 的操作都會被回退。讓我們來繪製出 add3() 是如何使用棧的:```SP+0x20: 傳回值 SP+0x18: 參數 a SP+0x10: ?? SP+0x08: BP 原來的值 SP+0x0: 局部變數 b```我們並沒看到哪裡有提及 SP+0x10,所以我們不知道它有什麼用。不過我可以告訴你,這裡儲存了函數返回的地址。這樣 `RET` 命令才知道應該返回到哪裡。好了,對這篇文章來講上述內容已經夠了。如果你以前不知道這些東西如何工作,那麼希望你現在明白了一點點。如果你曾因彙編而膽怯,也希望現在它對你來說不再那麼晦澀難懂。如果你希望瞭解更多的細節,可以寫下評論,我會考慮以後再寫一篇更加詳細的文章。如果你喜歡這篇文章,或者從中學到了東西,請點贊,這樣其他人也能看到它。
via: https://syslog.ravelin.com/anatomy-of-a-function-call-in-go-f6fc81b80ecc
作者:Phil Pearl 譯者:krystollia 校對:rxcai
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
484 次點擊