函数调用栈浅析

以前面试的时候,碰到过一个问题。函数的调用过程是怎样的?

听到问题的时候有点懵,这算是问题吗。马上胡乱诌了一通。说完以后面试官看我的表情 ﹁_﹁。

多年以后看到了一些文章,发现应该从汇编角度解释这个问题,更容易理解。值得记下来。

函数调用过程需要用函数调用栈来解释。函数调用栈是程序运行时一段连续的内存区域,栈是后进先出的数据结构。

内存的生长方向是从低地址向高地址,而栈是相反的,从高地址向低地址。压栈时栈顶地址变小,退栈时栈顶地址变大。

总的来说函数调用过程如下图:

函数调用时,先将主调函数(Caller)的状态信息压入栈中,再将被调函数(Callee)的状态压入栈中。

Callee函数执行完毕,Callee信息退栈,Caller信息退栈,这样程序回到调用之前的位置,继续执行后面的语句。

如果用汇编来理解。则会有恍然大悟的感觉。上学的时候的汇编没学好,只记得一点,汇编是跟寄存器打交道的底层编程语言。

函数的调用过程涉及到3个寄存器。

EBP(Base Point):基地址寄存器,记录当前函数状态的基地址。

ESP(Stack Point):栈顶寄存器,记录函数调用栈的栈顶地址,压栈和出栈时变化。压栈esp-4,退栈esp+4

EIP:记录即将执行的指令的地址。

简单写一段代码,完成一个简单的加法功能。

#include <iostream>

int fun(int a, int b)
{
    return a + b;
}

int main()
{
    int c = fun(1, 2);
    return 0;
}

在vs里调试时按Alt + 8看到汇编代码:

int fun(int a, int b)
{
01261700  push        ebp                      ;ebp压栈,记录Caller函数状态的基地址(step 3)
01261701  mov         ebp,esp                  ;esp的内容放入ebp,此时ebp,esp都指向栈顶(step 4)
01261703  sub         esp,0C0h                 ;函数内部操作开始
01261709  push        ebx                      
0126170A  push        esi  
0126170B  push        edi  
0126170C  lea         edi,[ebp-0C0h]  
01261712  mov         ecx,30h  
01261717  mov         eax,0CCCCCCCCh  
0126171C  rep stos    dword ptr es:[edi]  
    return a + b;
0126171E  mov         eax,dword ptr [a]  
01261721  add         eax,dword ptr [b]  
}
01261724  pop         edi  
01261725  pop         esi  
01261726  pop         ebx                      ;函数内部操作结束
01261727  mov         esp,ebp                  ;栈顶回到函数执行基地址(step 5)
01261729  pop         ebp                      ;ebp退栈,ebp回到Caller函数状态的基地址(step 5)
0126172A  ret                                  ;返回地址退栈,存到eip中,跳转返回地址(step 6)


    int c = fun(1, 2);
0126175E  push        2                      ;参数2压栈(step 1)
01261760  push        1                      ;参数1压栈(step 1)
01261762  call        fun (01261154h)          ;调用函数fun,返回地址压栈,跳转函数地址(step 2)
01261767  add         esp,8  
0126176A  mov         dword ptr [c],eax

 只将函数调用栈相关的汇编代码部分作了注解。可以看到函数调用分为6个步骤:

1.将Caller的参数逆序压栈。

2.将Caller的返回地址压栈

3.将Caller的状态基地址ebp压栈

4.ebp指向esp,将esp的值放入ebp,使得ebp,esp指向栈顶

5.函数内部操作结束之后,esp回到ebp,将ebp退栈,值放到ebp里,这样ebp回到Caller的基地址位置

6.再将返回地址退栈,放到epi里,这样回到Caller状态全部恢复,继续执行以后的语句。

函数调用栈大约是这么个过程。图是为了加深理解,我自己画的。大家可以看下面引用里画的图。

引用:

https://zhuanlan.zhihu.com/p/25816426

http://blog.csdn.net/zsJum/article/details/6117043

原文地址:https://www.cnblogs.com/yao2yaoblog/p/6567284.html