目录
[1.1、go tool compile](#1.1、go tool compile)
[1.2、dlv debug](#1.2、dlv debug)
概要
函数栈帧是指函数在被调用时,为该函数在栈上分配的一块内存区域(一般是连续的),主要用于保存函数的上下文信息,包括参数,返回值,局部变量,寄存器值(ebp/rbp)等信息
调试的服务器信息:Centos Linux 7 ,CPU AMD x86_64,Go version 1.24
在go源码runtime/stack.go文件中可以看到go给出的在x86 CPU下栈布局:
// (x86)
// +------------------+
// | args from caller |
// +------------------+ <- frame->argp
// | return address |
// +------------------+
// | caller's BP (*) | (*) if framepointer_enabled && varp > sp
// +------------------+ <- frame->varp
// | locals |
// +------------------+
// | args to callee |
// +------------------+ <- frame->sp
我们结合一下案例,结合汇编进行逐步分析:
Go
1 package main
2
3 func main() {
4 x,y:=int64(1),int64(2)
5 x=add(x,y)
6 }
7
8 func add(a, b int64) int64 {
9 c:=a+b
10 q:=int64(5)
11 p:=sub(c, q)
12 return p
13 }
14
15 func sub(x, y int64) int64{
16 return x -y
17 }
前置知识:
一、汇编分析
我们通过go tool compile和dlv两个工具结合来看。
1.1、go tool compile
bash
[root@test gofunc]# go tool compile -S -N -l main.go
main.main STEXT size=66 args=0x0 locals=0x28 funcid=0x0 align=0x0
0x0000 00000 (/home/gofunc/main.go:3) TEXT main.main(SB), ABIInternal, $40-0
0x0000 00000 (/home/gofunc/main.go:3) CMPQ SP, 16(R14)
0x0004 00004 (/home/gofunc/main.go:3) PCDATA $0, $-2 //GC相关
0x0004 00004 (/home/gofunc/main.go:3) JLS 58
0x0006 00006 (/home/gofunc/main.go:3) PCDATA $0, $-1 //GC相关
0x0006 00006 (/home/gofunc/main.go:3) PUSHQ BP
0x0007 00007 (/home/gofunc/main.go:3) MOVQ SP, BP
0x000a 00010 (/home/gofunc/main.go:3) SUBQ $32, SP
0x000e 00014 (/home/gofunc/main.go:3) FUNCDATA $0, gclocals·FzY36IO2mY0y4dZ1+Izd/w==(SB) //GC相关
0x000e 00014 (/home/gofunc/main.go:3) FUNCDATA $1, gclocals·FzY36IO2mY0y4dZ1+Izd/w==(SB) //GC相关
0x000e 00014 (/home/gofunc/main.go:4) MOVQ $1, main.x+24(SP)
0x0017 00023 (/home/gofunc/main.go:4) MOVQ $2, main.y+16(SP)
0x0020 00032 (/home/gofunc/main.go:5) MOVL $1, AX
0x0025 00037 (/home/gofunc/main.go:5) MOVL $2, BX
0x002a 00042 (/home/gofunc/main.go:5) PCDATA $1, $0
0x002a 00042 (/home/gofunc/main.go:5) CALL main.add(SB)
0x002f 00047 (/home/gofunc/main.go:5) MOVQ AX, main.x+24(SP)
0x0034 00052 (/home/gofunc/main.go:6) ADDQ $32, SP
0x0038 00056 (/home/gofunc/main.go:6) POPQ BP
0x0039 00057 (/home/gofunc/main.go:6) RET
0x003a 00058 (/home/gofunc/main.go:6) NOP
0x003a 00058 (/home/gofunc/main.go:3) PCDATA $1, $-1
0x003a 00058 (/home/gofunc/main.go:3) PCDATA $0, $-2
0x003a 00058 (/home/gofunc/main.go:3) CALL runtime.morestack_noctxt(SB)
0x003f 00063 (/home/gofunc/main.go:3) PCDATA $0, $-1
0x003f 00063 (/home/gofunc/main.go:3) NOP
0x0040 00064 (/home/gofunc/main.go:3) JMP 0
0x0000 49 3b 66 10 76 34 55 48 89 e5 48 83 ec 20 48 c7 I;f.v4UH..H.. H.
0x0010 44 24 18 01 00 00 00 48 c7 44 24 10 02 00 00 00 D$.....H.D$.....
0x0020 b8 01 00 00 00 bb 02 00 00 00 e8 00 00 00 00 48 ...............H
0x0030 89 44 24 18 48 83 c4 20 5d c3 e8 00 00 00 00 90 .D$.H.. ].......
0x0040 eb be ..
rel 43+4 t=R_CALL main.add+0
rel 59+4 t=R_CALL runtime.morestack_noctxt+0
main.add STEXT size=103 args=0x10 locals=0x38 funcid=0x0 align=0x0
0x0000 00000 (/home/gofunc/main.go:8) TEXT main.add(SB), ABIInternal, $56-16
0x0000 00000 (/home/gofunc/main.go:8) CMPQ SP, 16(R14)
0x0004 00004 (/home/gofunc/main.go:8) PCDATA $0, $-2
0x0004 00004 (/home/gofunc/main.go:8) JLS 76
0x0006 00006 (/home/gofunc/main.go:8) PCDATA $0, $-1
0x0006 00006 (/home/gofunc/main.go:8) PUSHQ BP
0x0007 00007 (/home/gofunc/main.go:8) MOVQ SP, BP
0x000a 00010 (/home/gofunc/main.go:8) SUBQ $48, SP
0x000e 00014 (/home/gofunc/main.go:8) FUNCDATA $0, gclocals·FzY36IO2mY0y4dZ1+Izd/w==(SB)
0x000e 00014 (/home/gofunc/main.go:8) FUNCDATA $1, gclocals·FzY36IO2mY0y4dZ1+Izd/w==(SB)
0x000e 00014 (/home/gofunc/main.go:8) FUNCDATA $5, main.add.arginfo1(SB)
0x000e 00014 (/home/gofunc/main.go:8) MOVQ AX, main.a+64(SP)
0x0013 00019 (/home/gofunc/main.go:8) MOVQ BX, main.b+72(SP)
0x0018 00024 (/home/gofunc/main.go:8) MOVQ $0, main.~r0+16(SP)
0x0021 00033 (/home/gofunc/main.go:9) ADDQ BX, AX
0x0024 00036 (/home/gofunc/main.go:9) MOVQ AX, main.c+40(SP)
0x0029 00041 (/home/gofunc/main.go:10) MOVQ $5, main.q+24(SP)
0x0032 00050 (/home/gofunc/main.go:11) MOVL $5, BX
0x0037 00055 (/home/gofunc/main.go:11) PCDATA $1, $0
0x0037 00055 (/home/gofunc/main.go:11) CALL main.sub(SB)
0x003c 00060 (/home/gofunc/main.go:11) MOVQ AX, main.p+32(SP)
0x0041 00065 (/home/gofunc/main.go:12) MOVQ AX, main.~r0+16(SP)
0x0046 00070 (/home/gofunc/main.go:12) ADDQ $48, SP
0x004a 00074 (/home/gofunc/main.go:12) POPQ BP
0x004b 00075 (/home/gofunc/main.go:12) RET
0x004c 00076 (/home/gofunc/main.go:12) NOP
0x004c 00076 (/home/gofunc/main.go:8) PCDATA $1, $-1
0x004c 00076 (/home/gofunc/main.go:8) PCDATA $0, $-2
0x004c 00076 (/home/gofunc/main.go:8) MOVQ AX, 8(SP)
0x0051 00081 (/home/gofunc/main.go:8) MOVQ BX, 16(SP)
0x0056 00086 (/home/gofunc/main.go:8) CALL runtime.morestack_noctxt(SB)
0x005b 00091 (/home/gofunc/main.go:8) PCDATA $0, $-1
0x005b 00091 (/home/gofunc/main.go:8) MOVQ 8(SP), AX
0x0060 00096 (/home/gofunc/main.go:8) MOVQ 16(SP), BX
0x0065 00101 (/home/gofunc/main.go:8) JMP 0
0x0000 49 3b 66 10 76 54 55 48 89 e5 48 83 ec 28 48 89 I;f.vTUH..H..(H.
0x0010 44 24 38 48 89 5c 24 40 48 c7 44 24 10 00 00 00 D$8H.\[email protected]$....
0x0020 00 48 8d 0c 18 48 89 4c 24 20 48 c7 44 24 18 05 .H...H.L$ H.D$..
0x0030 00 00 00 b8 4b 00 00 00 bb 05 00 00 00 0f 1f 00 ....K...........
0x0040 e8 00 00 00 00 48 89 44 24 18 48 8b 44 24 20 48 .....H.D$.H.D$ H
0x0050 89 44 24 10 48 83 c4 28 5d c3 48 89 44 24 08 48 .D$.H..(].H.D$.H
0x0060 89 5c 24 10 e8 00 00 00 00 48 8b 44 24 08 48 8b .\$......H.D$.H.
0x0070 5c 24 10 eb 8b \$...
rel 65+4 t=R_CALL main.sub+0
rel 101+4 t=R_CALL runtime.morestack_noctxt+0
main.sub STEXT nosplit size=39 args=0x10 locals=0x10 funcid=0x0 align=0x0
0x0000 00000 (/home/gofunc/main.go:15) TEXT main.sub(SB), NOSPLIT|ABIInternal, $16-16
0x0000 00000 (/home/gofunc/main.go:15) PUSHQ BP
0x0001 00001 (/home/gofunc/main.go:15) MOVQ SP, BP
0x0004 00004 (/home/gofunc/main.go:15) SUBQ $8, SP
0x0008 00008 (/home/gofunc/main.go:15) FUNCDATA $0, gclocals·FzY36IO2mY0y4dZ1+Izd/w==(SB)
0x0008 00008 (/home/gofunc/main.go:15) FUNCDATA $1, gclocals·FzY36IO2mY0y4dZ1+Izd/w==(SB)
0x0008 00008 (/home/gofunc/main.go:15) FUNCDATA $5, main.sub.arginfo1(SB)
0x0008 00008 (/home/gofunc/main.go:15) MOVQ AX, main.x+24(SP)
0x000d 00013 (/home/gofunc/main.go:15) MOVQ BX, main.y+32(SP)
0x0012 00018 (/home/gofunc/main.go:15) MOVQ $0, main.~r0(SP)
0x001a 00026 (/home/gofunc/main.go:16) SUBQ BX, AX
0x001d 00029 (/home/gofunc/main.go:16) MOVQ AX, main.~r0(SP)
0x0021 00033 (/home/gofunc/main.go:16) ADDQ $8, SP
0x0025 00037 (/home/gofunc/main.go:16) POPQ BP
0x0026 00038 (/home/gofunc/main.go:16) RET
0x0000 55 48 89 e5 48 83 ec 08 48 89 44 24 18 48 89 5c UH..H...H.D$.H.\
0x0010 24 20 48 c7 04 24 00 00 00 00 48 29 d8 48 89 04 $ H..$....H).H..
0x0020 24 48 83 c4 08 5d c3 $H...].
go:cuinfo.producer.<unlinkable> SDWARFCUINFO dupok size=0
0x0000 2d 4e 20 2d 6c 20 72 65 67 61 62 69 -N -l regabi
go:cuinfo.packagename.main SDWARFCUINFO dupok size=0
0x0000 6d 61 69 6e main
main..inittask SNOPTRDATA size=8
0x0000 00 00 00 00 00 00 00 00 ........
gclocals·FzY36IO2mY0y4dZ1+Izd/w== SRODATA dupok size=8
0x0000 01 00 00 00 00 00 00 00 ........
main.add.arginfo1 SRODATA static dupok size=5
0x0000 00 08 08 08 ff .....
main.sub.arginfo1 SRODATA static dupok size=5
0x0000 00 08 08 08 ff .....
我们对add函数汇编结果的前两行最分析(其他的内容用dlv分析,结果更直观):
1) 函数头信息 (STEXT
)
main.add STEXT size=103 args=0x10 locals=0x38 funcid=0x0 align=0x0
- **
STEXT
**
表示该段代码属于程序的代码段(Text Segment),用于存放可执行指令。- **
size=117
**
函数内容编译后的机器码总大小为 103 字节。- **
args=0x10
**
参数和返回值总大小为 16 字节(对应两个int64
类型,8+8)。- **
locals=0x38
**
局部变量和临时存储空间占用 56 字节(包含寄存器值、栈扩展保留空间等)。- **
funcid=0x0
**
函数标识符,0 表示普通函数(非闭包或方法)。- **
align=0x0
**
函数入口地址对齐方式,0 表示使用默认对齐(通常为 16 字节)。
2)函数定义 (TEXT
)
0x0000 00000 (/home/gofunc/main.go:8) TEXT main.add(SB), ABIInternal, $56-16
- **
main.add(SB)
**
定义函数main.add
,SB
是虚拟的静态基址寄存器,表示符号的全局地址。- **
ABIInternal
**
使用 Go 1.17+ 的内部调用约定(Internal ABI),通过寄存器传递参数,提升性能。- **
$56-16
**
- 56:函数栈帧总大小(单位字节)。
16
:参数和返回值的总大小(由调用者在栈上分配)。
PS:这里可能会疑惑为什么传参和返回值怎么才16字节,不应该34字节吗?这是因为go编译器编译时让返回值复用一个参数的内存了。
1.2、dlv debug
go汇编,汇编指令解析放在每一行最后。
bash
[root@test gofunc]# dlv debug main.go
Type 'help' for list of commands.
(dlv) b main.go:3
Breakpoint 1 set at 0x470aea for main.main() ./main.go:3
(dlv) b main.go:8
Breakpoint 2 set at 0x470b4a for main.add() ./main.go:8
(dlv) b main.go:15
Breakpoint 3 set at 0x470bc4 for main.sub() ./main.go:15
(dlv) c
> [Breakpoint 1] main.main() ./main.go:3 (hits goroutine(1):1 total:1) (PC: 0x470aea)
1: package main
2:
=> 3: func main() {
4: x,y:=int64(1),int64(2)
5: x=add(x,y)
6: }
7:
8: func add(a, b int64) int64 {
(dlv) disass
TEXT main.main(SB) /home/gofunc/main.go
main.go:3 0x470ae0 493b6610 cmp rsp, qword ptr [r14+0x10] //比较栈顶(rsp)和协程栈预警值(g.stackguard0)大小,小于则要栈扩容
main.go:3 0x470ae4 7634 jbe 0x470b1a//小于成立,则跳到0x470b9a指令处,进行栈扩容
main.go:3 0x470ae6 55 push rbp //入栈
main.go:3 0x470ae7 4889e5 mov rbp, rsp
=> main.go:3 0x470aea* 4883ec20 sub rsp, 0x20//SP寄存器值减去32,表示栈顶向下减小32字节,即为main函数申请32字节的栈内存保存其上下文
main.go:4 0x470aee 48c744241801000000 mov qword ptr [rsp+0x18], 0x1 //设置x变量的值,占用8字节,即其占用rsp(栈顶)向上[0x18,0x20]之间的内存,第24到32字节之间
main.go:4 0x470af7 48c744241002000000 mov qword ptr [rsp+0x10], 0x2 //设置y变量的值,占用8字节,即其占用rsp(栈顶)向上[0x10,0x18]之间的内存,第16到24字节之间
main.go:5 0x470b00 b801000000 mov eax, 0x1 //设置AX寄存器值为1,即调用add的参数x
main.go:5 0x470b05 bb02000000 mov ebx, 0x2 //设置BX寄存器值为2,即调用add的参数y
main.go:5 0x470b0a e831000000 call $main.add //调用add函数
main.go:5 0x470b0f 4889442418 mov qword ptr [rsp+0x18], rax //将add返回值从AX寄存器中取出来,赋值给x变量
main.go:6 0x470b14 4883c420 add rsp, 0x20//SP寄存器值加上32,表示栈顶向上增加32字节,即将main函数申请32字节的栈内存归还
main.go:6 0x470b18 5d pop rbp //出栈
main.go:6 0x470b19 c3 ret
main.go:3 0x470b1a e8c1adffff call $runtime.morestack_noctxt
main.go:3 0x470b1f 90 nop
main.go:3 0x470b20 ebbe jmp $main.main
(dlv) c
> [Breakpoint 2] main.add() ./main.go:8 (hits goroutine(1):1 total:1) (PC: 0x470b4a)
3: func main() {
4: x,y:=int64(1),int64(2)
5: x=add(x,y)
6: }
7:
=> 8: func add(a, b int64) int64 {
9: c:=a+b
10: q:=int64(5)
11: p:=sub(c, q)
12: return p
13: }
(dlv) disass
TEXT main.add(SB) /home/gofunc/main.go
main.go:8 0x470b40 493b6610 cmp rsp, qword ptr [r14+0x10]
main.go:8 0x470b44 7654 jbe 0x470b9a
main.go:8 0x470b46 55 push rbp //将BP寄存器的值入栈,此时BP寄存器存的是main函数栈帧起始位置的地址,此时SP值等于SP值减去8字节,用于存储BP寄存器的值
main.go:8 0x470b47 4889e5 mov rbp, rsp//将SP寄存器值赋给BP寄存器,作为add函数栈帧的栈底
=> main.go:8 0x470b4a* 4883ec28 sub rsp, 0x30//SP寄存器值减去48,表示栈顶向下减少48字节,即为add函数申请48字节的栈内存保存其上下文
main.go:8 0x470b4e 4889442438 mov qword ptr [rsp+0x40], rax //从AX寄存器取参数x的值,放到rsp+0x38地址处,使用的是main函数的栈
main.go:8 0x470b53 48895c2440 mov qword ptr [rsp+0x48], rbx//从BX寄存器取参数y的值,放到rsp+0x40地址处,使用的是main函数的栈
main.go:8 0x470b38 48c744241000000000 mov qword ptr [rsp+0x10], 0x0 //令rsp+0x10地址处的值为0,即rsp+[0x10,0x18]之间的内存处存储的值清空
main.go:9 0x470b41 4801d8 add rax, rbx//令AX寄存器值等于AX寄存器值+BX寄存器值,即c=a+b的操作
main.go:9 0x470b44 4889442428 mov qword ptr [rsp+0x28], rax //将AX寄存器值放到rsp+0x28地址处
main.go:10 0x470b6a 48c744241805000000 mov qword ptr [rsp+0x18], 0x5 //令rsp+0x18地址处值为5,即q:=int64(5)
main.go:11 0x470b78 bb05000000 mov ebx, 0x5//令AX寄存器值等与0x5(5),为调用sub函数第二个参数
main.go:11 0x470b7d 0f1f00 nop dword ptr [rax], eax
main.go:11 0x470b80 e83b000000 call $main.sub //调用sub本函数
main.go:11 0x470b5c 4889442420 mov qword ptr [rsp+0x20], rax //将sub函数返回结果放到rsp+0x20地址处
main.go:12 0x470b8f 4889442410 mov qword ptr [rsp+0x10], rax //令rsp+0x10地址处的值等与AX寄存器的值(莫名其妙的操作,大神知道这个操作做啥吗,评论区见)
main.go:12 0x470b94 4883c428 add rsp, 0x30//SP寄存器值加上48,表示栈顶向上增加48字节,即将add函数申请48字节的栈内存归还
main.go:12 0x470b98 5d pop rbp //出栈,即将栈顶值(此时其值是main函数栈帧起始位置的地址)设置给BP寄存器
main.go:12 0x470b99 c3 ret//返回到main函数调用add函数处继续执行
main.go:8 0x470b9a 4889442408 mov qword ptr [rsp+0x8], rax //栈扩容前保存下AX寄存器的值,即参数a,因为栈扩容会覆盖AX寄存器的值
main.go:8 0x470b9f 48895c2410 mov qword ptr [rsp+0x10], rbx//栈扩容前保存下BX寄存器的值,即参数b,因为栈扩容会覆盖BX寄存器的值
main.go:8 0x470ba4 e837adffff call $runtime.morestack_noctxt //栈扩容
main.go:8 0x470ba9 488b442408 mov rax, qword ptr [rsp+0x8] //扩容后重新设置参数a到AX寄存器
main.go:8 0x470bae 488b5c2410 mov rbx, qword ptr [rsp+0x10] //扩容后重新设置参数b到BX寄存器
main.go:8 0x470bb3 eb8b jmp $main.add //扩容后重新跳到add函数代码段(不是调用add函数),继续执行add函数
(dlv) c
> [Breakpoint 3] main.sub() ./main.go:15 (hits goroutine(1):1 total:1) (PC: 0x470bc4)
10: q:=int64(5)
11: q=sub(75, q)
12: return c
13: }
14:
=> 15: func sub(x, y int64) int64{
16: return x -y
17: }
(dlv) disass
TEXT main.sub(SB) /home/gofunc/main.go
main.go:15 0x470bc0 55 push rbp
main.go:15 0x470bc1 4889e5 mov rbp, rsp
=> main.go:15 0x470bc4* 4883ec08 sub rsp, 0x8//申请8字节栈内存
main.go:15 0x470bc8 4889442418 mov qword ptr [rsp+0x18], rax
main.go:15 0x470bcd 48895c2420 mov qword ptr [rsp+0x20], rbx
main.go:15 0x470bd2 48c7042400000000 mov qword ptr [rsp], 0x0//令rsp地址处的值为0
main.go:16 0x470bda 4829d8 sub rax, rbx //计算x-y的值并将结果存到AX寄存器中,这样add函数从AX寄存器取sub函数返回值
main.go:16 0x470bdd 48890424 mov qword ptr [rsp], rax//令rsp地址处的值为AX寄存器的值,也就是x-y的值
main.go:16 0x470be1 4883c408 add rsp, 0x8 //归还8字节栈内存
main.go:16 0x470be5 5d pop rbp//出栈,BP寄存器值恢复到add函数栈帧起始位置地址
main.go:16 0x470be6 c3 ret
通过观察三个函数的汇编,可以看到【栈的申请与归还操作】和【函数栈帧的恢复】:
栈申请:
push rbp //入栈操作等价于【sub rsp 0x8; mov rsp, rbp】,即申请8字节栈内存用于保存caller的函数栈帧起始位置地址
mov rbp, rsp //保存callee的函数栈帧起始位置地址
sub rsp, 0x28 //栈申请
栈归还:
add rsp, 0x28 //栈归还
pop rbp //出栈操作等价于【mov rbp, rsp; add rsp 0x8】,恢复caller的函数栈帧起始位置地址
二、栈布局与函数栈帧
通过第一章对示例的汇编进行分析,我们可以画出其栈布局,假设main函数被调用时的起始地址是ox3e8(1000),那么我们可以得到下图:
可以看到整体还是符合go源码给出的栈布局的:
// (x86)
// +------------------+
// | args from caller |
// +------------------+ <- frame->argp
// | return address |
// +------------------+
// | caller's BP (*) | (*) if framepointer_enabled && varp > sp (暂存调用者函数栈帧起始位置地址)
// +------------------+ <- frame->varp (函数栈帧起始位置地址 BP寄存器)
// | locals |
// +------------------+
// | args to callee |
// +------------------+ <- frame->sp (栈顶,SP寄存器)
一个函数栈帧由BP寄存器和SP寄存器确定。
C中函数栈帧是逐步扩张的(每定义一个变量就扩张一次), 但是go里面函数栈帧的扩张是一次性分配一大块(直接将栈指针移动到所需最大栈空间的位置,即栈顶),然后通过栈顶指针加偏移量这种相对寻址方式使用函数栈帧。
函数栈帧大小在编译期就可以确定!!! 对于栈消耗较大的函数,编译器会在函数头部插入检测代码,如果发现需要进行"栈扩容",就会调用runtime.morestack相关函数重新分配一块足够大的栈空间, 将原来的数据拷过来并释放原来的空间。