原創文章,歡迎轉載,轉載請註明出處,謝謝。
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 位元組的記憶體空間。繼續往下執行,分別將 0x1
,0x2
和 0x3
放到 [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
...
繼續往下分析指令的執行過程:
sub rsp, 0x28
:rsp
的記憶體地址減0x28
,意味著 main 函式開闢0x28
位元組的棧空間。mov qword ptr [rsp+0x18], 0x1
和mov qword ptr [rsp+0x10], 0x2
:將0x1
和0x2
分別放到記憶體地址[rsp+0x18]
和[rsp+0x10]
中。mov eax, 0x1
和mov ebx, 0x2
:將0x1
和0x2
分別放到暫存器eax
和ebx
中。
跳轉到 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
前,讓我們先看下記憶體分佈:
(綠色部分表示 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 個位元組空間。繼續往下分析指令:
push rbp
:將rbp
暫存器的值壓棧,rbp 中儲存的是地址0x000000c00003e758
。由於進行了壓棧操作,這裡的Rsp
會往下減 8 個位元組。mov rbp, rsp
:將當前 rsp 的值給rbp
,rbp
為sum
函式棧的棧底。sub rsp, 0x10
:rsp
往下減0X10
個位元組,開闢16 個位元組的空間,做為sum
的函式棧,此時rsp
的地址為0x000000c00003e710
,表示函式棧的棧頂。
執行到這裡,我們畫出記憶體分佈圖如下:
繼續往下分析:
mov qword ptr [rsp+0x20], rax
和mov qword ptr [rsp+0x28], rbx
:分別將rax
暫存器的值 1 放到[rsp+0x20]:0x000000c00003e730
,rbx
暫存器的值 2 放到[rsp+0x28]:0x000000c00003e738
。mov qword ptr [rsp], 0x0
:將 0 放到[rsp]
中。add rax, rbx
:將 rax 和 rbx 的值相加,結果放到 rax 中,相加後 rax 中的值為 3。mov qword ptr [rsp+0x8], rax
:將 3 放到[rsp+0x8]
中。mov qword ptr [rsp], rax
:將 3 放到[rsp]
中。
根據上述分析,畫出記憶體分佈圖如下:
可以看出,傳給 sum 的形參 x 和 y 實際是在 main 函式棧分配的。
繼續往下執行:
add rsp, 0x10
:rsp
暫存器加0x10
回收sum
棧空間。pop rbp
:將儲存在0x000000c00003e720
的值0x000000c00003e758
移到rbp
中。ret
:sum
函式返回。
在執行 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
指向了下一條指令的位置。
繼續往下執行:
mov qword ptr [rsp+0x20], rax
:將 3 放到[rsp+0x20]
中,[rsp+0x20]
就是存放sum
函式返回值的記憶體地址。call $runtime.printint
:呼叫runtime.printint
列印返回值 3。
分析完上述呼叫函式的過程我們可以畫出函式棧呼叫的完整記憶體分佈如下:
2. 小結
本文從彙編角度看函式呼叫的過程,力圖做到對函式呼叫有個比較通透的瞭解。