逆向反汇编: X86进入函数的时候到底发生了什么?

一、引入问题

      随便问一个软件开发人员,函数调用的时候发生了什么,大家都会说出,“函数先保存环境,再执行函数,再恢复环境,再返回”,这样是不错,但是不够具体,我们要知道函数的调用约定、帧栈的形成、平栈、函数的识别等具体的细节;

二、函数调用完整的代码及汇编

函数调用部分

                 bool bFlag = testFunction(i, l);
00D2244C 8B 45 94             mov         eax,dword ptr [l]  
00D2244F 50                   push        eax    ;压栈变量i
00D22450 8B 4D E8             mov         ecx,dword ptr [i]  
00D22453 51                   push        ecx  ;压栈变量l
00D22454 E8 3B F3 FF FF       call        testFunction (0D21794h)  ;调用函数
00D22459 83 C4 08             add         esp,8  ;平栈,__cdecl
00D2245C 88 45 8B             mov         byte ptr [bFlag],al  

函数定义部分

 /****************************************************
    12: 模块: 
    13: 版本: 
    14: 作者: XX
    15: 时间: 2019/03/15
    16: 功能: 两个数相加大于10,返回真,其他返回假
    17: 输入参数: 
    18: 输出参数: 
    19: 返回值: 
    20: 其他: 一个简单的函数,用来查看调用函数的时候到底发生了什么。
    21: *****************************************************/
    22: bool testFunction(int param1, int param2)
    23: {
00D27BC0 55                   push        ebp  ;进入函数的第一件事情是压栈调用函数的栈底指针ebp
00D27BC1 8B EC             mov         ebp,esp  ;调整当前栈顶指针到栈底
00D27BC3 81 EC CC 00 00 00    sub         esp,0CCh  ;拉高esp,给函数局部变量存储空间,即帧栈空间
00D27BC9 53                   push        ebx  ;保存寄存器ebx
00D27BCA 56                   push        esi  ;保存寄存器esi
00D27BCB 57                   push        edi  ;保存寄存器edi
00D27BCC 8D BD 34 FF FF FF    lea         edi,[ebp-0CCh]  ;edi为分配的栈空间的首地址,用于取出此函数可用的栈空间首地址,用于后面的初始化;
00D27BD2 B9 33 00 00 00       mov         ecx,33h  ;设置ecx为0x33,这个为count = 0x33次,
00D27BD7 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  ; 将局部变量设置为0CCCCCCCh
00D27BDC F3 AB                rep stos    dword ptr es:[edi]  ; 根据ecx中的值将eax中的内容,以4字节为单位写到edi中,共0x33 * 4 = 0xCC 个字节,就是全部的帧栈被初始化为0CCCCCCCh;
    24:     int retValue = param1 + param2;
00D27BDE 8B 45 08             mov         eax,dword ptr [param1]  
00D27BE1 03 45 0C             add         eax,dword ptr [param2]  
00D27BE4 89 45 F8             mov         dword ptr [retValue],eax  
    25:     if (retValue > 10)
00D27BE7 83 7D F8 0A          cmp         dword ptr [retValue],0Ah  
00D27BEB 7E 06                jle         testFunction+33h (0D27BF3h)  
    26:     {
    27:         return true;
00D27BED B0 01                mov         al,1  ;eax作为返回值
00D27BEF EB 04                jmp         testFunction+35h (0D27BF5h)  
    28:     }
    29:     else
00D27BF1 EB 02                jmp         testFunction+35h (0D27BF5h)  
    30:     {
    31:         return false;
00D27BF3 32 C0                xor         al,al  ;eax为返回值
    32:     }
    33: }
00D27BF5 5F                   pop         edi  ;恢复寄存器edi
00D27BF6 5E                   pop         esi  ;恢复寄存器esi
00D27BF7 5B                   pop         ebx  ;恢复寄存器ebx
    32:     }
    33: }
00D27BF8 8B E5                mov         esp,ebp  ; 调整栈底为栈顶(还原esp)
00D27BFA 5D                   pop         ebp  ;
00D27BFB C3                   ret  ;取得esp指向的4字节为函数返回地址,更新EIP,程序回到返回地址处,同时,esp + 4进行平栈;

三、调用约定

      调用约定很重要,它决定了参数的入栈顺序,参数传入时存入栈区还是寄存器,同时,也决定了由谁来平栈;

      _cdecl: C/C++ 默认的调用方式,参数从右向左入栈,调用方平栈,不定参的函数可以使用;

      _stdcall: 参数从右向左入栈,被调用方平栈,不定参的函数不能使用;

      _fastcall: 寄存器方式传参数,优先将参数存储于ecx (第一个参数) 和 edx(第二个参数) 中,被调用放平栈,不定参的函数不能使用;

      被调用方平栈的调用约定,不定参是不能用的,因为被调用者不知道自己会有多少参数,不能确定栈空间大小。

     其他:_thiscall:  this指针存于ecx,其他和_stdcall 一样;

     我们上面的例子中, 调用函数后 add         esp,8   表明属于_cdecl 的方式;

   具体见:《逆向反汇编:从C/C++到汇编 2.4》

四、帧栈的形成---寄存器ebp和esp

     ebp:帧栈的结束地址,栈底,关键字base,高地址在下;

     esp:帧栈的起始地址,栈顶,关键字stack,低地址在上;

     esp和ebp 就是当前函数的栈帧,里面是该函数的局部变量,所以,ebp也叫做函数框架指针;

     进入函数后,esp拉高,开辟帧栈空间,esp小于ebp就形成了帧栈;

     在OD调试时,可以在帧栈中选中ebp,enter,就能跳到上层函数的帧栈顶,可以看到函数返回的下一条地址 和当前函数的参数;

      ebp 上面四当前函数的帧栈,下面是函数的返回地址和参数(属于调用者);

五、函数的识别

      通过上面的分析,我们可以发现,函数的开头都是:

     push ebp

     mov   ebp,esp

     sub esp, 0x XX

      函数结尾都是:

mov         esp,ebp  ; 调整栈底为栈顶(还原esp)
00D27BFA 5D                   pop         ebp  ;
00D27BFB C3                   ret  

    这就成为我们识别函数的关键所在,逆向时,找到函数头P一下,Hex-ray就能反编译处函数,如果IDA识别错误,就需要进行修改分析,可能时调用约定有问题,可能时平栈的问题,具体的情况见前面发表的《IDA 逆向技巧》7.8~7.10

      

六、总结

函数调用过程:

  1、参数传递

  2、函数调用,将返回地址压栈    

  3、保存栈底 push ebp

   4、申请栈空间,保存寄存器环境  sub         esp,0CCh  ;

00D27BC9 53                   push        ebx  ;保存寄存器ebx
00D27BCA 56                   push        esi  ;保存寄存器esi
00D27BCB 57                   push        edi  ;保存寄存器edi

   5、函数代码实现

   6、还原环境

    7、平栈空间

    8、ret 返回,函数调用结束

    9、调整esp,平栈顶

七、其他参考(此部分摘自其他博文)

Q: esp指向哪?
A: 就是栈顶,而不是栈顶的下一个元素。
Q: 如何在windbg中查看当前栈?
A: dp esp. p意指 Pointer-sized. 单列模式格式: dp /c1 esp
因为栈是向下生长的,dp是向上显示的,所以dp esp从栈顶开始,向上显示的内存,刚好就是栈的内容。
Q: call指令做了些什么事?
A: call做的仅仅是把返回地址(call的下一条指令的地址)压栈,然后跳转到目标地址。
它不会为参数压栈,也不会保存其它寄存器,更不好为局部变量分配栈空间。这些事都是程序员干的,或是编译器自动完成的。
Q: 为什么需要ebp?
A:esp虽然是栈指针,但它是游动的,只要代码中有栈操作,它就随栈顶变量而变化。在引用局部变量(局部变量存放在脆弱的栈中)时,极不方便。
引入ebp,固定指向调用帧,方便引用局部变量。当然ebp需要手动指定。所以函数开头经常可见如下代码:
push ebp     
move ebp esp
调用帧一般结构:(在OD里面,右下角的堆栈窗口,可以看到esp在上,ebp在下)
|             | High
| Parameter   | 
|-------------|
| Parameter   |
|-------------|
| Return Addr |
|-------------|
| Old ebp     | <-- New ebp
|-------------|
| Local var   |
|-------------|
| Local var   | 
|             | Low
--------------------- 
作者:键盘上的疯兔 
来源:CSDN 
原文:https://blog.csdn.net/tms_li/article/details/40707549 
版权声明:本文为博主原创文章,转载请附上博文链接!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值