1. 程式人生 > >Golang原始碼學習:使用gdb除錯探究Golang函式呼叫棧結構

Golang原始碼學習:使用gdb除錯探究Golang函式呼叫棧結構

本文所使用的golang為1.14,gdb為8.1。 一直以來對於函式呼叫都僅限於函式呼叫棧這個概念上,但對於其中的詳細結構卻瞭解不多。所以用gdb除錯一個簡單的例子,一探究竟。 ## 函式呼叫棧的結構(以下簡稱棧) 棧包含以下作用: - 儲存函式返回地址。 - 儲存呼叫者的rbp。 - 儲存區域性變數。 - 為被呼叫函式預留返回值記憶體空間。 - 向被呼叫函式傳遞引數。 每個函式在執行時都需要一段記憶體來儲存上述的內容,這段記憶體被稱為函式的“**棧幀**” 一般CPU中包含兩個與棧相關的暫存器: - rsp:**始終指向整個函式呼叫棧的棧頂** - rbp:**指向棧幀的開始位置** 但儲存函式返回地址的記憶體單元的地址並不在rbp~rsp之間。而是在0x8(%rbp)的位置 ## 棧的工作原理 棧是一種後進先出(LIFO)的結構,在Linux AMD64環境中,golang棧由高地址向低地址生長。 當發生函式呼叫時,由於呼叫者未執行完成,棧幀還要繼續使用,不可以被呼叫者覆蓋,所以要在當前棧頂外繼續為被呼叫者劃分棧幀。這個操作叫做壓棧(push),並向外移動rbp、rsp,棧空間隨之增長。 與之對應的,當被呼叫者執行完成時,其棧幀就會被收回。這個操作叫出棧(pop),並向內移動rbp、rsp,棧空間隨之縮小。呼叫者繼續執行 棧空間的生長和收縮是由編譯器生成的程式碼自動管理的的,與堆不同(手動或者gc)。 ## 流程圖 先給出流程圖,好心裡有個數: ## 程式碼及編譯 指定 -gcflags="-N -l" 是為了關閉編譯器優化。 ``` go build -gcflags="-N -l" -o test test.go ``` 為了方便檢視記憶體內容,將變數都宣告為了int64。 ``` package main func main() { caller() } func caller() { var a int64 = 1 var b int64 = 2 callee(a, b) } func callee(a, b int64) (int64, int64) { c := a + 5 d := b * 4 return c, d } ``` ## 反彙編程式碼 反彙編的內容為: - 指令地址 - 指令相對於當前函式起始位置以位元組為單位的偏移 - 指令內容 ``` gdb test ``` 斷點打在caller方法上,因為主要的研究物件是caller與callee。 ``` (gdb) b main.caller Breakpoint 1 at 0x458360: file /root/study/test.go, line 7. ``` 輸入run 執行程式。 caller函式反彙編,/s 表示將原始碼與彙編程式碼一起顯示,如不指定則只顯示彙編程式碼。 可使用step(s)按原始碼級別除錯,或者stepi(si)按彙編指令級別除錯。 下面是caller、callee的反彙編程式碼和原始碼註釋,還有與之相關的記憶體結構對照表。 ``` (gdb) disassemble /s Dump of assembler code for function main.caller: 7 func caller() { =>
0x0000000000458360 <+0>: mov %fs:0xfffffffffffffff8,%rcx # 將當前g的指標存入rcx 0x0000000000458369 <+9>: cmp 0x10(%rcx),%rsp # 比較g.stackguard0和rsp 0x000000000045836d <+13>: jbe 0x4583b0 # 如果rsp較小,表示棧有溢位風險,呼叫runtime.morestack_noctxt 0x000000000045836f <+15>: sub $0x38,%rsp # 劃分0x38位元組的棧空間 0x0000000000458373 <+19>: mov %rbp,0x30(%rsp) # 儲存呼叫者main的rbp 0x0000000000458378 <+24>: lea 0x30(%rsp),%rbp # 設定此函式棧的rbp 8 var a int64 = 1 0x000000000045837d <+29>: movq $0x1,0x28(%rsp) # 區域性變數a入棧 9 var b int64 = 2 0x0000000000458386 <+38>: movq $0x2,0x20(%rsp) # 區域性變數b入棧 10 callee(a, b) 0x000000000045838f <+47>: mov 0x28(%rsp),%rax # 讀取第一個引數到rax 0x0000000000458394 <+52>: mov %rax,(%rsp) # callee第一個引數入棧 0x0000000000458398 <+56>: movq $0x2,0x8(%rsp) # callee第二個引數入棧 0x00000000004583a1 <+65>: callq 0x4583c0 # 呼叫callee 11 } 0x00000000004583a6 <+70>: mov 0x30(%rsp),%rbp # rbp還原為main的rbp 0x00000000004583ab <+75>: add $0x38,%rsp # rsp還原為main的rsp 0x00000000004583af <+79>: retq # 返回 : 0x00000000004583b0 <+80>: callq 0x451b30 0x00000000004583b5 <+85>: jmp 0x458360 End of assembler dump. ``` callee函式反彙編 ``` (gdb) s # 單步除錯進入的callee函式 main.callee (a=1, b=2, ~r2=824634073176, ~r3=0) at /root/study/test.go:13 13 func callee(a, b int64) (int64, int64) { (gdb) disassemble /s Dump of assembler code for function main.callee: 13 func callee(a, b int64) (int64, int64) { => 0x00000000004583c0 <+0>: sub $0x18,%rsp # 劃分0x18大小的棧 0x00000000004583c4 <+4>: mov %rbp,0x10(%rsp) # 儲存呼叫者caller的rbp 0x00000000004583c9 <+9>: lea 0x10(%rsp),%rbp # 設定此函式棧的rbp 0x00000000004583ce <+14>: movq $0x0,0x30(%rsp) # 初始化第一個返回值為0 0x00000000004583d7 <+23>: movq $0x0,0x38(%rsp) # 初始化第二個返回值為0 14 c := a + 5 0x00000000004583e0 <+32>: mov 0x20(%rsp),%rax # 從記憶體中獲取第一個引數值到rax 0x00000000004583e5 <+37>: add $0x5,%rax # rax+=5 0x00000000004583e9 <+41>: mov %rax,0x8(%rsp) # 區域性變數c入棧 15 d := b * 4 0x00000000004583ee <+46>: mov 0x28(%rsp),%rax # 從記憶體中獲取第二個引數值到rax 0x00000000004583f3 <+51>: shl $0x2,%rax # rax*=2 0x00000000004583f7 <+55>: mov %rax,(%rsp) # 區域性變數d入棧 16 return c, d 0x00000000004583fb <+59>: mov 0x8(%rsp),%rax # 區域性變數c的值儲存到rax 0x0000000000458400 <+64>: mov %rax,0x30(%rsp) # 將c賦值給第一個返回值 0x0000000000458405 <+69>: mov (%rsp),%rax # 區域性變數d的值儲存到rax 0x0000000000458409 <+73>: mov %rax,0x38(%rsp) # 將d賦值給第二個返回值 17 } 0x000000000045840e <+78>: mov 0x10(%rsp),%rbp # rbp還原為caller的rbp 0x0000000000458413 <+83>: add $0x18,%rsp # rsp還原為caller的rsp 0x0000000000458417 <+87>: retq # 返回 End of assembler dump. ``` ## 記憶體結構對照表
## 一些結論 - golang通過rsp加偏移量訪問棧幀。 - 被呼叫者的入參是位於呼叫者的棧中。 - caller會為有返回值的callee,在棧中預留返回值記憶體空間。而callee在執行return時,會將返回值寫入caller在棧中預留的空間。 - 意外收穫是瞭解了多值返回的