共计 3844 个字符,预计需要花费 10 分钟才能阅读完成。
假如:
- AMD64 Linux
- C/C++
首先,咱们不须要讲太多的概念。只须要回顾几个根本的寄存器:
%rsp
:保留栈顶指针%rbp
:保留栈底指针%rbp~%rsp
这一段向下舒展的区域,就是栈帧。%rip
:保留下条指令的地址%rdi
:保留函数的第一个参数%rsi
:保留函数的第二个参数%rax
:保留返回值
而后,间接看代码吧!
样例程序
假如有程序如下:
int sum(int x, int y)
{return a + b;}
int main(int argc, char const *argv[])
{
int a = 1, b = 2;
int c = sum(a, b);
return 0;
}
应用
gcc -g prog.c -o prog
进行编译。
其汇编代码如下:
int sum(int x, int y)
{
1125: 55 push %rbp
1126: 48 89 e5 mov %rsp,%rbp
1129: 89 7d fc mov %edi,-0x4(%rbp)
112c: 89 75 f8 mov %esi,-0x8(%rbp)
return a + b;
112f: 8b 55 fc mov -0x4(%rbp),%edx
1132: 8b 45 f8 mov -0x8(%rbp),%eax
1135: 01 d0 add %edx,%eax
}
1137: 5d pop %rbp
1138: c3 retq
0000000000001139 <main>:
int main(int argc, char const *argv[])
{
1139: 55 push %rbp
113a: 48 89 e5 mov %rsp,%rbp
113d: 48 83 ec 20 sub $0x20,%rsp
1141: 89 7d ec mov %edi,-0x14(%rbp)
1144: 48 89 75 e0 mov %rsi,-0x20(%rbp)
int a = 1;
1148: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
int b = 2;
114f: c7 45 f8 02 00 00 00 movl $0x2,-0x8(%rbp)
int c = sum(a, b);
1156: 8b 55 f8 mov -0x8(%rbp),%edx
1159: 8b 45 fc mov -0x4(%rbp),%eax
115c: 89 d6 mov %edx,%esi
115e: 89 c7 mov %eax,%edi
1160: e8 c0 ff ff ff callq 1125 <sum>
1165: 89 45 f4 mov %eax,-0xc(%rbp)
return 0;
1168: b8 00 00 00 00 mov $0x0,%eax
}
执行流程
咱们间接从 main
读起。请务必认真 关注调用栈的变动。
0000000000001139 <main>:
int main(int argc, char const *argv[])
{
1139: 55 push %rbp #
113a: 48 89 e5 mov %rsp,%rbp #
113d: 48 83 ec 20 sub $0x20,%rsp # 这代码是将局部变量 argc 和 argv 保留到内存
1141: 89 7d ec mov %edi,-0x14(%rbp) # 咱们只需注意,rbp 往下开拓了 0x20,即 32 个
# 字节来寄存一些局部变量
1144: 48 89 75 e0 mov %rsi,-0x20(%rbp) #
int a = 1; #
1148: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp) #
int b = 2; #
114f: c7 45 f8 02 00 00 00 movl $0x2,-0x8(%rbp) #
下面这些代码,实际上是将 main 函数的上下文保留到内存。
为什么要保留呢?因为当初参数 argc,argv 都保留在寄存器里,局部变量还是一个立刻数。如果不保留下来,等咱们调用完 sum 再回来,寄存器等现场数据的值就变了。
#
int c = sum(a, b); # 咱们从这里开始看
1156: 8b 55 f8 mov -0x8(%rbp),%edx # \
1159: 8b 45 fc mov -0x4(%rbp),%eax # \ 这部分代码将参数 a, b 寄存在 rdi, rsi
115c: 89 d6 mov %edx,%esi # / 为调用 sum 函数做筹备
115e: 89 c7 mov %eax,%edi # /
#
1160: e8 c0 ff ff ff callq 1125 <sum> # callq 相当于:# pushq %rip
# jmpq <sum>
# 而 pushq %rip 相当于
# sub $0x8, %rsp
# movq %rip, (%rsp)
#
# callq 后的栈帧是:# +-------+
# |main_val| <--- rbp
# | ... |
# | ... |
# | ... |
# |main_val|
# | 1165 | <--- rsp
# | |
# 其中 1165 是下条指令的地址
#
下面这些代码,就是为调用 sum 函数做筹备。首先是筹备参数,而后是保留下条指令(%rip
)的数据。这样咱们调用完之后,就能够从内存中读出 %rip
的值,从而持续执行程序。
因为 callq
指令的作用,咱们跳转到了 1125
的中央:
0000000000001125 <sum>:
int sum(int x, int y)
{
# 在正式执行函数前,栈的内容是:# +-------+
# |main_val| <--- rbp (main_rbp)
# | ... |
# | ... |
# | ... |
# |main_val|
# | 1165 | <--- rsp
# | |
#
1125: 55 push %rbp # 这一步时,rbp 还和在 main 中时一样,因为没有
# 批改过。当 push 其入栈之后,记它值为 main_rsp.
# %rbp 入栈,栈内容变成:# +--------+
# |main_val|
# | ... |
# | ... |
# | ... |
# |main_val|
# | 1165 |
# |main_rbp| <--- rsp
#
1126: 48 89 e5 mov %rsp,%rbp # 在这里,rbp 的值变成 rsp,rbp 曾经属于新函数
# 栈内容变成:# +--------+
# |main_val|
# | ... |
# | ... |
# | ... |
# |main_val|
# | 1165 |
# |main_rbp| <--- rsp, rbp
这两个指令都是惯例操作:
- 保留上一个函数的栈底到内存。
- 创立本人的栈底。
# 这里,rbp 往下调配了 8 个字节,# 来保留局部变量(x, y)1129: 89 7d fc mov %edi,-0x4(%rbp) # 栈内容变成:112c: 89 75 f8 mov %esi,-0x8(%rbp) # +--------+
# |main_val|
# | ... |
# | ... |
# | ... |
# |main_val|
# | 1165 |
# |main_rbp| <--- rsp, rbp
# | x |
# | y |
这里同样是惯例操作,保留两个参数的值到栈上。
return a + b; #
112f: 8b 55 fc mov -0x4(%rbp),%edx # \
1132: 8b 45 f8 mov -0x8(%rbp),%eax # > 在这里,用 edx, eax 别离暂存参数 a, b。1135: 01 d0 add %edx,%eax # / 调用机器指令 add 将其相加,后果保留到 eax
}
下面这几句,筹备了 add
机器指令的参数,而后调用 add
指令实现运算。
# eax 是 Linux 规定的返回值寄存器
#
1137: 5d pop %rbp # 在这里,将栈中的值弹出,放到 rbp 寄存器。因为
# 栈指针 rsp 指向的值就是 main 的 rbp,所以 rbp
# 从新还原为原来的值,rbp 也就从新指向回原来的地位
# +--------+
# |main_val| <--- rbp (main_rbp)
# | ... |
# | ... |
# | ... |
# |main_val|
# | 1165 | <--- rsp
# | |
# | | 这里 x, y 的值曾经废除了。# | |
#
1138: c3 retq # retq 指令相当于 popq %rip
# 所以,1165 将被弹出,赋予 rip
# 而 rip 指向的是 main 中 callq <sum> 的下一
# 条指令,因而 main 函数复原了其执行流程
#
上面咱们又返回到了 main
函数继续执行:
1165: 89 45 f4 mov %eax,-0xc(%rbp) # 咱们从 <sum> 函数返回到此处继续执行 return 0; 1168: b8 00 00 00 00 mov $0x0,%eax} 116d: c9 leaveq 116e: c3 retq 116f: 90 nop
总结
程序运行之后,所有的函数调用会反映在一个栈上,这个栈被称为 程序栈(Program stack),简称栈。栈保留在内存上,从高地址向低地址增长(也即栈顶是“朝下”的)。
栈帧(Stack frame)是栈的组成单位,形成如下:
能够认为,栈帧上保留着的全副是都是被调用的函数的信息。只不过 A 调 B,B 调 C,使得对于 C 而言,B 是 Caller,对于 B 而言,A 是 Caller。栈帧上信息包含:
- BP 指针(
%ebp
)。这个指针是被调函数一开始的时候保留的。 - 保留的寄存器和局部变量。这个也是被调函数负责保留的。
- 输出的参数。这个也是被调函数负责保留的。
- 返回地址。这个是主调函数保留的,通过
retq
指令读取后设置给rip
。
函数调用的流程如下:
1. 主调:保留上下文
将本人的理论参数、局部变量、调用者保留寄存器等保留到栈上。
2. 主调:执行 callq 指令,跳转执行
将返回地址 %rip
保留到栈上,而后跳转到被调函数执行
3. 被调:替换栈底
将主调的栈底 %rbp
保留到栈上,而后将主调的栈顶 %rsp
值作为本人的栈底值。
4. 被调:保留上下文
将本人的理论参数、局部变量、调用者保留寄存器等保留到栈上。
5. 被调:执行本人的指令
执行机器指令等
6. 被调:还原栈底
将栈顶保留的主调的栈底还原
7. 被调:还原返回地址
通过 retq
将 %rip
从栈上取出
8. 主调:继续执行
CPU 从 %rip
处继续执行主调的指令。