• VS下函数调用汇编解析


    #include <stdio.h>
    
    int Output(int a, int b) 
    {
    	int c = a + b;
    	return c;
    }
    
    int Test(int a, int b)
    {
    	return Output(a, b);
    }
    
    int __cdecl main(int argc, char* argv[]) {
    	int a = 10;
    	int b = 20;
    	Test(a, b);
    
    	return 0;
    }
    

    直接打开反汇编,内存,寄存器和局部变量四个窗口
    图1

    这里先看看进main函数之前的栈基址ebp和栈顶esp
    ebp = 0x0117FE2C,esp = 0x0117FE1C
    即调用main函数的栈大小实际只有16个字节。
    图2

    push esp之后,ebp = 0x0117FE2C压栈,同时esp = 0x0117FE1C - 4 = 0x0117FE18
    图3

    mov ebp,esp之后,ebp变为0x0117FE18

    sub esp,0xD8之后,即重新定位esp位置向低地址偏移0xD8长度,偏移后esp = 0x0117FD40

    图4

    这个时候esp = 0x0117FD40,经过三次push,esp = 0x0117FD34即原值-0xC
    而ebp还是0x0117FE18
    图5

    这4条整理用于清栈空间,本应该是ebp(高地址)到esp(低地址),但由于在设定函数栈后,又压了寄存器,故而esp已经变了,这里通过ebp到ebp-0xD8后即栈空间大小,eax用于存放初始化值,ecx用于计数为应该是rep stos专用寄存器,这条指令重复ecx次数,将es:[edi]地址初始化为eax的值,同时es:[edi]值更新。功能就是初始化栈为全0xCCCCCCCC,0xCC为INT 3的指令码,如果取到了0xCCCCCCCC就进入调试模式。
    图6

    然后执行局部变量的定义,可看到对a, 和b顺序压栈,而且是从基址ebp开始的,至于中间为何空了部分字节,这个原因还不清楚。
    图7

    继续看到两个push即从右往左的顺序将b和a的值压栈了。esp -= 8,即esp = 0x0117FD34 - 0x8 = 0x0117FD2C
    图8

    在call Test函数之前,我们先注意一下,call之后的下一个指令的地址,即0x00C71779
    图9

    进入到Test函数像main函数一样
    先对上一个函数栈的ebp进行压栈,但是在压栈之前我们看到栈中多了一条数据,这条命令不在反汇编代码之中,即push EIP(下一条将要执行的指令地址),是将跳转到Test函数时的main即call后面的那条指令的地址即0x00C71779压栈,esp更新为esp - 0x4 = 0x0117FD28,然后进入Test函数又压了main函数的ebp即0x0117FE18,压栈后esp - 0x4 = 0x0117FD24,然后mov ebp ,esp执行完,,Test函数的ebp = 0x0117FD24, 然后继续重新分配和初始化Test函数栈,和main中一样的流程,esp = 0x0117FD24 - 0xC0(函数栈大小) - 0xC(3个寄存器) = 0x0117FC58
    图10

    然后经过压b和a形参,esp = 0x0117FC58 - 0x8 = 0x0117FC50
    执行call Output前压了EIP后, esp = 0x0117FC50 - 0x4 = 0x0117FC4C

    图11

    进Output后压了Test的ebp即0x0117FD24,此时esp - 4 = 0x0117FC48

    图12

    这张图是为了看之前压入的EIP的地址即0x00C7170B

    图13

    继续在Output中重新初始化Output的栈基址ebp和更新esp,ebp = 0x0117FC48,同时esp经过分配Output占空间和压寄存器 esp = 0x0117FC48 - 0xCC - 0xC(34) = 0x0‭117 FB70‬,但实际初始化Out栈为0xCC的是0x0117FC48到0x0117FC48-0xCC,即现在esp加34的位置。图中还没初始化完。

    图14

    这是初始化完的图,Output的ebp = 0x0117FC48,esp = 0x0‭117 FB70,图中指出了各指令对应操作的内存数据。

    图15

    可见运算的内存结果存放在栈基址ebp = 0x0117FC48开始后的某个位置

    图16

    前面我们是一层一层的进入函数,这里开始要一层层的退出函数了。此图是还没有执行前的
    按照FILO先入后出,从esp = 0x0‭117 FB70位置开始弹栈,之后esp + 3*4 = 0x0‭117 FB7C
    mov esp ebp,即将当前Output函数的栈基址,还原为栈顶即esp = ebp = 0x0117FC48位置, 再pop出ebp进入该函数前压入的上一个函数的栈基址,在这个位置进行了还原,EBP = 0x0117FD24,pop后esp + 4 = 0x0117FC4C,结果见图17寄存器值,看出是一致的

    图17

    这里还存在之前额外push进来的EIP,这里要pop,但反汇编代码里面没有出现。
    EIP执行pop后esp = 0x0117FC4C + 4 = 0x0117FC50。这下才退出到上一层函数即Test

    回到Test
    图18

    我们看到esp的寄存器值确实为0x0117FC50,ebp = 0x0117FD24证明还原是正确的。这里执行到了
    add esp,8;为何要对esp加8,因为之前形参压栈占用了8个字节,这里还原后esp = 0x0117FC50+ 8= 0x0117FC58
    后面的代码执行,都是恢复从main进Test时的现场保留信息
    即三个pop恢复main调用Test时的寄存器值,同时esp + 3*4 = 0x0117FC58 + 0xC = 0x0117FC64,再加上Test栈大小0xD8即esp = 0x0117FC64 + 0xC0 = 0x0117FD24,然后pop出main的栈底ebp = 后,ebp = 0x0117Fe18,esp = 0x0117FD24 + 4 = 0x0117FD28,在退出Test前还要弹出main的执行Test前的EIP,之后esp = 0x0117FD28 + 4 = 0x0117FD2C,然后我们回到main的call Test下面

    图19

    图示执行前还原的esp = 0x0117FD2C,ebp = 0x0117Fe18,对照main的寄存器指示,完全一致。

    图20

    再把esp + 8 = 0x0117FD2C + 8 = 0x0117FD34,即之前main之前Test前的形参压栈去掉
    再把main之前压栈保存的寄存器弹出esp + 3*4 = 0x0117FD34 + 0xC = 0x0117FD40
    再加上main的函数栈esp + 0xD8 = 0x0117FD40 + 0xD8 = 0x0117FE18
    再在esp位置pop出ebp即ebp = 0x0117FE2C, esp = 0x0117FE18 + 4 = 0x0117FE1C

    对照图1,可见至此,函数栈还原到调用main之前的状态。

    这里另外说一点,就是之前有个cmp ebp,esp,这个是VS的栈平衡策略,即栈在一层函数使用完毕,释放栈空间后esp应该是和进该函数前的基址ebp是一致的,这个逻辑细理一下就比较清楚了。

  • 相关阅读:
    vue 中input的输入限制
    PC端百度地理围栏、绘制工具以及判断当前坐标是否再围栏中
    js判断鼠标点击的是哪个键
    vue过滤器的使用
    3.Mybatis的配置解析
    2.MyBatis的CRUD操作
    4.JVM类加载器深入解析及重要特性剖析
    3.JVM的接口初始化规则与类加载器准备阶段和初始化阶段的重要意义分析
    2.JVM的类加载器
    1.JVM如何学习
  • 原文地址:https://www.cnblogs.com/kuikuitage/p/12346816.html
Copyright © 2020-2023  润新知