前言
函数是 Go 的一级公民,本文从汇编角度出发看看我们常用的一些函数在干什么。
函数
main 函数
在 main 函数中计算两数之和如下:
1package main
2
3func main() {
4 x, y := 1, 2
5 z := x + y
6 print(z)
7}
使用 dlv 调试函数(不了解 dlv 的请看 Go plan9 汇编: 打通应用到底层的任督二脉):
1# dlv debug
2Type 'help' for list of commands.
3(dlv) b main.main
4Breakpoint 1 set at 0x45feca for main.main() ./ex4.go:3
5(dlv) c
6> main.main() ./ex4.go:3 (hits goroutine(1):1 total:1) (PC: 0x45feca)
7 1: package main
8 2:
9=> 3: func main() {
10 4: x, y := 1, 2
11 5: z := x + y
12 6: print(z)
13 7: }
disass 查看对应的汇编指令:
1(dlv)
2TEXT main.main(SB) /root/go/src/foundation/ex4/ex4.go
3 ex4.go:3 0x45fec0 493b6610 cmp rsp, qword ptr [r14+0x10]
4 ex4.go:3 0x45fec4 763d jbe 0x45ff03
5 ex4.go:3 0x45fec6 55 push rbp
6 ex4.go:3 0x45fec7 4889e5 mov rbp, rsp
7=> ex4.go:3 0x45feca* 4883ec20 sub rsp, 0x20
8 ex4.go:4 0x45fece 48c744241801000000 mov qword ptr [rsp+0x18], 0x1
9 ex4.go:4 0x45fed7 48c744241002000000 mov qword ptr [rsp+0x10], 0x2
10 ex4.go:5 0x45fee0 48c744240803000000 mov qword ptr [rsp+0x8], 0x3
11 ex4.go:6 0x45fee9 e8d249fdff call $runtime.printlock
12 ex4.go:6 0x45feee 488b442408 mov rax, qword ptr [rsp+0x8]
13 ex4.go:6 0x45fef3 e86850fdff call $runtime.printint
14 ex4.go:6 0x45fef8 e8234afdff call $runtime.printunlock
15 ex4.go:7 0x45fefd 4883c420 add rsp, 0x20
16 ex4.go:7 0x45ff01 5d pop rbp
17 ex4.go:7 0x45ff02 c3 ret
18 ex4.go:3 0x45ff03 e8d8cdffff call $runtime.morestack_noctxt
19 ex4.go:3 0x45ff08 ebb6 jmp $main.main
20(dlv) regs
21 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 的值。实现输出两数之后。后续的指令我们就跳过了,不在赘述。
函数调用
在 main 函数中实现两数之和,我们没办法看到函数调用的过程。
接下来,定义 sum 函数实现两数之和,在 main 函数中调用 sum。重点看函数在调用时做了什么。
示例如下:
1package main
2
3func main() {
4 a, b := 1, 2
5 println(sum(a, b))
6}
7
8func sum(x, y int) int {
9 z := x + y
10 return z
11}
使用 dlv 调试函数:
1# dlv debug
2Type 'help' for list of commands.
3(dlv) b main.main
4Breakpoint 1 set at 0x45feca for main.main() ./ex6.go:3
5(dlv) c
6> main.main() ./ex6.go:3 (hits goroutine(1):1 total:1) (PC: 0x45feca)
7 1: package main
8 2:
9=> 3: func main() {
10 4: a, b := 1, 2
11 5: println(sum(a, b))
12 6: }
13 7:
14 8: func sum(x, y int) int {
15(dlv) disass
16Sending output to pager...
17TEXT main.main(SB) /root/go/src/foundation/ex6/ex6.go
18 ex6.go:3 0x45fec0 493b6610 cmp rsp, qword ptr [r14+0x10]
19 ex6.go:3 0x45fec4 764f jbe 0x45ff15
20 ex6.go:3 0x45fec6 55 push rbp
21 ex6.go:3 0x45fec7 4889e5 mov rbp, rsp
22=> ex6.go:3 0x45feca* 4883ec28 sub rsp, 0x28
23 ex6.go:4 0x45fece 48c744241801000000 mov qword ptr [rsp+0x18], 0x1
24 ex6.go:4 0x45fed7 48c744241002000000 mov qword ptr [rsp+0x10], 0x2
25 ex6.go:5 0x45fee0 b801000000 mov eax, 0x1
26 ex6.go:5 0x45fee5 bb02000000 mov ebx, 0x2
27 ex6.go:5 0x45feea e831000000 call $main.sum
28 ex6.go:5 0x45feef 4889442420 mov qword ptr [rsp+0x20], rax
29 ex6.go:5 0x45fef4 e8c749fdff call $runtime.printlock
30 ex6.go:5 0x45fef9 488b442420 mov rax, qword ptr [rsp+0x20]
regs 查看寄存器状态:
1(dlv) regs
2 Rip = 0x000000000045feca
3 Rsp = 0x000000c00003e758
4 Rbp = 0x000000c00003e758
5 ...
继续往下分析指令的执行过程:
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 指令:
1(dlv) b *0x45feea
2Breakpoint 2 set at 0x45feea for main.main() ./ex6.go:5
3(dlv) c
4> main.main() ./ex6.go:5 (hits goroutine(1):1 total:1) (PC: 0x45feea)
5 1: package main
6 2:
7 3: func main() {
8 4: a, b := 1, 2
9=> 5: println(sum(a, b))
10 6: }
11 7:
12 8: func sum(x, y int) int {
13 9: z := x + y
14 10: return z
15(dlv) disass
16Sending output to pager...
17TEXT main.main(SB) /root/go/src/foundation/ex6/ex6.go
18 ex6.go:3 0x45fec0 493b6610 cmp rsp, qword ptr [r14+0x10]
19 ex6.go:3 0x45fec4 764f jbe 0x45ff15
20 ex6.go:3 0x45fec6 55 push rbp
21 ex6.go:3 0x45fec7 4889e5 mov rbp, rsp
22 ex6.go:3 0x45feca* 4883ec28 sub rsp, 0x28
23 ex6.go:4 0x45fece 48c744241801000000 mov qword ptr [rsp+0x18], 0x1
24 ex6.go:4 0x45fed7 48c744241002000000 mov qword ptr [rsp+0x10], 0x2
25 ex6.go:5 0x45fee0 b801000000 mov eax, 0x1
26 ex6.go:5 0x45fee5 bb02000000 mov ebx, 0x2
27=> ex6.go:5 0x45feea* e831000000 call $main.sum
28 ex6.go:5 0x45feef 4889442420 mov qword ptr [rsp+0x20], rax
29 ex6.go:5 0x45fef4 e8c749fdff call $runtime.printlock
30 ex6.go:5 0x45fef9 488b442420 mov rax, qword ptr [rsp+0x20]
31 ex6.go:5 0x45fefe 6690 data16 nop
在执行 call $main.sum 前,让我们先看下内存分布:

(绿色部分表示 main 函数栈)
继续执行 call $main.sum:
1(dlv) si
2> main.sum() ./ex6.go:8 (PC: 0x45ff20)
3TEXT main.sum(SB) /root/go/src/foundation/ex6/ex6.go
4=> ex6.go:8 0x45ff20 55 push rbp
5 ex6.go:8 0x45ff21 4889e5 mov rbp, rsp
6 ex6.go:8 0x45ff24 4883ec10 sub rsp, 0x10
7 ex6.go:8 0x45ff28 4889442420 mov qword ptr [rsp+0x20], rax
8 ex6.go:8 0x45ff2d 48895c2428 mov qword ptr [rsp+0x28], rbx
9 ex6.go:8 0x45ff32 48c7042400000000 mov qword ptr [rsp], 0x0
10 ex6.go:9 0x45ff3a 4801d8 add rax, rbx
11 ex6.go:9 0x45ff3d 4889442408 mov qword ptr [rsp+0x8], rax
12 ex6.go:10 0x45ff42 48890424 mov qword ptr [rsp], rax
13 ex6.go:10 0x45ff46* 4883c410 add rsp, 0x10
14 ex6.go:10 0x45ff4a 5d pop rbp
15 ex6.go:10 0x45ff4b c3 ret
16(dlv) regs
17 Rip = 0x000000000045ff20
18 Rsp = 0x000000c00003e728
19 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 指令前最后看下寄存器的状态:
1(dlv) regs
2 Rip = 0x000000000045ff4b
3 Rsp = 0x000000c00003e728
4 Rbp = 0x000000c00003e758
我们知道 Rip 寄存器存储的是运行指令所在的内存地址,那么问题就来了,当函数返回时,要执行调用函数的下一条指令:
1TEXT main.sum(SB) /root/go/src/foundation/ex6/ex6.go
2 ex6.go:5 0x45feea* e831000000 call $main.sum
3 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,查看其中的内容:
1(dlv) print *(*int)(uintptr(0x000000c00003e728))
24587247
4587247 的十六进制就是 0x45feef。
执行 ret:
1(dlv) si
2> main.main() ./ex6.go:5 (PC: 0x45feef)
3 ex6.go:4 0x45fece 48c744241801000000 mov qword ptr [rsp+0x18], 0x1
4 ex6.go:4 0x45fed7 48c744241002000000 mov qword ptr [rsp+0x10], 0x2
5 ex6.go:5 0x45fee0 b801000000 mov eax, 0x1
6 ex6.go:5 0x45fee5 bb02000000 mov ebx, 0x2
7 ex6.go:5 0x45feea* e831000000 call $main.sum
8=> ex6.go:5 0x45feef 4889442420 mov qword ptr [rsp+0x20], rax
9 ex6.go:5 0x45fef4 e8c749fdff call $runtime.printlock
10 ex6.go:5 0x45fef9 488b442420 mov rax, qword ptr [rsp+0x20]
11 ex6.go:5 0x45fefe 6690 data16 nop
12 ex6.go:5 0x45ff00 e85b50fdff call $runtime.printint
13 ex6.go:5 0x45ff05 e8f64bfdff call $runtime.printnl
14(dlv) regs
15 Rip = 0x000000000045feef
16 Rsp = 0x000000c00003e730
17 Rbp = 0x000000c00003e758
可以看到 Rip 指向了下一条指令的位置。
继续往下执行:
mov qword ptr [rsp+0x20], rax:将 3 放到[rsp+0x20]中,[rsp+0x20]就是存放sum函数返回值的内存地址。call $runtime.printint:调用runtime.printint打印返回值 3。
分析完上述调用函数的过程我们可以画出函数栈调用的完整内存分布如下:

小结
本文从汇编角度看函数调用的过程,力图做到对函数调用有个比较通透的了解。