Go plan9 彙編:說透函式棧

hxia043發表於2024-09-02

原創文章,歡迎轉載,轉載請註明出處,謝謝。


Go plan9 彙編系列文章:

  • Go plan9 彙編: 打通應用到底層的任督二脈
  • Go plan9 彙編:手寫彙編
  • Go plan9 彙編:說透函式棧
  • Go plan9 彙編:記憶體對齊和遞迴

0. 前言

函式是 Go 的一級公民,本文從彙編角度出發看看我們常用的一些函式在幹什麼。

1. 函式

1.1 main 函式

在 main 函式中計算兩數之和如下:

package main

func main() {
	x, y := 1, 2
	z := x + y
	print(z)
}

使用 dlv 除錯函式(不瞭解 dlv 的請看 Go plan9 彙編: 打通應用到底層的任督二脈):

# dlv debug
Type 'help' for list of commands.
(dlv) b main.main
Breakpoint 1 set at 0x45feca for main.main() ./ex4.go:3
(dlv) c
> main.main() ./ex4.go:3 (hits goroutine(1):1 total:1) (PC: 0x45feca)
     1: package main
     2:
=>   3: func main() {
     4:         x, y := 1, 2
     5:         z := x + y
     6:         print(z)
     7: }

disass 檢視對應的彙編指令:

(dlv) 
TEXT main.main(SB) /root/go/src/foundation/ex4/ex4.go
        ex4.go:3        0x45fec0        493b6610                cmp rsp, qword ptr [r14+0x10]
        ex4.go:3        0x45fec4        763d                    jbe 0x45ff03
        ex4.go:3        0x45fec6        55                      push rbp
        ex4.go:3        0x45fec7        4889e5                  mov rbp, rsp
=>      ex4.go:3        0x45feca*       4883ec20                sub rsp, 0x20
        ex4.go:4        0x45fece        48c744241801000000      mov qword ptr [rsp+0x18], 0x1
        ex4.go:4        0x45fed7        48c744241002000000      mov qword ptr [rsp+0x10], 0x2
        ex4.go:5        0x45fee0        48c744240803000000      mov qword ptr [rsp+0x8], 0x3
        ex4.go:6        0x45fee9        e8d249fdff              call $runtime.printlock
        ex4.go:6        0x45feee        488b442408              mov rax, qword ptr [rsp+0x8]
        ex4.go:6        0x45fef3        e86850fdff              call $runtime.printint
        ex4.go:6        0x45fef8        e8234afdff              call $runtime.printunlock
        ex4.go:7        0x45fefd        4883c420                add rsp, 0x20
        ex4.go:7        0x45ff01        5d                      pop rbp
        ex4.go:7        0x45ff02        c3                      ret
        ex4.go:3        0x45ff03        e8d8cdffff              call $runtime.morestack_noctxt
        ex4.go:3        0x45ff08        ebb6                    jmp $main.main
(dlv) regs
    Rsp = 0x000000c00003e758

相信看過 Go plan9 彙編: 打通應用到底層的任督二脈 的同學對上述彙編指令已經有一定了解的。

這裡進入 main 函式,執行到 sub rsp, 0x20 指令,該指令為 main 函式開闢 0x20 位元組的記憶體空間。繼續往下執行,分別將 0x10x20x3 放到 [rsp+0x18][rsp+0x10][rsp+0x8] 處(從彙編指令好像沒看到 z := x + y 的加法,合理懷疑是編譯器做了最佳化)。

繼續,mov rax, qword ptr [rsp+0x8][rsp+0x8] 地址的值 0x3 放到 rax 暫存器中。然後,呼叫 call $runtime.printint 列印 rax 的值。實現輸出兩數之後。後續的指令我們就跳過了,不在贅述。

1.2 函式呼叫

在 main 函式中實現兩數之和,我們沒辦法看到函式呼叫的過程。
接下來,定義 sum 函式實現兩數之和,在 main 函式中呼叫 sum。重點看函式在呼叫時做了什麼。

示例如下:

package main

func main() {
	a, b := 1, 2
	println(sum(a, b))
}

func sum(x, y int) int {
	z := x + y
	return z
}

使用 dlv 除錯函式:

# dlv debug
Type 'help' for list of commands.
(dlv) b main.main
Breakpoint 1 set at 0x45feca for main.main() ./ex6.go:3
(dlv) c
> main.main() ./ex6.go:3 (hits goroutine(1):1 total:1) (PC: 0x45feca)
     1: package main
     2:
=>   3: func main() {
     4:         a, b := 1, 2
     5:         println(sum(a, b))
     6: }
     7:
     8: func sum(x, y int) int {
(dlv) disass
Sending output to pager...
TEXT main.main(SB) /root/go/src/foundation/ex6/ex6.go
        ex6.go:3        0x45fec0        493b6610                cmp rsp, qword ptr [r14+0x10]
        ex6.go:3        0x45fec4        764f                    jbe 0x45ff15
        ex6.go:3        0x45fec6        55                      push rbp
        ex6.go:3        0x45fec7        4889e5                  mov rbp, rsp
=>      ex6.go:3        0x45feca*       4883ec28                sub rsp, 0x28
        ex6.go:4        0x45fece        48c744241801000000      mov qword ptr [rsp+0x18], 0x1
        ex6.go:4        0x45fed7        48c744241002000000      mov qword ptr [rsp+0x10], 0x2
        ex6.go:5        0x45fee0        b801000000              mov eax, 0x1
        ex6.go:5        0x45fee5        bb02000000              mov ebx, 0x2
        ex6.go:5        0x45feea        e831000000              call $main.sum
        ex6.go:5        0x45feef        4889442420              mov qword ptr [rsp+0x20], rax
        ex6.go:5        0x45fef4        e8c749fdff              call $runtime.printlock
        ex6.go:5        0x45fef9        488b442420              mov rax, qword ptr [rsp+0x20]

regs 檢視暫存器狀態:

(dlv) regs
    Rip = 0x000000000045feca
    Rsp = 0x000000c00003e758
    Rbp = 0x000000c00003e758
    ...

繼續往下分析指令的執行過程:

  1. sub rsp, 0x28: rsp 的記憶體地址減 0x28,意味著 main 函式開闢 0x28 位元組的棧空間。
  2. mov qword ptr [rsp+0x18], 0x1mov qword ptr [rsp+0x10], 0x2:將 0x10x2 分別放到記憶體地址 [rsp+0x18][rsp+0x10] 中。
  3. mov eax, 0x1mov ebx, 0x2:將 0x10x2 分別放到暫存器 eaxebx 中。

跳轉到 0x45feea 指令:

(dlv) b *0x45feea
Breakpoint 2 set at 0x45feea for main.main() ./ex6.go:5
(dlv) c
> main.main() ./ex6.go:5 (hits goroutine(1):1 total:1) (PC: 0x45feea)
     1: package main
     2:
     3: func main() {
     4:         a, b := 1, 2
=>   5:         println(sum(a, b))
     6: }
     7:
     8: func sum(x, y int) int {
     9:         z := x + y
    10:         return z
(dlv) disass
Sending output to pager...
TEXT main.main(SB) /root/go/src/foundation/ex6/ex6.go
        ex6.go:3        0x45fec0        493b6610                cmp rsp, qword ptr [r14+0x10]
        ex6.go:3        0x45fec4        764f                    jbe 0x45ff15
        ex6.go:3        0x45fec6        55                      push rbp
        ex6.go:3        0x45fec7        4889e5                  mov rbp, rsp
        ex6.go:3        0x45feca*       4883ec28                sub rsp, 0x28
        ex6.go:4        0x45fece        48c744241801000000      mov qword ptr [rsp+0x18], 0x1
        ex6.go:4        0x45fed7        48c744241002000000      mov qword ptr [rsp+0x10], 0x2
        ex6.go:5        0x45fee0        b801000000              mov eax, 0x1
        ex6.go:5        0x45fee5        bb02000000              mov ebx, 0x2
=>      ex6.go:5        0x45feea*       e831000000              call $main.sum
        ex6.go:5        0x45feef        4889442420              mov qword ptr [rsp+0x20], rax
        ex6.go:5        0x45fef4        e8c749fdff              call $runtime.printlock
        ex6.go:5        0x45fef9        488b442420              mov rax, qword ptr [rsp+0x20]
        ex6.go:5        0x45fefe        6690                    data16 nop

在執行 call $main.sum 前,讓我們先看下記憶體分佈:

image

(綠色部分表示 main 函式棧)

繼續執行 call $main.sum:

(dlv) si
> main.sum() ./ex6.go:8 (PC: 0x45ff20)
TEXT main.sum(SB) /root/go/src/foundation/ex6/ex6.go
=>      ex6.go:8        0x45ff20        55                      push rbp
        ex6.go:8        0x45ff21        4889e5                  mov rbp, rsp
        ex6.go:8        0x45ff24        4883ec10                sub rsp, 0x10
        ex6.go:8        0x45ff28        4889442420              mov qword ptr [rsp+0x20], rax
        ex6.go:8        0x45ff2d        48895c2428              mov qword ptr [rsp+0x28], rbx
        ex6.go:8        0x45ff32        48c7042400000000        mov qword ptr [rsp], 0x0
        ex6.go:9        0x45ff3a        4801d8                  add rax, rbx
        ex6.go:9        0x45ff3d        4889442408              mov qword ptr [rsp+0x8], rax
        ex6.go:10       0x45ff42        48890424                mov qword ptr [rsp], rax
        ex6.go:10       0x45ff46*       4883c410                add rsp, 0x10
        ex6.go:10       0x45ff4a        5d                      pop rbp
        ex6.go:10       0x45ff4b        c3                      ret
(dlv) regs
    Rip = 0x000000000045ff20
    Rsp = 0x000000c00003e728
    Rbp = 0x000000c00003e758

可以看到,Rsp 暫存器往下減 8 個位元組,壓棧開闢 8 個位元組空間。繼續往下分析指令:

  1. push rbp:將 rbp 暫存器的值壓棧,rbp 中儲存的是地址 0x000000c00003e758。由於進行了壓棧操作,這裡的 Rsp 會往下減 8 個位元組。
  2. mov rbp, rsp:將當前 rsp 的值給 rbprbpsum 函式棧的棧底。
  3. sub rsp, 0x10rsp 往下減 0X10 個位元組,開闢16 個位元組的空間,做為 sum 的函式棧,此時 rsp 的地址為 0x000000c00003e710,表示函式棧的棧頂。

執行到這裡,我們畫出記憶體分佈圖如下:

image

繼續往下分析:

  1. mov qword ptr [rsp+0x20], raxmov qword ptr [rsp+0x28], rbx:分別將 rax 暫存器的值 1 放到 [rsp+0x20]:0x000000c00003e730rbx 暫存器的值 2 放到 [rsp+0x28]:0x000000c00003e738
  2. mov qword ptr [rsp], 0x0:將 0 放到 [rsp] 中。
  3. add rax, rbx:將 rax 和 rbx 的值相加,結果放到 rax 中,相加後 rax 中的值為 3。
  4. mov qword ptr [rsp+0x8], rax:將 3 放到 [rsp+0x8] 中。
  5. mov qword ptr [rsp], rax:將 3 放到 [rsp] 中。

根據上述分析,畫出記憶體分佈圖如下:

image

可以看出,傳給 sum 的形參 x 和 y 實際是在 main 函式棧分配的。

繼續往下執行:

  1. add rsp, 0x10rsp 暫存器加 0x10 回收 sum 棧空間。
  2. pop rbp:將儲存在 0x000000c00003e720 的值 0x000000c00003e758 移到 rbp 中。
  3. retsum 函式返回。

在執行 ret 指令前最後看下暫存器的狀態:

(dlv) regs
    Rip = 0x000000000045ff4b
    Rsp = 0x000000c00003e728
    Rbp = 0x000000c00003e758

我們知道 Rip 暫存器儲存的是執行指令所在的記憶體地址,那麼問題就來了,當函式返回時,要執行呼叫函式的下一條指令:

TEXT main.sum(SB) /root/go/src/foundation/ex6/ex6.go
        ex6.go:5        0x45feea*       e831000000              call $main.sum
        ex6.go:5        0x45feef        4889442420              mov qword ptr [rsp+0x20], rax

這裡我們需要 main.sum 返回後執行的下一條指令是 mov qword ptr [rsp+0x20], rax。可是 Rip 指令怎麼獲得指令所在的地址 0x45feef 呢?

答案在 call $main.sum 這裡,這條指令會將下一條指令壓棧,在 sum 函式呼叫 ret 返回時,將之前壓棧的指令移到 Rip 暫存器中。這個壓棧的記憶體地址是 0x000000c00003e728,檢視其中的內容:

(dlv) print *(*int)(uintptr(0x000000c00003e728))
4587247

4587247 的十六進位制就是 0x45feef

執行 ret

(dlv) si
> main.main() ./ex6.go:5 (PC: 0x45feef)
        ex6.go:4        0x45fece        48c744241801000000      mov qword ptr [rsp+0x18], 0x1
        ex6.go:4        0x45fed7        48c744241002000000      mov qword ptr [rsp+0x10], 0x2
        ex6.go:5        0x45fee0        b801000000              mov eax, 0x1
        ex6.go:5        0x45fee5        bb02000000              mov ebx, 0x2
        ex6.go:5        0x45feea*       e831000000              call $main.sum
=>      ex6.go:5        0x45feef        4889442420              mov qword ptr [rsp+0x20], rax
        ex6.go:5        0x45fef4        e8c749fdff              call $runtime.printlock
        ex6.go:5        0x45fef9        488b442420              mov rax, qword ptr [rsp+0x20]
        ex6.go:5        0x45fefe        6690                    data16 nop
        ex6.go:5        0x45ff00        e85b50fdff              call $runtime.printint
        ex6.go:5        0x45ff05        e8f64bfdff              call $runtime.printnl
(dlv) regs
    Rip = 0x000000000045feef
    Rsp = 0x000000c00003e730
    Rbp = 0x000000c00003e758

可以看到 Rip 指向了下一條指令的位置。

繼續往下執行:

  1. mov qword ptr [rsp+0x20], rax:將 3 放到 [rsp+0x20] 中,[rsp+0x20] 就是存放 sum 函式返回值的記憶體地址。
  2. call $runtime.printint:呼叫 runtime.printint 列印返回值 3。

分析完上述呼叫函式的過程我們可以畫出函式棧呼叫的完整記憶體分佈如下:

image

2. 小結

本文從彙編角度看函式呼叫的過程,力圖做到對函式呼叫有個比較通透的瞭解。


相關文章