一句话总结:栈 我们通常指保存局部变量,具有先进后出 (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 寄存器,将程序控制权交给调用者
总结
通过分析汇编代码,可以看到 栈 完整的工作特点
- 栈由内存高地址(虚拟地址空间最顶端)向低地址移动,
- 一个函数调用由 rbp、rsp 两个寄存器来完成 FILO 特性的栈操作,其中 rbp 称做基准帧指针(栈底),rsp 称做栈指针(栈顶)
- 扩栈操作即减小 rsp
- 局部对像一般使用 rbp + offset 栈内寻址
- 被调用者负责保存调用者的 rbp
- pushq、popq 操作,会使 rsp 相应的减小或是增大
- 函数传参优先使用寄存器
疑问
在 64 位平台编绎,发现 rsp -= 16、rsp -= 32,开避很多空间,但是实际有部分空洞未使用。这是为什么?字节对齐?
其它内容如有错误,请大家指正
网友评论