C++函数调用过程解析
编译环境:Windows 10 + VS2015。
0、引言
函数调用的过程实际上也就是一个中断的过程,本文演示和深入分析参数入栈、函数跳转、保护现场、恢复现场等函数调用过程。
首先对三个常用的寄存器进行说明:
- EIP:指令指针,即指向下一条即将执行的指令的地址。
- EBP:基址指针,常用来指向栈底。
- ESP:栈指针,常用来指向栈顶。
先看简单程序,并在Visual Studio 2015中查看并分析汇编代码。
图 1
1、函数调用
g_fun函数调用的汇编代码如图2所示,调用g_fun函数(call指令)之前,EBP保存main函数栈基地址:0x0113FC28。
图2
调用call指令之前,需要执行三条push指令,分别将三个参数压入栈中。执行三条push指令指挥,可以查看栈中的数据进行验证(从汇编指令可以看出,参数压栈顺序为从右向左)。如图3所示,从右边的实时寄存器表中可以看到ESP(栈顶指针)值为0x0113FB50,然后从内存表中找到内存地址0x005BFD08处,可以看到内存依次存储了0x00000001(即参数a),0x00000002(即参数b),0x00000003(即参数c),此时栈顶存储的是三个参数的值,说明压栈成功。
图3
然后可以看到call指令跳到地址0x02C1302。继续执行,可以看到指令调转到0x02C1700。此时,EBP值依然是0x0113FC28(main函数栈基址),说明仍然运行main函数中的指令,暂未跳转至g_fun函数基址0x0C1700。
图4
执行jmp指令后,调转到了g_fun函数内部,图5显示0x0C1700确实是g_fun函数起始地址,如此实现了到g_fun函数的跳转。
图5
2、保存现场
此时,查看栈中数据,如图6所示,此时ESP(栈顶)值为0x0113FB4C,在内存表中可以看到栈顶存放的地址是0x002C1769,下面还是前面压栈的参数(1,2,3)。也就是执行call指令后,系统默认的往栈中压入了一个数据(0x002C1769),再看图3,call指令后面一条指令的地址就是0x002C1769,实际上就是调用函数结束后需要继续执行的指令地址,函数返回后会跳转到该地址。这就是我们常说的函数中断前的“保护现场”。这一过程是编译器隐含完成的。实际上是将EIP(指令指针)压栈,即隐含执行了一条push eip指令,在中断函数返回时,再从栈中弹出该值到EIP,程序继续往下执行。
图6
继续往下执行,进入g_fun函数后第一条指令是push ebp,即将epb入栈。因为每一个函数都有自己的栈区域没所以基地址也是不一样的。现在进入了一个中断函数,函数执行过程中也需要ebp寄存器,而在进入函数之前的main函数的ebp值怎么办呢?为了不覆盖,将它压入栈中保存。执行push ebp指令后,查看寄存器和内存中数据显示EBP存放的地址(main函数基地址0x0113FC28)确实压入ESP所指向的栈顶地址0x0113FB48。
图7
下一条mov ebp, esp将此时的栈顶地址作为该函数的栈基址,确定g_gunc函数的栈区域,EBP栈底地址为0x0113FB48,ESP栈顶地址为0x0113FB48。
图8
再往下的指令是sub esp,D8h,指令的字面意思是将栈顶指针往上移动D8h Byte。这个区域为间隔区域,将两个函数的栈区域隔开一段距离。如图8所示。而该间隔区域大小固定为D0h,即208Byte,然后还要预留出存储局部变量的内存区域。g_fun函数有两个局部变量x和y,所有esp需要移动的长度为D0h+8h=D8h。
图9
执行sub esp,D8h指令后,EBP栈基址不变为,仍为0x0113FB48。ESP栈顶地址在0x0113FB48基础上往上移动往上移动D8h Byte,变为0x0113FA70。如图10所示:
图10
接下来的三条压栈指令,分别将EBX,ESI,EDI压入栈中,这也是属于保护现场的一部分,这些是属于main函数的一些数据。EBX,ESI,EDI分别为基址寄存器,源变址寄存器,目的变址寄存器。
图11
接下来的几条指令(如下)是刚才留出的D8h的内存区域赋值为0x0CCCCCCCh。
002C170C lea edi,[ebp-0D8h]
002C1712 mov ecx,36h
002C1717 mov eax,0CCCCCCCCh
002C171C rep stos dword ptr es:[edi]
如图12所示:
图12
3、执行函数
继续往下看,接下来是局部变量x和y的赋值,汇编指令中怎样去计算x和y的地址呢?如图13所示,是基于ebp去计算的,分别是[ebp-4-4]和[epb-4-8],为什么需要多每次计算多要先行上移4个Byte呢?应该是变量之间增加间隔区域(固定值为4 Byte),保护变量之间互不影响,跟函数间隔区域类似。查看内存表可以看到响应的内存区域已经存入了0x11111111和0x22222222。
图13
此时我们对整个内存中存储的内容应该非常清晰了。如图14所示:
图14
4、恢复现场
这时,子函数部分的代码已经执行完毕,继续往下看,编译器会做一些事后处理工作,如图15所示。首先是三条出栈指令,分别从栈顶读取EDI,ESI和EBX值。从图9的内存数据分别我们可以得知此时栈顶的数据确实是EDI,ESI和EBX,这样就恢复了调用前的EDI,ESI和EBX值。这是“恢复现场”的一部分。
图15
第四条指令是mov esp,ebp即将epb的值赋给esp。什么意思呢?看看图14的内存数据分布就明白了,这条语句是让ESP指向EBP所指向的内存单元,也就是让ESP跳过一段区域,很明显掉过的区域恰好是间隔区和局部数据区域,因为函数已经退出了,这两个区域都已经没有用处了。实际上这条语句是进入函数时创建间隔区的语句sub esp,D8h的相反操作。这也刚好说明了调用函数时在栈上自动申请内存,调用结束后自动释放内存的操作。
图16
再往下是pop ebp,我们从图14的内存数据分布可以看出此时栈顶确实是存储的前EBP值,这个就恢复了调用前的EBP值(0x0113FC28),这也是“恢复现场”的一部分。该指令执行完后,内存数据分布如图17和图18所示。
图17
图18
再往下是一条ret指令,即返回指令。注意再执行指令前ESP值和EIP值(如图19所示),ESP指向栈顶地址0x0113FB4C存放的是地址0x002C1769(调用g_fun函数call指令的下一个指令地址)。
图19
执行ret指令后,查看ESP和EIP值(如图20所示),此时ESP为0x0113FB50,即往下移动了4Byte。显然此处编译器隐含执行了一条pop指令。这个值怎么这么熟悉呢!它实际上就是栈顶的4Byte数据,所以这里隐含执行的指令应该是pop eip。而这个值就是前面讲到过的,在调用call指令前压栈的call的下一条指令的地址。从图20中可以看出,正是因为EIP的值变成了0x002C1769,所以程序跳转到了call指令后面的一条指令,又回到了中断前的地方,这就是所谓的恢复断点。
图20
还没有完全结束,此时还有最后一条指令add esp, 0Ch。这个就很简单了,从图20中可以看出现在栈顶的数据是1,2,3,也就是函数调用前压入的三个实参。这是函数已经执行完了,显然这三个参数没有用处了。所以add esp, 0Ch就是让栈顶指针往下移动12Byte的位置,ESP地址由0x0113FB50变成0x0113FB5C。为什么是12Byte呢,很简单,因为入栈的是3个int数据。这样由于函数调用在栈中添加的所有数据都已清除,栈顶指针(ESP)真正回到了函数调用前的位置,所有寄存器的值也恢复到了函数调用之前。如图21所示:
图21
到处为止,函数调用结束。
C++函数调用过程解析的更多相关文章
- C语言的函数调用过程
从汇编的角度解析函数调用过程 看看下面这个简单函数的调用过程: int Add(int x,int y) { ; sum = x + y; return sum; } int main () { ; ...
- C语言的函数调用过程(栈帧的创建与销毁)
从汇编的角度解析函数调用过程 看看下面这个简单函数的调用过程: int Add(int x,int y) { ; sum = x + y; return sum; } int main () { ; ...
- 用systemtap跟踪打印动态链接库的所有c++函数调用过程
http://gmd20.blog.163.com/blog/static/168439232015475525227/ 用systemtap跟踪打印动态链接库的所有c++函数 ...
- 从一个新手容易混淆的例子简单分析C语言中函数调用过程
某天,王尼玛写了段C程序: #include <stdio.h> void input() { int i; ]; ; i < ; i++) { array[i] = i; } } ...
- c函数调用过程原理及函数栈帧分析
转载自地址:http://blog.csdn.net/zsy2020314/article/details/9429707 今天突然想分析一下函数在相互调用过程中栈帧的变化,还是想尽量以比 ...
- MHA自动Failover过程解析(updated) 转
允许转载, 转载时请以超链接形式标明文章原始出处和网站信息 http://www.mysqlsystems.com/2012/03/figure-out-process-of-autofailover ...
- 函数调用过程&生成器解释
摘自马哥解答,感谢. 函数调用过程: 假设程序是单进程,单执行流,在某一时刻,能运行的程序流只能有一个.但函数调用会打开新的执行上下文,因此,为了确保main函数可以恢复现场,在main函数调用其它函 ...
- Linux驱动调试-根据oops的栈信息,确定函数调用过程
上章链接入口: http://www.cnblogs.com/lifexy/p/8006748.html 在上章里,我们分析了oops的PC值在哪个函数出错的,那如何通过栈信息来查看出错函数的整个调用 ...
- SpringBoot的自动配置原理过程解析
SpringBoot的最大好处就是实现了大部分的自动配置,使得开发者可以更多的关注于业务开发,避免繁琐的业务开发,但是SpringBoot如此好用的 自动注解过程着实让人忍不住的去了解一番,因为本文的 ...
随机推荐
- 【LeetCode】516. Longest Palindromic Subsequence 最长回文子序列
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题思路 代码 刷题心得 日期 题目地址:https://le ...
- 1016 - Brush (II)
1016 - Brush (II) PDF (English) Statistics Forum Time Limit: 2 second(s) Memory Limit: 32 MB Afte ...
- Oracle导出导入dmp文件(exp.imp命令行)
1.说明 使用Oracle命令行导出导入dmp文件, 从而在两个数据库之间快速转移数据, 也可以用来作为数据库的备份, 将来可以快速恢复数据. 命令:导出exp.导入imp 步骤: 使用Oracle的 ...
- windows 安装GCC
1. 下载GCC执行文件 https://pan.baidu.com/s/1foOeAo29gLr_8HhTo_69pA 提取码 cs93 2. 解压文件到D:\mingw64 3. 新建系统环境变 ...
- Ant <Delete> 如何只删掉文件夹下所有文件和文件夹
用 fileset 来过滤要删掉的目录和文件 <project name="ant-project" default="example"> < ...
- vue3.0+vite+ts项目搭建--初始化项目(一)
vite 初始化项目 使用npm npm init vite@latest 使用yarn yarn create vite 使用pnpm pnpx create-vite 根据提示输入项目名称,选择v ...
- textarea换行符转换
/** * @description textarea换行符转指定字符 * @param str:要放到textarea的字符串 * @param code:要转换成换行的字符,默认为',' */ e ...
- 【Java】java基础
文章目录 Java基础 1 注释.标识符.关键字 1.1 注释 1.2 关键字 1.3 标识符 1.4 数据类型 1.4.1 基本类型 1.4.2 引用类型 1.4.3 整数类型拓展 1.4.4 浮点 ...
- Solon 开发,八、注入依赖与初始化
Solon 开发 一.注入或手动获取配置 二.注入或手动获取Bean 三.构建一个Bean的三种方式 四.Bean 扫描的三种方式 五.切面与环绕拦截 六.提取Bean的函数进行定制开发 七.自定义注 ...
- 《剑指offer》面试题56 - II. 数组中数字出现的次数 II
问题描述 在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次.请找出那个只出现一次的数字. 示例 1: 输入:nums = [3,4,3,3] 输出:4 示例 2: 输入:nums ...