转自http://blog.csdn.net/dongtingzhizi/article/details/6680050

C++函数调用过程深入分析

作者:靠谱哥

微博:洞庭之子-Bing

0. 引言

  函数调用的过程实际上也就是一个中断的过程,那么C++中到底是怎样实现一个函数的调用的呢?参数入栈、函数跳转、保护现场、回复现场等又是怎样实现的呢?本文将对函数调用的过程进行深入的分析和详细解释,并在VC 6.0环境下进行演示。分析不到位或者存在错误的地方请批评指正,请与作者联系。

  首先对三个常用的寄存器做一下说明,EIP是指令指针,即指向下一条即将执行的指令的地址;EBP为基址指针,常用来指向栈底;ESP为栈指针,常用来指向栈顶。

  看下面这个简单的程序并在VC 6.0中查看并分析汇编代码。

图1

1. 函数调用

  g_func函数调用的汇编代码如图2:

图2

   首先是三条push指令,分别将三个参数压入栈中,可以发现参数的压栈顺序是从右向左的。这时我们可以查看栈中的数据验证一下。如图3所示,从右边的实时寄存器表中我们可以看到ESP(栈顶指针)值为0x0012FEF0,然后从中间的内存表中找到内存地址0x0012FEF0处,我们可以看到内存中依次存储了0x00000001(即参数1),0x00000002(即参数2),0x00000003(即参数3),即此时栈顶存储的是三个参数值,说明压栈成功。

图3

  然后可以看到call指令跳转到地址0x00401005,那么该地址处是什么呢?我们继续跟踪一下,在图4中我们看到这里又是一条跳转指令,跳转到0x00401030。我们再看一下地址0x00401030处,在图5中可以看到这才是真正的g_func函数,0x00401030是该函数的起始地址,这样就实现了到g_func函数的跳转。

图4

图5

2. 保存现场          

  此时我们再来查看一下栈中的数据,如图6所示,此时的ESP(栈顶)值为0x0012FEEC,在内存表中我们可以看到栈顶存放的是0x00401093,下面还是前面压栈的参数1,2,3,也就是执行了call指令后,系统默认的往栈中压入了一个数据(0x00401093),那么它究竟是什么呢?我们再看到图3,call指令后面一条指令的地址就是0x00401093,实际上就是函数调用结束后需要继续执行的指令地址,函数返回后会跳转到该地址。这也就是我们常说的函数中断前的“保护现场”。这一过程是编译器隐含完成的,实际上是将EIP(指令指针)压栈,即隐含执行了一条push eip指令,在中断函数返回时再从栈中弹出该值到EIP,程序继续往下执行。

图6

  继续往下看,进入g_func函数后的第一条指令是push ebp,即将ebp入栈。因为每一个函数都有自己的栈区域,所以栈基址也是不一样的。现在进入了一个中断函数,函数执行过程中也需要ebp寄存器,而在进入函数之前的main函数的ebp值怎么办呢?为了不被覆盖,将它压入栈中保存。

下一条mov ebp, esp 将此时的栈顶地址作为该函数的栈基址,确定g_func函数的栈区域(ebp为栈底,esp为栈顶)。

  再往下的指令是sub esp, 48h,指令的字面意思是将栈顶指针往上移动48h Byte。那为什么要移动呢?这中间的内存区域用来做什么呢?这个区域为间隔空间,将两个函数的栈区域隔开一段距离,如图7所示。而该间隔区域的大小固定为40h,即64Byte,然后还要预留出存储局部变量的内存区域。g_func函数有两个局部变量x和y,所以esp需移动的长度为40h+8=48h。

图7

  接下来的几行指令(如下)是将刚才留出的48h的内存区域赋值为0CCCCCCCCh。

00401039   lea        edi,[ebp-48h]

0040103C   mov      ecx,12h

00401041   mov            eax,0CCCCCCCCh

00401046   rep stos    dword ptr [edi] 。

  接下来三条压栈指令,分别将EBX,ESI,EDI压入栈中,这也是属于“保护现场”的一部分,这些是属于main函数执行的一些数据。EBX,ESI,EDI分别为基址寄存器,源变址寄存器,目的变址寄存器。

3. 执行子函数

  继续往下看,接下来是局部变量的x和y的赋值,看汇编指令中是怎样去计算x和y的内存地址的呢?如图8所示,是基于ebp去计算的,分别是[ebp-4]和[ebp-8]。我们查看内存表可以看到相应的内存区域已经存入了0x11111111和0x22222222。

图8

  此时我们对整个内存区域中存储的内容应该非常清晰了(如图9所示)。

图9

4. 恢复现场

  这时子函数部分的代码已经执行完,继续往下看,编译器将会做一些事后处理的工作(如图10所示)。首先是三条出栈指令,分别从栈顶读取EDI,ESI和EBX的值。从图9的内存数据分布我们可以得知此时栈顶的数据确实是EDI,ESI和EBX,这样就恢复了调用前的EDI,ESI和EBX值,这是“恢复现场”的一部分。

图10

  第四条指令是mov esp, ebp 即将ebp的值赋给esp。那这是什么意思呢?看看图9的内存数据分布,我们就能很明白了,这条语句是让ESP指向EBP所指的内存单元,也就是让ESP跳过了一段区域,很明显跳过的恰好是间隔区域和局部数据区域,因为函数已经退出了,这两个区域都已经没有用处了。实际上这条语句是进入函数时创建间隔区域的语句 sub esp, 48h的相反操作。

  再往下是pop ebp,我们从图9的内存数据分布可以看出此时栈顶确实是存储的前EBP值,这样就恢复了调用前的EBP值,这也是“恢复现场”的一部分。该指令执行完后,内存数据分布如图11所示。

图11

  再往下是一条ret指令,即返回指令,他会怎么处理呢?注意在执行ret指令前的ESP值和EIP值(如图12所示),ESP指向栈顶的0x00401093,EIP的值是0x0040105C(即ret指令的地址)。

图12

  执行ret指令后我们来查看ESP和EIP值(如图13所示),此时ESP为0012FEF0,即往下移动了4Byte。显然此处编译器隐含的执行了一条pop指令。再来看一下EIP的值,变为了0x00401093,这个值怎么这么熟悉呢!它实际上就是栈顶的4Byte数据,所以这里隐含执行的指令应该是pop eip。而这个值就是前面讲到过的,在调用call指令前压栈的call的下一条指令的地址。从图13中可以看出,正是因为EIP的值变成了0x00401093,所以程序跳转到了call指令后面的一条指令,又回到了中断前的地方,这就是所谓的恢复断点。

图13

  还没有完全结束,此时还有最后一条指令add esp, 0Ch。这个就很简单了,从图13中可以看出现在栈顶的数据是1,2,3,也就是函数调用前压入的三个实参。这是函数已经执行完了,显然这三个参数没有用处了。所以add esp, 0Ch就是让栈顶指针往下移动12Byte的位置。为什么是12Byte呢,很简单,因为入栈的是3个int数据。这样由于函数调用在栈中添加的所有数据都已清除,栈顶指针(ESP)真正回到了函数调用前的位置,所有寄存器的值也恢复到了函数调用之前。

结束!

C++函数调用过程深入分析<转>的更多相关文章

  1. C++函数调用过程深入分析

    http://blog.csdn.net/dongtingzhizi/article/details/6680050 0. 引言 函数调用的过程实际上也就是一个中断的过程,那么C++中到底是怎样实现一 ...

  2. C++函数调用过程解析

    编译环境:Windows 10 + VS2015. 0.引言 函数调用的过程实际上也就是一个中断的过程,本文演示和深入分析参数入栈.函数跳转.保护现场.恢复现场等函数调用过程. 首先对三个常用的寄存器 ...

  3. 从一个新手容易混淆的例子简单分析C语言中函数调用过程

    某天,王尼玛写了段C程序: #include <stdio.h> void input() { int i; ]; ; i < ; i++) { array[i] = i; } } ...

  4. c函数调用过程原理及函数栈帧分析

    转载自地址:http://blog.csdn.net/zsy2020314/article/details/9429707       今天突然想分析一下函数在相互调用过程中栈帧的变化,还是想尽量以比 ...

  5. android的编译和运行过程深入分析

    android的编译和运行过程深入分析 作者: 字体:[增加 减小] 类型:转载 首先来看一下使用Java语言编写的Android应用程序从源码到安装包的整个过程,此过程对了解android的编译和运 ...

  6. 函数调用过程&生成器解释

    摘自马哥解答,感谢. 函数调用过程: 假设程序是单进程,单执行流,在某一时刻,能运行的程序流只能有一个.但函数调用会打开新的执行上下文,因此,为了确保main函数可以恢复现场,在main函数调用其它函 ...

  7. Linux驱动调试-根据oops的栈信息,确定函数调用过程

    上章链接入口: http://www.cnblogs.com/lifexy/p/8006748.html 在上章里,我们分析了oops的PC值在哪个函数出错的,那如何通过栈信息来查看出错函数的整个调用 ...

  8. C语言的函数调用过程

    从汇编的角度解析函数调用过程 看看下面这个简单函数的调用过程: int Add(int x,int y) { ; sum = x + y; return sum; } int main () { ; ...

  9. 37.Linux驱动调试-根据oops的栈信息,确定函数调用过程

    上章链接入口: http://www.cnblogs.com/lifexy/p/8006748.html 在上章里,我们分析了oops的PC值在哪个函数出错的 本章便通过栈信息来分析函数调用过程 1. ...

随机推荐

  1. CodeForces 349B Color the Fence (DP)

    题意:给出1~9数字对应的费用以及一定的费用,让你输出所选的数字所能组合出的最大的数值. 析:DP,和01背包差不多的,dp[i] 表示费用最大为 i 时,最多多少位,然后再用两个数组,一个记录路径, ...

  2. 洛谷 - P4449 - 于神之怒加强版 - 莫比乌斯反演

    https://www.luogu.org/problemnew/show/P4449 \(F(n)=\sum\limits_{i=1}^{n}\sum\limits_{i=1}^{m} gcd(i, ...

  3. Unity 5.4 公开测试版发布:增强的视觉效果,更佳的性能表现

    为用户提供可靠稳定的产品是我们的一贯使命,现在我们将发布Unity 5.4 beta版本,提供所有的用户公开测试,这包含了Unity Personal Edition版本用户.我们非常希望大家下载并尝 ...

  4. UIWebView与JavaScript的交互

    UIWebView是iOS最常用的SDK之一,它有一个stringByEvaluatingJavaScriptFromString方法可以将javascript嵌入页面中,通过这个方法我们可以在iOS ...

  5. CF1175E Minimal Segment Cover 题解

    题意:给出\(n\)个形如\([l,r]\)的线段.\(m\)次询问,每次询问区间\([x,y]\),问至少选出几条线段,使得区间\([x,y]\)的任何一个部位都被至少一条线段覆盖. 首先有一个显然 ...

  6. ZooKeeper-3.3.4集群安装配置(转载)

    ZooKeeper是一个分布式开源框架,提供了协调分布式应用的基本服务,它向外部应用暴露一组通用服务——分布式同步(Distributed Synchronization).命名服务(Naming S ...

  7. echarts相关属性设置(2)--折线图和柱状图的结合使用

    type:bar和line的组合 option = { { tooltip: { trigger: 'axis', axisPointer: { // type: 'shadow' }, // lab ...

  8. numpy使用示例

    numpy介绍 创建numpy的数 一维数组是什么样子 可以理解为格子纸的一行就是一个一维数据 two_arr = np.array([1, 2, 3]) 二维数组什么样子 理解为一张格子纸, 多个一 ...

  9. ZOJ How Many Nines 模拟 | 打表

    How Many Nines Time Limit: 1 Second      Memory Limit: 65536 KB If we represent a date in the format ...

  10. ubuntu触摸板关闭开启

    sudo rmmod psmouse          #用来禁用触摸板 sudo modprobe psmouse     #用来启用触摸板