栈帧浅析

500 阅读6分钟

栈帧

什么是栈帧?

每一次函数的调用,都会在调用栈(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

打断点,单步调试记录主要寄存器变化:

行数rdirsirdxrcxr8r9rbprspraxrip
70x10x7fffffffdf080x7fffffffdf180x7ffff7fbc7180x00x7ffff7fe21b00x7fffffffde100x7fffffffde000x5555555551790x555555555181
80x10x7fffffffdf080x7fffffffdf180x7ffff7fbc7180x00x7ffff7fe21b00x7fffffffde100x7fffffffde000x5555555551790x555555555188
20x10x20x30x40x50x60x7fffffffdde00x7fffffffdde00x5555555551790x555555555141
30x10x20x150x40x50x60x7fffffffdde00x7fffffffdde00x1c0x555555555173
40x10x20x150x40x50x60x7fffffffdde00x7fffffffdde00x80x555555555177
90x10x20x150x40x50x60x7fffffffde100x7fffffffde000x80x5555555551b9
100x10x20x150x40x50x60x7fffffffde100x7fffffffde000x80x5555555551bd

全局代码段数据

(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

堆栈变化图:

stack_rbp_rsp.png 从 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

打断点,单步调试记录执行到指定源代码行数时,主要寄存器变化:

行数raxrbxrcxrdirsir8r9r10r11rbprsprip
100x459b800x00x00x10x10xc0001800000x00x7fffffffde100x00xc00003c7700xc00003c7000x459b94
110x459b800x00x00x10x10xc0001800000x00x7fffffffffffffff0x00xc00003c7700xc00003c7000x459b9d
30x10x20x30x40x50x60x70x80x90xc00003c7700xc00003c6f80x4599e0
60x10x20x30x40x50x60x70x80x90xc00003c6f00xc00003c6a80x459a95
120x20x30x40x50x60x70x80x90xa0xc00003c7700xc00003c7000x459be5

全局代码段数据

   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>

堆栈变化示意图:

image.png

由上可知,go 和 C++ 相似,调用新函数时也会将函数返回的下一条指令地址压栈(没有 pop、push 指令,使用 mov 指令)。