0%

《Golang》函数调用栈解析

下面根据例子讲解函数调用栈的原理:

1
2
3
4
5
6
7
8
9
10
11
12
package main

func incr(a int64) (int64, int64) {
return a, 1
}

func main() {
var a, b int64
a, b = incr(a)
a++
b++
}

首先分析下面 main 函数的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
main.go:7             0x105e990               65488b0c2530000000      MOVQ GS:0x30, CX     // GS 是指向线程本地存储(TLS)的指针                     
main.go:7 0x105e999 483b6110 CMPQ 0x10(CX), SP // 0x10(CX) 就是 16(CX) 指向了 g.stackguard0
main.go:7 0x105e99d 767a JBE 0x105ea19 // 这里往前都是检查栈
main.go:7 0x105e99f 4883ec40 SUBQ $0x40, SP // 分配栈帧大小为 64(0x40)个字节
main.go:7 0x105e9a3 48896c2438 MOVQ BP, 0x38(SP) // 将 main 调用者栈基内容放到 0x38(SP) 位置,以备恢复。占用范围 56~64 8 个字节
main.go:7 0x105e9a8 488d6c2438 LEAQ 0x38(SP), BP // 0x38(SP) 位置设置为 main 函数的栈基
main.go:8 0x105e9ad 48c744242000000000 MOVQ $0x0, 0x20(SP) // 局部变量 a 的初始值 0 放到 0x20(SP) 位置。占用范围 32~40 8 个字节
main.go:8 0x105e9b6 48c744241800000000 MOVQ $0x0, 0x18(SP) // 局部变量 b 的初始值 0 放到 0x18(SP) 位置。占用范围 24~32 8 个字节
main.go:9 0x105e9bf 488b442420 MOVQ 0x20(SP), AX // 将 a 的值拷贝到 AX 寄存器
main.go:9 0x105e9c4 48890424 MOVQ AX, 0(SP) // 将 AX 中的值放到 0(SP) 位置作为 incr 的参数。占用范围 0~8 8 个字节
main.go:9 0x105e9c8 e893ffffff CALL main.incr(SB) // 调用 incr 函数,SP 再减 8 个字节,将返回地址放入范围 -8~0,所以 main 函数栈帧是范围 56~-8 64 个字节
main.go:9 0x105e9cd 488b442408 MOVQ 0x8(SP), AX // 取出第一个返回值。范围是 8~16 8 个字节
main.go:9 0x105e9d2 4889442430 MOVQ AX, 0x30(SP) // 第一个返回值放到 0x30(SP),占用范围 48~56 8 个字节
main.go:9 0x105e9d7 488b442410 MOVQ 0x10(SP), AX // 取出第二个返回值。范围是 16-24 8 个字节
main.go:9 0x105e9dc 4889442428 MOVQ AX, 0x28(SP) // 第二个返回值放到 0x28(SP),占用范围 40~48 8 个字节
main.go:9 0x105e9e1 488b442430 MOVQ 0x30(SP), AX // incr 第一个返回放入 AX
main.go:9 0x105e9e6 4889442420 MOVQ AX, 0x20(SP) // incr 的第一个返回值赋值给 a
main.go:9 0x105e9eb 488b442428 MOVQ 0x28(SP), AX // incr 第二个返回放入 AX
main.go:9 0x105e9f0 4889442418 MOVQ AX, 0x18(SP) // incr 的第二个返回值赋值给 b
main.go:10 0x105e9f5 488b442420 MOVQ 0x20(SP), AX // 0x20(SP) 中 a 的值放入 AX 寄存器
main.go:10 0x105e9fa 48ffc0 INCQ AX // a++
main.go:10 0x105e9fd 4889442420 MOVQ AX, 0x20(SP) // 加完后赋值给 a
main.go:11 0x105ea02 488b442418 MOVQ 0x18(SP), AX // 0x18(SP) 中 b 的值放入 AX 寄存器
main.go:11 0x105ea07 48ffc0 INCQ AX // b++
main.go:11 0x105ea0a 4889442418 MOVQ AX, 0x18(SP) // 加完后赋值给 b
main.go:12 0x105ea0f 488b6c2438 MOVQ 0x38(SP), BP // 恢复调用者的栈基
main.go:12 0x105ea14 4883c440 ADDQ $0x40, SP // 销毁 main 函数的栈帧
main.go:12 0x105ea18 c3 RET // main 函数返回
main.go:7 0x105ea19 e8d29cffff CALL runtime.morestack_noctxt(SB)
main.go:7 0x105ea1e e96dffffff JMP main.main(SB)

所以 main 函数的栈帧是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
64(0x40)
main 的调用者的栈基
56(0x38) <--------- main 的栈基 BP 指向位置
incr 第一个返回值临时存放位置
48(0x30)
incr 第二个返回值临时存放位置
40(0x28)
main 函数中局部变量 a # cpu 一般都是小端模式,这里 a 的值会按字节倒序存放
32(0x20)
main 函数中局部变量 b
24(0x18)
incr 第二个返回值
16(0x10)
incr 第一个返回值
8(0x8)
incr 第一个参数
0(0x0) <---------- 没 call 之前,main 的栈顶 SP 指向位置
incr 执行完后的返回地址
-8 <---------- main 的栈顶 SP 指向位置
incr 的调用者main的栈基
-16 <---------- incr 的栈基

-24

-32

<---------- incr 的栈顶

注意:栈帧的大小 = 栈基 BP 指向位置 - 没 call 之前 main 的栈顶 SP 指向位置

接着看 incr 函数的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
main.go:3             0x105e960               48c744241000000000      MOVQ $0x0, 0x10(SP)  // 第一个返回值的初始值 0 放到 0x10(SP) 的位置。范围是 8~16  
main.go:3 0x105e969 48c744241800000000 MOVQ $0x0, 0x18(SP) // 第二个返回值的初始值 0 放到 0x18(SP) 的位置。占用范围 16-24
main.go:4 0x105e972 488b442408 MOVQ 0x8(SP), AX // 参数 a 取出来
main.go:4 0x105e977 4889442410 MOVQ AX, 0x10(SP) // a 又赋值给第一个返回值
main.go:4 0x105e97c 48c744241801000000 MOVQ $0x1, 0x18(SP) // 1 赋值给第二个返回值
main.go:4 0x105e985 c3 RET // 弹出 -8~0 中的返回地址,并跳转到指向的位置执行
:-1 0x105e986 cc INT $0x3
:-1 0x105e987 cc INT $0x3
:-1 0x105e988 cc INT $0x3
:-1 0x105e989 cc INT $0x3
:-1 0x105e98a cc INT $0x3
:-1 0x105e98b cc INT $0x3
:-1 0x105e98c cc INT $0x3
:-1 0x105e98d cc INT $0x3
:-1 0x105e98e cc INT $0x3
:-1 0x105e98f cc INT $0x3

可以看到,实际上 incr 函数并没有分配自己的栈帧,所以并不是所有函数都有栈帧的。编译器会做相应的判断。

调用者的栈基是由被调用者一开始存入栈(不在任何函数的栈帧中,而是被栈帧夹在中间),也是由被调用者在 RET 前恢复的

总结

一个方法 a 的执行流程:

  1. g 的栈检查,检查是否要扩容(有的函数可能没有)
  2. a 分配自己的栈帧
  3. 存放 a 的调用者的栈基
  4. 设置 a 函数的栈基
  5. 向栈中放入参数(后面的参数先入栈,方便使用者顺序出栈使用参数,也就是越在前面的参数地址越小),call 调用 b。b 的执行流程与 a 一样
  6. b 函数返回值放入 a 栈合适位置(也是后面的返回值先入栈,方便使用者顺序出栈使用返回值),继续 a 的执行
  7. a 执行完后,栈基设置为 a 的调用者的栈基(前面的保存)
  8. 销毁 a 的栈帧
  9. 调用 RET 返回到 a 被 CALL 的地方的下一行继续执行

下篇预告

Panic 和 Recover 的实现原理




微信关注我,及时接收最新技术文章