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如此好用的 自动注解过程着实让人忍不住的去了解一番,因为本文的 ...
随机推荐
- 【剑指Offer】二叉搜索树的后序遍历序列 解题报告(Python)
[剑指Offer]二叉搜索树的后序遍历序列 解题报告(Python) 标签(空格分隔): 剑指Offer 题目地址:https://www.nowcoder.com/ta/coding-intervi ...
- Oracle VirtualBox 配置宿主机与虚拟机互访
写作原因 之前经常使用 VMware Workstation Pro,由于是非正常付费使用的不打算再用了.替代品就是 Oracle VirtualBox,但是发现了一个问题: "我想用NAT ...
- 面试造火箭系列,栽在了cglib和jdk动态代理
"喂,你好,我是XX巴巴公司的技术面试官,请问你是张小帅吗".声音是从电话那头传来的 "是的,你好".小帅暗喜,大厂终于找上我了. "下面我们来进行一 ...
- Dubbo 的设计思想
在java远程调用多年的沉淀 <1>首先是socket调用.在orderService中开放socket服务,在userService中进行远程调用. 优点:解决了单机调用的问题. 缺点: ...
- 「算法笔记」Splay
一.简介 Splay(伸展树)是平衡树中的一种.它通过不断将某个节点旋转到根节点的位置,使整棵树仍满足 BST 的性质,并且保持平衡而不至于退化为链. 频繁访问的节点会被移动到离根节点较近的位置,进而 ...
- vue 设置请求超时时间处理
Vue.http.post('http://114.214.164.77:2222/crptorgraphy',{msg:JSON.stringify(req)},{emulateJSON:true, ...
- Eclipse+Maven+JDK+tomcat搭建java的开发环境
由于最近有几个同事都在学习java方面的东西,所以我写个博文做下笔记,其中遇到过很多个坑,这里就不多说了 首先,我用的是Eclipse+Maven的组合,用Ecplise是周边java开发的同事用这个 ...
- Window10系统修改hosts文件的方法
背景: 调试smtp程序时遇到问题,度娘说需要修改hosts文件 使用老方法修改了很久,始终无法保存 又百度了一下,在此重温,以加深记忆 方法: Step1.同时按住Windows+X Step2.选 ...
- docker安装elasticsearch6.8.3-单机模式及可视化Kibana6.8.3
docker安装elasticsearch6.8.3-单机模式 拉取镜像 docker pull elasticsearch:6.8.3 创建容器 测试环境加上-e "discovery. ...
- java.exe and -classpth or -cp
mydirname=$(dirname $0) java -cp $classes_dir:$lib_dir/*:$config_dir -Doracle.net.wallet_location=${ ...