在开发程序中, 有很多的函数之间的调用,这些调用是如何在OS中实现的呢, 下面试我的理解。
1 首先明确几个概念:
1)系统栈:OS在调用每个进程的时候都会分配相应的堆栈给进程
2)函数栈:在调用每个函数的时候,都会从系统栈中分配相应的栈空间给函数。这个函数栈是如何来确定的呢? 通过ESP(栈顶指针)和EBP(栈底指针)来确定的。
3)寄存器:EIP。cpu通过这个寄存器的值来确定下一条所执行的指令。
2 调用过程:
比如有个例子:这个例子是在32位linux用gcc编译的结果.
int bar(int c, int d) { int e = c + d; return e; } int foo(int a, int b) { return bar(a, b); } int main(void) { foo(2, 3); return 0; }产生的汇编代码如下:
int bar(int c, int d)
{
8048394: 55 push %ebp
8048395: 89 e5 mov %esp,%ebp
8048397: 83 ec 10 sub $0x10,%esp
int e = c + d;
804839a: 8b 55 0c mov 0xc(%ebp),%edx
804839d: 8b 45 08 mov 0x8(%ebp),%eax
80483a0: 01 d0 add %edx,%eax
80483a2: 89 45 fc mov %eax,-0x4(%ebp)
return e;
80483a5: 8b 45 fc mov -0x4(%ebp),%eax
}
80483a8: c9 leave
80483a9: c3 ret
080483aa <foo>:
int foo(int a, int b)
{
80483aa: 55 push %ebp
80483ab: 89 e5 mov %esp,%ebp
80483ad: 83 ec 08 sub $0x8,%esp
return bar(a, b);
80483b0: 8b 45 0c mov 0xc(%ebp),%eax
80483b3: 89 44 24 04 mov %eax,0x4(%esp)
80483b7: 8b 45 08 mov 0x8(%ebp),%eax
80483ba: 89 04 24 mov %eax,(%esp)
80483bd: e8 d2 ff ff ff call 8048394 <bar>
}
80483c2: c9 leave
80483c3: c3 ret
080483c4 <main>:
int main(void)
{
80483c4: 8d 4c 24 04 lea 0x4(%esp),%ecx
80483c8: 83 e4 f0 and $0xfffffff0,%esp
80483cb: ff 71 fc pushl -0x4(%ecx)
80483ce: 55 push %ebp
80483cf: 89 e5 mov %esp,%ebp
80483d1: 51 push %ecx
80483d2: 83 ec 08 sub $0x8,%esp
foo(2, 3);
80483d5: c7 44 24 04 03 00 00 movl $0x3,0x4(%esp)
80483dc: 00
80483dd: c7 04 24 02 00 00 00 movl $0x2,(%esp)
80483e4: e8 c1 ff ff ff call 80483aa <foo>
return 0;
80483e9: b8 00 00 00 00 mov $0x0,%eax
}
这个函数的调用过程如下:
1) 调用foo(2,3)之前,把main函数中的临时变量(3,2) 压入堆栈,代码为
80483d5: c7 44 24 04 03 00 00 movl $0x3,0x4(%esp)
80483dc: 00
80483dd: c7 04 24 02 00 00 00 movl $0x2,(%esp)
2) 调用 函数foo, call 80483aa <foo>
call的作用为:1)EIP值指向call的下一条指令, 即return 0, 将这条指令压入堆栈;2)将EIP值指向foo函数的入口处。
所以, 再执行完这条指令后, %esp的值会减去4。总的来说这条指令得作用就是将main函数运行的地址压栈,并将运行地址切入到foo函数的开始处。
3)进入foo后的动作如下:
1、将main函数的stack frame 的基址%ebp压栈,并将ebp的地址设为esp;将esp-8留出存放临时变量的栈空间
2 访问main函数中的变量(通过ebp+12和ebp+8),并将这两个值压入foo的函数栈帧中
3 call bar函数。将foo的运行地址压栈
4) 进入函数bar,将foo的函数栈帧的基址ebp压栈
其他动作类似3
5)返回:
leave: 这个指令是将栈顶平衡, 回到调用之前的状态。
mov %ebp %esp
pop %ebp
这个指令如下操作: 将ebp付给esp, 恢复上个函数stack frame的 栈顶, 并将存在栈中的上个函数的基址出栈,存入%ebp中
ret;
这个指令一个出栈操作,类似 pop %eip. 将栈中的下一条执行地址放入eip中。CPU通过EIP来确定下一条执行的命令。并且esp+4。
以上基本就是整个函数调用的cpu的执行过程。
堆栈的示意图如下。
函数栈帧示意图
3 总结:
函数调用和返回过程中的这些规则:
-
参数压栈传递,并且是从右向左依次压栈。
-
ebp
总是指向当前栈帧的栈底。 -
返回值通过
eax
寄存器传递。 -
函数调用前堆栈操作:1) push ebp 2) mov ebp esp 3) sub esp xxx
-
函数调用恢复堆栈操作:1) mov esp ebp 2) pop ebp 3) ret
参考文章:
http://learn.akae.cn/media/ch19s01.html