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

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 C++函数调用过程深入分析 作者:靠谱哥 微博:洞庭之子-Bing 0. 引言 ...

  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. HDU 5313 Bipartite Graph(二分图染色+01背包水过)

    Problem Description Soda has a bipartite graph with n vertices and m undirected edges. Now he wants ...

  2. Eclipse中同时打开多个Console

    实现方法: 1.点击Open Console案例下拉三角,选择New Console View. 2.点击Pin Console按钮將两个控制台同时固定住. 3.点击Display Selected ...

  3. Spring 配置JNDI数据源

    1.Spring 提供的JNDI调用类. 2.使用weblogic进行部署项目,所以使用WebLogicNativeJdbcExtrator类进行配置. 3.配置完数据源后配置sessionFacto ...

  4. iOS 第三方登录之 QQ登录

    一. 首先需要下载腾讯qq登录所需的库,下载地址是http://open.qq.com/ . 需要用到的有TencentOpenAPI.framework 和TencentOpenApi_IOS_Bu ...

  5. HttpServlet cannot be resolved to a type解决方法

    1:是因为没有加入servlet-api.jar 2:下载网址:http://download.csdn.net/detail/jiuyueguang/5745209 3:然后在项目右键->bu ...

  6. CodeChef Forest Gathering —— 二分

    题目链接:https://vjudge.net/problem/CodeChef-FORESTGA 题解: 现场赛.拿到这题很快就知道是二分,但是一直wa,怎么修改也wa,后来又换了种错误的思路,最后 ...

  7. Redis集群与事务

    redis集群对象JedisCluster不支持事务,但是,集群里面的每个节点支持事务 但是可以用第三方呀 启动下,然后看看事务问题: /usr/local/redis/bin/redis-serve ...

  8. 关于git上传文件的一个小问题

    *** Please tell me who you are. Run git config --global user.email "you@example.com" git c ...

  9. 迁移学习算法之TrAdaBoost ——本质上是在用不同分布的训练数据,训练出一个分类器

    迁移学习算法之TrAdaBoost from: https://blog.csdn.net/Augster/article/details/53039489 TradaBoost算法由来已久,具体算法 ...

  10. PIL 安装及使用

    我ubunto虚拟机自带的是python2.7,好像PIL也只支持到2.7. PIL包的安装 Debian/Ubunto Linux下直接安装: sudo apt-get install python ...