函数调用栈
# 函数调用栈
在程序运行过程中,栈用于维护函数调用的上下文,离开了栈,函数调用就无法实现。
# 栈帧
每个函数发生调用时,都会有一块栈空间,这块栈空间称为栈帧
。
栈帧的结构如下:
这里要注意的是:栈空间是从高地址到低地址分配的。
rbp
:基址指针寄存器(reextended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
rsp
:栈指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
栈帧保存了一个函数调用所需要的维护信息:
- 函数的返回地址和参数。
- 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量。
- 保存的上下文:包括在函数调用前后需要保持不变的寄存器。
# 函数调用与函数调用栈
写一个程序,通过反汇编,了解真实的函数调用栈。
// demo.cpp
#include <cstdint>
int32_t Func2(int32_t a);
int32_t Func1(int32_t a, int32_t b)
{
int32_t c = 10;
int32_t d = 0;
c = Func2(c + d);
return c + d + a + b;
}
int32_t Func2(int32_t a)
{
int b = a * 2;
return b;
}
int main(int argc, char** argv)
{
int32_t a = 42;
int32_t b = 101;
int32_t c = 0;
c = Func1(a, b);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
g++ -g demo.cpp -o demo
:编译得到可执行文件。objdump -s -d demo
:反汇编得到汇编代码。
函数调用过程如下:
总结一下,当函数 A 调用函数 B 时:
- 将函数入参保存要寄存器中。(在函数 A 的调用栈中)
- 将函数 B 的返回地址压入栈,即将
callq <B>
的下一行指令地址压入栈,然后调用函数 B。(在函数 A 的调用栈中) - 函数参数入栈。(在函数 B 的调用栈中)
- 局部变量入栈。(在函数 B 的调用栈中)
- 执行一些运算指令。(在函数 B 的调用栈中)
- 将返回结果保存到寄存器
eax
中。(在函数 B 的调用栈中) - 从寄存器
eax
中获得函数 B 的返回值。(在函数 A 的调用栈中)
其实函数调用过程中涉及到栈基指针rbp
和栈顶指针rsp
的变化。也就是这段汇编:
push %rbp
mov %rsp,%rbp
sub $0x20,%rsp
1
2
3
2
3
它的作用就是把上一个栈帧的rbp
存起来,将当前栈的rbp
更新成上一个栈帧的rsp
,然后设置当前栈的rsp
。这样就完成了两个指针维护一个栈帧大小的功能。函数调用就是这两个指针的移动,函数调用栈的扩张和收缩。
# 实际函数调用栈的阅读
首先明确一点,栈空间是从高地址到低地址扩张的,阅读也是从高地址开始读。
# 查看调用栈
首先需要有调试信息的二进制文件。
利用gdb
查看。
gdb demo
(gdb) b Func1(int, int) # 打个断点
(gdb) r
(gdb) s
(gdb) disassemble # 查看当前函数的汇编,能看到具体执行到哪一条指令
(gdb) ni # 执行一条指令
(gdb) p $rbp
(gdb) p $rsp
(gdb) x/32xg $rbp - 128 # 查看从$rbp - 128的地址开始32的单位(以16进制显示,每个单元8字节)的内存内容
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
上次更新: 2022/06/17, 07:22:19