编译环境: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++函数调用过程解析的更多相关文章

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

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

  2. C语言的函数调用过程(栈帧的创建与销毁)

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

  3. 用systemtap跟踪打印动态链接库的所有c++函数调用过程

    http://gmd20.blog.163.com/blog/static/168439232015475525227/             用systemtap跟踪打印动态链接库的所有c++函数 ...

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

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

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

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

  6. MHA自动Failover过程解析(updated) 转

    允许转载, 转载时请以超链接形式标明文章原始出处和网站信息 http://www.mysqlsystems.com/2012/03/figure-out-process-of-autofailover ...

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

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

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

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

  9. SpringBoot的自动配置原理过程解析

    SpringBoot的最大好处就是实现了大部分的自动配置,使得开发者可以更多的关注于业务开发,避免繁琐的业务开发,但是SpringBoot如此好用的 自动注解过程着实让人忍不住的去了解一番,因为本文的 ...

随机推荐

  1. Mod Tree(hdu2815)

    Mod Tree Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Su ...

  2. 洛谷1052——过河(DP+状态压缩)

    题目描述 在河上有一座独木桥,一只青蛙想沿着独木桥从河的一侧跳到另一侧.在桥上有一些石子,青蛙很讨厌踩在这些石子上.由于桥的长度和青蛙一次跳过的距离都是正整数,我们可以把独木桥上青蛙可能到达的点看成数 ...

  3. 【C\C++笔记】数组指针越界

    指针越界,t的数组指针越界,修改了c的内容. 使用指针时,必须规定指针移动的范围 #include <iostream> using namespace std; int main(){ ...

  4. BAIRE ONE FUNCTIONS (Baire第一类函数)

    目录 定义 导函数 一致收敛性质 的连续点 JOHNNY HU, BAIRE ONE FUNCTIONS. 一些基本的定义(诸如逐点收敛, 一致收敛\(F_{\sigma}\)集合等)就不叙述了. 定 ...

  5. Java初学者作业——编写Java程序, 实现根据用户购买商品总金额, 计算实际支付的金额及所获得的购物券金额。

    返回本章节 返回作业目录 需求说明: 编写Java程序, 实现根据用户购买商品总金额, 计算实际支付的金额及所获得的购物券金额. 购买总金额达到或超过 1000元,按 8折优惠,送 200元的购物券: ...

  6. JavaScript交互式网页设计 • 【第1章 JavaScript 基本语法】

    全部章节   >>>> 本章目录 1.1 JavaScript 概述 1.1.1 JavaScript 简介 1.1.2 JavaScript 的概念和执行原理 1.1.3 J ...

  7. 三角网格上的寻路算法Part.2—A*算法

    背景 继上一篇三角网格Dijkstra寻路算法之后,本篇将继续介绍一种更加智能,更具效率的寻路算法-A*算法,本文将首先介绍该算法的思想原理,再通过对比来说明二者之间的相同与不同之处,然后采用类似Di ...

  8. Jenkins_构建任务提示文件权限不足的处理方法

    问题现象 构建任务失败,查看日志提示读取文件权限不足. 问题分析 在linux上查看对应文件,发现这些文件只有root用户才有读的权限,jenkins默认是以jenkins用户在操作linux系统,因 ...

  9. JMeter_性能压测报错address already in use:connect

    报错截图如下: 原因分析: 这个问题的原因是windows端口被耗尽了(默认1024-5000),而且操作系统要 2~4分钟才会重新释放这些端口,所以可以增加windows的可用端口来解决.windo ...

  10. JSP页面中最常使用的脚本元素

    注:图片如果损坏,点击文章链接:https://www.toutiao.com/i6513082449755374093/ 前面简单说了一个<JSP页面实际上就是Servlet>,接下来说 ...