编译环境: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

到处为止,函数调用结束。

最新文章

  1. STM32用JLINK 烧写程序时出现NO Cortex-m device found in JTAG chain现象和解决方案
  2. PHP常用算法
  3. 2-5. Working with Compile Time Constants
  4. HDU4430 Yukari's Birthday(枚举+二分)
  5. 利用SecureCRT上传、下载文件(使用sz与rz命令),超实用!
  6. 如何从Windows Phone 生成PDF文档
  7. Ubuntu 无法拖拽复制
  8. IOC(控制反转)
  9. Apache HTTPserver安装后报:无法启动,由于应用程序的并行配置不对-(已解决)
  10. 洛谷P4003 无限之环(infinityloop)(网络流,费用流)
  11. Mysql表结构导出excel(含数据类型、字段备注注释)
  12. 一个word文档中,多个表格的批量调整(根据窗口调整表格和添加表格水平线)
  13. 最新最全的Java面试题整理(内附答案)
  14. Attempting to badge the application icon but haven't received permission from the user to badge the application错误解决办法
  15. Atitit.每周计划日程表 流程表v3
  16. ROS 进阶学习笔记(12) - Communication with ROS through USART Serial Port
  17. hdu 5831 Rikka with Parenthesis II 线段树
  18. How to deal with "Could not find component on update server. Contact VMware Support or your system administrator." in Vmware.
  19. argc和argv
  20. Java-二分查找算法

热门文章

  1. 【LeetCode】48. Rotate Image 解题报告(Python & C++)
  2. 【LeetCode】583. Delete Operation for Two Strings 解题报告(Python & C++)
  3. Sublime Text3快速创建HTML5框架
  4. 初识JavaScript变量
  5. c#16进制转浮点数单精度类型
  6. 利用自定义动画 animate() 方法,实现某图书网站中“近 7 日畅销榜”中的图书无缝垂直向上滚动特效:当光标移入到图书上时,停止滚动,鼠标移开时,继续滚动
  7. SpringBoot中JPA使用动态SQL查询
  8. 线性基(Linear Basis)学习笔记
  9. DGHV同态库
  10. django 字段默认值