栈帧
什么是栈帧?
每一次函数的调用,都会在调用栈(call stack)上维护一个独立的栈帧(stack frame).每个独立的栈帧一般包括:
-
函数的返回地址和参数
-
临时变量: 包括函数的非静态局部变量以及编译器自动生成的其他临时变量
-
函数调用的上下文
栈是从高地址向低地址延伸,一个函数的栈帧用 bp 和 sp 这两个寄存器来划定范围。bp 指向当前的栈帧的底部,,sp 始终指向栈帧的顶部
不同语言的调用过程的栈帧是不同的,但是可以过栈数据的变化分析。
接下来分析 C++ 和 Go 的栈帧变化。
C++ 栈帧
平台 linux,结构 x86_64,gcc 未开编译优化。 源码定义 sum 函数,输入 8 个 long 参数,返回 1 个 long。
1: long sum(long a, long b, long c, long d, long e, long f, long g, long h) {
2: long i = a + b + c + d + e + f + g;
3: return h;
4: }
5:
6: int main() {
7: int a = 10;
8: long c = sum(1, 2, 3, 4, 5, 6, 7, 8);
9: return c;
10:}
编译后 gdb 调试:
> g++ -g x86_64.cc -o x86_64
> gdb x86_64
打断点,单步调试记录主要寄存器变化:
| 行数 | rdi | rsi | rdx | rcx | r8 | r9 | rbp | rsp | rax | rip |
|---|---|---|---|---|---|---|---|---|---|---|
| 7 | 0x1 | 0x7fffffffdf08 | 0x7fffffffdf18 | 0x7ffff7fbc718 | 0x0 | 0x7ffff7fe21b0 | 0x7fffffffde10 | 0x7fffffffde00 | 0x555555555179 | 0x555555555181 |
| 8 | 0x1 | 0x7fffffffdf08 | 0x7fffffffdf18 | 0x7ffff7fbc718 | 0x0 | 0x7ffff7fe21b0 | 0x7fffffffde10 | 0x7fffffffde00 | 0x555555555179 | 0x555555555188 |
| 2 | 0x1 | 0x2 | 0x3 | 0x4 | 0x5 | 0x6 | 0x7fffffffdde0 | 0x7fffffffdde0 | 0x555555555179 | 0x555555555141 |
| 3 | 0x1 | 0x2 | 0x15 | 0x4 | 0x5 | 0x6 | 0x7fffffffdde0 | 0x7fffffffdde0 | 0x1c | 0x555555555173 |
| 4 | 0x1 | 0x2 | 0x15 | 0x4 | 0x5 | 0x6 | 0x7fffffffdde0 | 0x7fffffffdde0 | 0x8 | 0x555555555177 |
| 9 | 0x1 | 0x2 | 0x15 | 0x4 | 0x5 | 0x6 | 0x7fffffffde10 | 0x7fffffffde00 | 0x8 | 0x5555555551b9 |
| 10 | 0x1 | 0x2 | 0x15 | 0x4 | 0x5 | 0x6 | 0x7fffffffde10 | 0x7fffffffde00 | 0x8 | 0x5555555551bd |
全局代码段数据
(gdb) x/60i 0x555555555120
0x555555555120 <frame_dummy>: jmp 0x5555555550a0 <register_tm_clones>
0x555555555125 <_Z3sumllllllll>: push %rbp
0x555555555126 <_Z3sumllllllll+1>: mov %rsp,%rbp
0x555555555129 <_Z3sumllllllll+4>: mov %rdi,-0x18(%rbp)
0x55555555512d <_Z3sumllllllll+8>: mov %rsi,-0x20(%rbp)
0x555555555131 <_Z3sumllllllll+12>: mov %rdx,-0x28(%rbp)
0x555555555135 <_Z3sumllllllll+16>: mov %rcx,-0x30(%rbp)
0x555555555139 <_Z3sumllllllll+20>: mov %r8,-0x38(%rbp)
0x55555555513d <_Z3sumllllllll+24>: mov %r9,-0x40(%rbp)
0x555555555141 <_Z3sumllllllll+28>: mov -0x18(%rbp),%rdx
0x555555555145 <_Z3sumllllllll+32>: mov -0x20(%rbp),%rax
0x555555555149 <_Z3sumllllllll+36>: add %rax,%rdx
0x55555555514c <_Z3sumllllllll+39>: mov -0x28(%rbp),%rax
0x555555555150 <_Z3sumllllllll+43>: add %rax,%rdx
0x555555555153 <_Z3sumllllllll+46>: mov -0x30(%rbp),%rax
0x555555555157 <_Z3sumllllllll+50>: add %rax,%rdx
0x55555555515a <_Z3sumllllllll+53>: mov -0x38(%rbp),%rax
0x55555555515e <_Z3sumllllllll+57>: add %rax,%rdx
0x555555555161 <_Z3sumllllllll+60>: mov -0x40(%rbp),%rax
0x555555555165 <_Z3sumllllllll+64>: add %rax,%rdx
0x555555555168 <_Z3sumllllllll+67>: mov 0x10(%rbp),%rax
0x55555555516c <_Z3sumllllllll+71>: add %rdx,%rax
0x55555555516f <_Z3sumllllllll+74>: mov %rax,-0x8(%rbp)
0x555555555173 <_Z3sumllllllll+78>: mov 0x18(%rbp),%rax
0x555555555177 <_Z3sumllllllll+82>: pop %rbp
0x555555555178 <_Z3sumllllllll+83>: ret
0x555555555179 <main()>: push %rbp
0x55555555517a <main()+1>: mov %rsp,%rbp
0x55555555517d <main()+4>: sub $0x10,%rsp
0x555555555181 <main()+8>: movl $0xa,-0x4(%rbp)
0x555555555188 <main()+15>: push $0x8
0x55555555518a <main()+17>: push $0x7
0x55555555518c <main()+19>: mov $0x6,%r9d
0x555555555192 <main()+25>: mov $0x5,%r8d
0x555555555198 <main()+31>: mov $0x4,%ecx
0x55555555519d <main()+36>: mov $0x3,%edx
0x5555555551a2 <main()+41>: mov $0x2,%esi
0x5555555551a7 <main()+46>: mov $0x1,%edi
0x5555555551ac <main()+51>: call 0x555555555125 <_Z3sumllllllll>
0x5555555551b1 <main()+56>: add $0x10,%rsp
0x5555555551b5 <main()+60>: mov %rax,-0x10(%rbp)
0x5555555551b9 <main()+64>: mov -0x10(%rbp),%rax
0x5555555551bd <main()+68>: leave
0x5555555551be <main()+69>: ret
堆栈变化图:
从 C 语言的栈帧中可知,当调用另一个函数时,会将函数返回的下一条指令地址压栈,然后在新函数中会储存原函数的堆栈参数,另起新栈使用。新函数处理数据后,会根据不同版本的调用约定执行堆栈的清理。
go 栈帧
平台 linux,go 1.17,关闭内联优化。
源代码如下,定义 11 个入参的 test 函数,增加 1 后返回 11 个参数。
1: package main
2:
3: func test(a, b, c, d, e, f, g, h, i, j, k int) (
4: int, int, int, int, int, int, int, int, int, int, int,
5: ) {
6: return a + 1, b + 1, c + 1, d + 1, e + 1, f + 1, g + 1, h + 1, i + 1, j + 1, k + 1
7: }
8:
9: func main() {
10: a := 1
11: _, _, _, _, _, _, _, _, _, _, _ = test(a, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
12:}
编译后 gdb 调试:
> go build -gcflags=all="-N -l" call_convention.go
> gdb call_convention
打断点,单步调试记录执行到指定源代码行数时,主要寄存器变化:
| 行数 | rax | rbx | rcx | rdi | rsi | r8 | r9 | r10 | r11 | rbp | rsp | rip |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 10 | 0x459b80 | 0x0 | 0x0 | 0x1 | 0x1 | 0xc000180000 | 0x0 | 0x7fffffffde10 | 0x0 | 0xc00003c770 | 0xc00003c700 | 0x459b94 |
| 11 | 0x459b80 | 0x0 | 0x0 | 0x1 | 0x1 | 0xc000180000 | 0x0 | 0x7fffffffffffffff | 0x0 | 0xc00003c770 | 0xc00003c700 | 0x459b9d |
| 3 | 0x1 | 0x2 | 0x3 | 0x4 | 0x5 | 0x6 | 0x7 | 0x8 | 0x9 | 0xc00003c770 | 0xc00003c6f8 | 0x4599e0 |
| 6 | 0x1 | 0x2 | 0x3 | 0x4 | 0x5 | 0x6 | 0x7 | 0x8 | 0x9 | 0xc00003c6f0 | 0xc00003c6a8 | 0x459a95 |
| 12 | 0x2 | 0x3 | 0x4 | 0x5 | 0x6 | 0x7 | 0x8 | 0x9 | 0xa | 0xc00003c770 | 0xc00003c700 | 0x459be5 |
全局代码段数据
0x4599e0 <main.test>: sub $0x50,%rsp
0x4599e4 <main.test+4>: mov %rbp,0x48(%rsp)
0x4599e9 <main.test+9>: lea 0x48(%rsp),%rbp
0x4599ee <main.test+14>: mov %rax,0x78(%rsp)
0x4599f3 <main.test+19>: mov %rbx,0x80(%rsp)
0x4599fb <main.test+27>: mov %rcx,0x88(%rsp)
0x459a03 <main.test+35>: mov %rdi,0x90(%rsp)
0x459a0b <main.test+43>: mov %rsi,0x98(%rsp)
0x459a13 <main.test+51>: mov %r8,0xa0(%rsp)
0x459a1b <main.test+59>: mov %r9,0xa8(%rsp)
0x459a23 <main.test+67>: mov %r10,0xb0(%rsp)
0x459a2b <main.test+75>: mov %r11,0xb8(%rsp)
0x459a33 <main.test+83>: movq $0x0,0x40(%rsp)
0x459a3c <main.test+92>: movq $0x0,0x38(%rsp)
0x459a45 <main.test+101>: movq $0x0,0x30(%rsp)
0x459a4e <main.test+110>: movq $0x0,0x28(%rsp)
0x459a57 <main.test+119>: movq $0x0,0x20(%rsp)
0x459a60 <main.test+128>: movq $0x0,0x18(%rsp)
0x459a69 <main.test+137>: movq $0x0,0x10(%rsp)
0x459a72 <main.test+146>: movq $0x0,0x8(%rsp)
0x459a7b <main.test+155>: movq $0x0,(%rsp)
0x459a83 <main.test+163>: movq $0x0,0x68(%rsp)
0x459a8c <main.test+172>: movq $0x0,0x70(%rsp)
0x459a95 <main.test+181>: mov 0x78(%rsp),%rdx
0x459a9a <main.test+186>: inc %rdx
0x459a9d <main.test+189>: mov %rdx,0x40(%rsp)
0x459aa2 <main.test+194>: mov 0x80(%rsp),%rdx
0x459aaa <main.test+202>: inc %rdx
0x459aad <main.test+205>: mov %rdx,0x38(%rsp)
0x459ab2 <main.test+210>: mov 0x88(%rsp),%rdx
0x459aba <main.test+218>: inc %rdx
0x459abd <main.test+221>: mov %rdx,0x30(%rsp)
0x459ac2 <main.test+226>: mov 0x90(%rsp),%rdx
0x459aca <main.test+234>: inc %rdx
0x459acd <main.test+237>: mov %rdx,0x28(%rsp)
0x459ad2 <main.test+242>: mov 0x98(%rsp),%rdx
0x459ada <main.test+250>: inc %rdx
0x459add <main.test+253>: mov %rdx,0x20(%rsp)
0x459ae2 <main.test+258>: mov 0xa0(%rsp),%rdx
0x459aea <main.test+266>: inc %rdx
0x459aed <main.test+269>: mov %rdx,0x18(%rsp)
0x459af2 <main.test+274>: mov 0xa8(%rsp),%rdx
0x459afa <main.test+282>: inc %rdx
0x459afd <main.test+285>: mov %rdx,0x10(%rsp)
0x459b02 <main.test+290>: mov 0xb0(%rsp),%rdx
0x459b0a <main.test+298>: inc %rdx
0x459b0d <main.test+301>: mov %rdx,0x8(%rsp)
0x459b12 <main.test+306>: mov 0xb8(%rsp),%rdx
0x459b1a <main.test+314>: inc %rdx
0x459b1d <main.test+317>: mov %rdx,(%rsp)
0x459b21 <main.test+321>: mov 0x58(%rsp),%rdx
0x459b26 <main.test+326>: inc %rdx
0x459b29 <main.test+329>: mov %rdx,0x68(%rsp)
0x459b2e <main.test+334>: mov 0x60(%rsp),%rdx
0x459b33 <main.test+339>: inc %rdx
0x459b36 <main.test+342>: mov %rdx,0x70(%rsp)
0x459b3b <main.test+347>: mov 0x40(%rsp),%rax
0x459b40 <main.test+352>: mov 0x38(%rsp),%rbx
0x459b45 <main.test+357>: mov 0x30(%rsp),%rcx
0x459b4a <main.test+362>: mov 0x28(%rsp),%rdi
0x459b4f <main.test+367>: mov 0x20(%rsp),%rsi
0x459b54 <main.test+372>: mov 0x18(%rsp),%r8
0x459b59 <main.test+377>: mov 0x10(%rsp),%r9
0x459b5e <main.test+382>: mov 0x8(%rsp),%r10
0x459b63 <main.test+387>: mov (%rsp),%r11
0x459b67 <main.test+391>: mov 0x48(%rsp),%rbp
0x459b6c <main.test+396>: add $0x50,%rsp
0x459b70 <main.test+400>: ret
0x459b80 <main.main>: cmp 0x10(%r14),%rsp
0x459b84 <main.main+4>: jbe 0x459bef <main.main+111>
0x459b86 <main.main+6>: sub $0x78,%rsp
0x459b8a <main.main+10>: mov %rbp,0x70(%rsp)
0x459b8f <main.main+15>: lea 0x70(%rsp),%rbp
=> 0x459b94 <main.main+20>: movq $0x1,0x68(%rsp)
0x459b9d <main.main+29>: movq $0xa,(%rsp)
0x459ba5 <main.main+37>: movq $0xb,0x8(%rsp)
0x459bae <main.main+46>: mov $0x1,%eax
0x459bb3 <main.main+51>: mov $0x2,%ebx
0x459bb8 <main.main+56>: mov $0x3,%ecx
0x459bbd <main.main+61>: mov $0x4,%edi
0x459bc2 <main.main+66>: mov $0x5,%esi
0x459bc7 <main.main+71>: mov $0x6,%r8d
0x459bcd <main.main+77>: mov $0x7,%r9d
0x459bd3 <main.main+83>: mov $0x8,%r10d
0x459bd9 <main.main+89>: mov $0x9,%r11d
0x459bdf <main.main+95>: nop
0x459be0 <main.main+96>: call 0x4599e0 <main.test>
0x459be5 <main.main+101>: mov 0x70(%rsp),%rbp
0x459bea <main.main+106>: add $0x78,%rsp
0x459bee <main.main+110>: ret
0x459bef <main.main+111>: call 0x456820 <runtime.morestack_noctxt>
0x459bf4 <main.main+116>: jmp 0x459b80 <main.main>
堆栈变化示意图:
由上可知,go 和 C++ 相似,调用新函数时也会将函数返回的下一条指令地址压栈(没有 pop、push 指令,使用 mov 指令)。