什么是栈帧

2018-07-08  本文已影响198人  董泽润

一句话总结: 我们通常指保存局部变量,具有先进后出 (FILO) 特性的一段高内存地址空间,维护这个 结构而使用两个寄存器:栈指针 rsp 帧指针 rbp. 当程序调用层数比较深时,栈内存呈现出一块一块连续空间,每一块空间属于某一个调用对像,这个块可以称之为.


c 示例

这算是大学基础课了,以前没好好学,工作多年不用就忘;) 那么,什么是 栈?什么是 帧,先看一段代码:
实验环境:ubuntu 3.19.0 内核 x86_64

#include<stdio.h>

void swap(int *a, int *b) {
    int c=0;
    c=*a;
    *a=*b;
    *b=c;
}

int main(){
    int a=16;
    int b=4;
    swap(&a,&b);
    return (b-a);
}

这段代码很好理解,main 创建两个 int 变量 a、b, 然后调用 swap 函数,最后返回交换后的 b - a 值。

生成汇编代码:

gcc -O0 -S test.c

这里 -O0 是指关掉编绎器优化,否则 swap 函数极有可能被做成 inline 内联函数。

1   .file   "test.c"
2   .text
3   .globl  swap
4   .type   swap, @function
5swap:
6.LFB0:
7   .cfi_startproc
8   pushq   %rbp
9   .cfi_def_cfa_offset 16
10  .cfi_offset 6, -16
11  movq    %rsp, %rbp
12  .cfi_def_cfa_register 6
13  movq    %rdi, -24(%rbp)
14  movq    %rsi, -32(%rbp)
15  movl    $0, -4(%rbp)
16  movq    -24(%rbp), %rax
17  movl    (%rax), %eax
18  movl    %eax, -4(%rbp)
19  movq    -32(%rbp), %rax
20  movl    (%rax), %edx
21  movq    -24(%rbp), %rax
22  movl    %edx, (%rax)
23  movq    -32(%rbp), %rax
24  movl    -4(%rbp), %edx
25  movl    %edx, (%rax)
26  popq    %rbp
27  .cfi_def_cfa 7, 8
28  ret
29  .cfi_endproc
30.LFE0:
31  .size   swap, .-swap
32  .globl  main
33  .type   main, @function
34main:
35.LFB1:
36  .cfi_startproc
37  pushq   %rbp
38  .cfi_def_cfa_offset 16
39  .cfi_offset 6, -16
40  movq    %rsp, %rbp
41  .cfi_def_cfa_register 6
42  subq    $16, %rsp
43  movl    $16, -8(%rbp)
44  movl    $4, -4(%rbp)
45  leaq    -4(%rbp), %rdx
46  leaq    -8(%rbp), %rax
47  movq    %rdx, %rsi
48  movq    %rax, %rdi
49  call    swap
50  movl    -4(%rbp), %edx
51  movl    -8(%rbp), %eax
52  subl    %eax, %edx
53  movl    %edx, %eax
54  leave
55  .cfi_def_cfa 7, 8
56  ret
57  .cfi_endproc
58.LFE1:
59  .size   main, .-main
60  .ident  "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
61  .section    .note.GNU-stack,"",@progbits

看起来有点得杂,实际上无需关心 . 开头的符合信息,只要关注 main、swap 的汇编即可。

先看 main

37  pushq   %rbp 保存调用者的 rbp
40  movq    %rsp, %rbp main 函数 rbp 设为当前 rsp 指针
42  subq    $16, %rsp  开避空间
43  movl    $16, -8(%rbp)  a=16
44  movl    $4, -4(%rbp)   b=4
45  leaq    -4(%rbp), %rdx  取 b 地址
46  leaq    -8(%rbp), %rax  取 a 地址
47  movq    %rdx, %rsi 传参 &b
48  movq    %rax, %rdi 传参 &a
49  call    swap

37 行将当前 rbp 帧指针压栈保存起来,对于很多寄存器,有的由调用者负责压栈保存,有的由被调用者完成,rbp 就是由被调用者负责保存。

38行 将 rsp 赋值给 rbp,为了容易理解,假设此时 rbp = 0

42 行 由于栈是从高地址向低地址增长,所以 subq $16, %rsp 即开辟了 16 字节栈空间。

43、44 行就是在 rbp - 8, rbp - 4 地址处创建 32 位 int 变量 a = 16, b = 4. 那么刚才栈开避出来的空间,使用了 8 字节,还剩 8 字节

45、46 行将在栈上的变量 a、b 取地址后,将值分别放到 rax、rdx 寄存器中

47、48 行分别将 rax、rdx 值存放到 rdi、rsi 寄存器

49 行 call 相当于pushq $rip,先将 ip 寄存器入栈,然后将控制权交给 swap 函数

那么此时内存空间,及各寄存器值如下图所示:


调用swap前.png

再看 swap

8   pushq   %rbp 保存调用者 main 的 rbp
11  movq    %rsp, %rbp 生成当前 swap 的 rbp,rbp = rsp
13  movq    %rdi, -24(%rbp)
14  movq    %rsi, -32(%rbp)
15  movl    $0, -4(%rbp)
16  movq    -24(%rbp), %rax
17  movl    (%rax), %eax
18  movl    %eax, -4(%rbp)
19  movq    -32(%rbp), %rax
20  movl    (%rax), %edx
21  movq    -24(%rbp), %rax
22  movl    %edx, (%rax)
23  movq    -32(%rbp), %rax
24  movl    -4(%rbp), %edx
25  movl    %edx, (%rax)
26  popq    %rbp 弹出栈顶值,赋给 rbp,此时 rbp = 0
28  ret

8 行 由被调用者 swap 保存 main 的 rbp, 压栈。此时被保存的 rbp = 0

11 行 将 rbp 帧指针设置为当前 rsp 值,即此时 rbp = -16

13、14 行 将由 main 传递进来的参数 a、b 地址分别放到栈内存 rbp - 24、rbp - 32 处

15 行 在栈上创建临时变量 int c = 0

16 - 18 行 完成 c = *a 操作

19 - 22 行 完成 *a = *b 操作

23 - 25 行 完成 *b = c 操作

26 行 将栈顶元素弹出,赋值给 rbp 寄存器,即此时恢复 main 的现场 rbp = 0

28 行 ret 相当于 popq %rip,将控制权转交给 main

那么此时内存空间,及各寄存器值如下:


调用swap后.png

swap 返回后

50  movl    -4(%rbp), %edx  取值 edx=b
51  movl    -8(%rbp), %eax  取值 eax=a
52  subl    %eax, %edx edx=edx-eax 即 b=b-a
53  movl    %edx, %eax  eax=b
54  leave
56  ret

50 - 53 行 完成变量 (b-a) 操作,并将结果写到 eax 寄存器

54 行 leave 相当于 movq %rbp, %rsp ; popq %rbp 将栈的两个寄存器回到 main 调用初始状态

56 行 ret 相当于popq %rip,恢复 rip 寄存器,将程序控制权交给调用者

总结

通过分析汇编代码,可以看到 栈 完整的工作特点

  1. 栈由内存高地址(虚拟地址空间最顶端)向低地址移动,
  2. 一个函数调用由 rbp、rsp 两个寄存器来完成 FILO 特性的栈操作,其中 rbp 称做基准帧指针(栈底),rsp 称做栈指针(栈顶)
  3. 扩栈操作即减小 rsp
  4. 局部对像一般使用 rbp + offset 栈内寻址
  5. 被调用者负责保存调用者的 rbp
  6. pushq、popq 操作,会使 rsp 相应的减小或是增大
  7. 函数传参优先使用寄存器

疑问

在 64 位平台编绎,发现 rsp -= 16、rsp -= 32,开避很多空间,但是实际有部分空洞未使用。这是为什么?字节对齐?

其它内容如有错误,请大家指正

上一篇下一篇

猜你喜欢

热点阅读