相关:
《Postgresql中的pg_memory_barrier_impl和C的volatile》
《X86函数调用模型分析》
函数A调用函数B,B执行完毕后继续执行函数A,如何实现这样的调用?
直接思考可能会存在以下几步:
下面分析x86的具体实现。
(资料汇编)
速查:
二进制程序执行时的内存结构:
0x0000000
到0xC0000000
,内核空间1G从0xC0000000
到0xFFFFFFFF
。0x0000 0000 0000 0000
到0x0000 7FFF FFFF F0000
,内核空间128T从0xFFFF 8000 0000 0000
到0xFFFF FFFF FFFF FFFF
。和函数调用相关的寄存器(e表示扩展的意思):
下图便于理解:
|----------------------| high address
| ... |
|-------frame----------|
| ... |
| ... |
| ... |
|-------frame----------| # current frame <----- ebp
| ... |
| ... |
| ... | <----- esp
|----------------------| low address
当需要调用另一个函数时,栈空间需要生长,用来保存一些局部变量 或者 寄存器信息。
当调用函数发生时,caller执行逻辑会跳转到callee,拿到结果后,在跳转会caller。这就需要改变下面几个寄存器的值:
当函数返回时,需要恢复寄存器中的旧值,才可以返回caller。所以更新寄存器的值,需要将它的旧值保存在堆栈中,以便在函数返回后恢复旧值。
下面是main调用foo的执行过程:
将参数压入堆栈。 x86将参数压入堆栈来传递参数。请注意,当我们将参数压入堆栈时,esp 会递减。参数以相反的顺序压入堆栈。(上面是高地址)
旧的eip(rip)压入堆栈。跳转到子函数执行eip需要指向子函数,所以这里先保存下。
已经保存了 eip 的旧值,可以安全地将 eip 更改为指向被callee的指令。
这就是mov %esp %ebp
的含义:
通过sub esp
(esp地址–) 来为新栈帧分配新空间。编译器会根据函数的复杂度确定 esp 应该减少多少。
现在堆栈中已经保存了函数的局部变量和跳转控制信息;由于ebp指向栈帧的顶部,所以可以用ebp+8找到第一个参数的保存位置。
使用esp从堆栈中pop出一个值(old ebp),把old ebp的值赋给ebp。
继续使用esp弹出old eip的值赋给eip。
继续讲堆栈上的参数弹出到寄存器,然后删除esp栈顶以下的元素。栈顶以下的元素已经不在栈中,没有意义。
int main(void) {
foo(1, 2);
}
void foo(int a, int b) {
int bar[4];
}
gcc -O0 t.c -o t -g
(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,%edi # 参数2入寄存器传递
0x0000000000401130 <+14>: e8 07 00 00 00 callq 0x40113c # push %rip 然后 jmpq
# push %rip 等价与 sub $0x8, %rsp
# mov $rip, %rsp
0x0000000000401135 <+19>: b8 00 00 00 00 mov $0x0,%eax
5 }
0x000000000040113a <+24>: 5d pop %rbp # 先恢复rbp的值
0x000000000040113b <+25>: c3 retq # 在恢复rip的值 popq %rip
End of assembler dump.
(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 %edi,-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.