相关: 《Postgresql中的pg_memory_barrier_impl和C的volatile》 《X86函数调用模型分析》
函数A调用函数B,B执行完毕后继续执行函数A,如何实现这样的调用?
直接思考可能会存在以下几步:
- A的局部变量如果在寄存器,需要保存起来。
- 这些变量保存在栈中,栈中的位置需要记录。
- 多层调用的话记录堆栈位置的信息会有多组,也都需要记录。
- A调用完B后还需要继续执行,继续执行的位置需要保存起来。
下面分析x86的具体实现。
(资料汇编)
速查:
- 对于栈帧来说:栈帧顶部用bp指针(高地址),栈帧底部(低地址)用sp指针。
- 对于堆栈来说:整体堆栈的顶部为sp指针(堆栈生长到的最低地址)。
一、内存结构
二进制程序执行时的内存结构:
- code section:保存程序执行指令的机器码。
- static section:在程序执行期间不改变的常量和静态变量。
- heap:使用malloc申请的堆内存,向内存地址升序的方向生长:grows up。
- stack:保存函数局部变量和函数调用的控制信息,向内存地址降序的方向生长:grows down。
- (32位系统)程序的虚拟内存空间提供了
的空间保存数据,用户地址空间3G从0x0000000
到0xC0000000
,内核空间1G从0xC0000000
到0xFFFFFFFF
。
- (64位系统)程序的虚拟内存空间提供了
的空间保存数据,用户地址空间128T从0x0000 0000 0000 0000
到0x0000 7FFF FFFF F0000
,内核空间128T从0xFFFF 8000 0000 0000
到0xFFFF FFFF FFFF FFFF
。
二、寄存器
- 寄存器提供了额外的存储空间,每个寄存器可以存一个字(4字节)。
和函数调用相关的寄存器(e表示扩展的意思):
- eip:指令指针,存储当前正在执行的机器指令的地址。也叫PC(程序计数器)。
- ebp:帧指针,保存当前栈帧顶部地址(高地址)。
- esp:堆栈指针,保存当前堆栈底部地址(低地址)。
下图便于理解:
代码语言:javascript复制|----------------------| high address
| ... |
|-------frame----------|
| ... |
| ... |
| ... |
|-------frame----------| # current frame <----- ebp
| ... |
| ... |
| ... | <----- esp
|----------------------| low address
三、x86函数调用
- 当需要调用另一个函数时,栈空间需要生长,用来保存一些局部变量 或者 寄存器信息。
- 当调用函数发生时,caller执行逻辑会跳转到callee,拿到结果后,在跳转会caller。这就需要改变下面几个寄存器的值:
- eip指令指针,需要改成指向callee的指令。
- ebp 和 esp 当前分别指向caller栈帧的顶部和底部。两个寄存器都需要更新为 指向callee的新栈帧的顶部和底部。当函数返回时,需要恢复寄存器中的旧值,才可以返回caller。所以更新寄存器的值,需要将它的旧值保存在堆栈中,以便在函数返回后恢复旧值。
下面是main调用foo的执行过程:
step0
step1:参数入栈
将参数压入堆栈。 x86将参数压入堆栈来传递参数。请注意,当我们将参数压入堆栈时,esp 会递减。参数以相反的顺序压入堆栈。(上面是高地址)
step2:旧的eip入栈
旧的eip(rip)压入堆栈。跳转到子函数执行eip需要指向子函数,所以这里先保存下。
step3:修改eip指向
已经保存了 eip 的旧值,可以安全地将 eip 更改为指向被callee的指令。
step4:将旧的ebp入栈
step5:ebp向下移动指向新栈帧顶部
这就是mov %esp �p
的含义:
step6:esp向下移动
通过sub esp
(esp地址–) 来为新栈帧分配新空间。编译器会根据函数的复杂度确定 esp 应该减少多少。
- 例如,只有几个局部变量的函数不需要太多的堆栈空间,因此 esp 只会减少几个字节。
- 例如,如果一个函数将一个大数组声明为一个局部变量,那么 esp 会减少很多来适应堆栈中的数组。
step7:执行callee
现在堆栈中已经保存了函数的局部变量和跳转控制信息;由于ebp指向栈帧的顶部,所以可以用ebp 8找到第一个参数的保存位置。
step8:返回esp回到堆栈顶部
step9:恢复旧的ebp
使用esp从堆栈中pop出一个值(old ebp),把old ebp的值赋给ebp。
step10:弹出eip
继续使用esp弹出old eip的值赋给eip。
step11:从堆栈中删除参数
继续讲堆栈上的参数弹出到寄存器,然后删除esp栈顶以下的元素。栈顶以下的元素已经不在栈中,没有意义。
四、实例分析
代码语言:javascript复制int main(void) {
foo(1, 2);
}
void foo(int a, int b) {
int bar[4];
}
gcc -O0 t.c -o t -g
main执行过程
代码语言:javascript复制(gdb) disassemble /rm
Dump of assembler code for function main:
3 int main(void) {
# 由_start调入main函数
0x0000000000401122 < 0>: 55 push %rbp # 栈帧顶部入栈
0x0000000000401123 < 1>: 48 89 e5 mov %rsp,%rbp # 栈帧顶部指针rbp指向新栈帧顶部
4 foo(1, 2);
=> 0x0000000000401126 < 4>: be 02 00 00 00 mov $0x2,%esi # 参数1入寄存器传递
0x000000000040112b < 9>: bf 01 00 00 00 mov $0x1,�i # 参数2入寄存器传递
0x0000000000401130 < 14>: e8 07 00 00 00 callq 0x40113c <foo> # push %rip 然后 jmpq
# push %rip 等价与 sub $0x8, %rsp
# mov $rip, %rsp
0x0000000000401135 < 19>: b8 00 00 00 00 mov $0x0,�x
5 }
0x000000000040113a < 24>: 5d pop %rbp # 先恢复rbp的值
0x000000000040113b < 25>: c3 retq # 在恢复rip的值 popq %rip
End of assembler dump.
foo函数
代码语言:javascript复制(gdb) disassemble /rm
Dump of assembler code for function foo:
7 void foo(int a, int b) {
0x000000000040113c < 0>: 55 push %rbp # 帧顶位置 入栈
0x000000000040113d < 1>: 48 89 e5 mov %rsp,%rbp # rbp帧顶指针,指向新帧顶
0x0000000000401140 < 4>: 89 7d ec mov �i,-0x14(%rbp) # 参数2入栈(先压最后一个参数入栈)
0x0000000000401143 < 7>: 89 75 e8 mov %esi,-0x18(%rbp) # 参数1入栈
8 int bar[4];
9 }
=> 0x0000000000401146 < 10>: 90 nop
0x0000000000401147 < 11>: 5d pop %rbp # 先恢复rbp的值
0x0000000000401148 < 12>: c3 retq # 在恢复rip的值 popq %rip
End of assembler dump.