[教程]逆向反汇编第二课

既然说的是函数,那么参数肯定不可缺少了,函数是有参数的,可是他们的参数传递却并不全部相同.函数传递参数有三种方式:堆栈方式 寄存器方式和通过全局变量进行隐含参数的传递.如果参数是通过堆栈传递的,就需要定义参数在堆栈中的顺序,并约定函数被调用后,由谁来平衡堆栈如果参数是通过寄存器传递的,就要确定信息存放在那个寄存器中.每种机制都有优缺点,而且这个还是和使用的语言也有关系的.
       利用堆栈传递参数
       堆栈是一种"先进后出"的存储区,栈顶指针ESP之象堆栈中第一个可用的数据项.调用函数时,调用者依次把参数压站,然后调用函数,函数被调用后,在堆栈中取得数
据,并且进行计算.函数计算结束后,或者调用者,或者函数本身修改堆栈,使堆栈恢复原形(这就是传说中的堆栈平衡).
       在参数传递中,有两个很重要的问题必须得到明确的说明:当参数个数大于1时,是按照什么顺序把参数压入堆栈的?函数结束后,由谁来平衡堆栈?这些都必须有个明确的规定,这种程序设计语言中为了实现函数而建立的协议称为调用约定.这种协议规定了函数中的参数传递方式 参数是否可变和由谁来处理堆栈等问题.不同的编程语言有着不同的调用约定.
_Cdelc(C规范)是从左到右而且由调用者来平衡堆栈的,并且允许使用VARARG.C规范(也就是_cdecl)函数参数按照从右稻作的顺序入栈,由调用者来负责清除堆栈,_codecl是C和C++程序的默认调用约定.C/C++和MFC程序默认使用调用约定是_cdecl,也可以在函数声明时加上_codecl关键字来手动指定.
PASCAL也是从左到右,是由子程序自己平衡堆栈的,但是不允许使用VARARG. PASCAL规范按从左到右的顺序压入堆栈,要求被调用函数负责清除堆栈.
stdcall却是从右到左调用参数,由子程序自己来平衡堆栈,也是可以使用VARARG,32位的ASM就是由这种方式调用的.这个调用约定是Win 32API函数采用的约定方式,踏实"标准调用"的意思,它结合C的约定入栈方式和PASCAL调用约定的调整栈指针方式,也就是函数入口参数按从右到左的顺序入栈,并由调用的函数在返回前清理传送参数的内存栈,函数参数个数固定.由函数体本身知道传进来的参数个数,因此被调用的函数可以在返回前用一条"retn"指令直接清理传递参数的堆栈.在Win 32API中,也有一些函数是_cdecl调用的,比如wsprintf.
Fastcall的调用方法时使用寄存器和堆栈来完成的,平衡堆栈也是使用子程序,到了64的ASM就变成这种方式了.
       所谓的VARARG表示参数的个数可以是不确定的;如果stdcall使用了VARARG,那么是由调用程序来平衡堆栈的,否则是由被调用程序来平衡的.
       为了更形象的了解不同类型约定的处理方式,来看看这个例子.架设调用函数test(var1,var2,var3),按_cdecl PASCAL和stdcall的调用约定,他们的反汇编代码分别如下:

可以很清楚的看到,_cedcl类型和_stdcall类型是把右边参数压入堆栈,而PASCAL则相反.在堆栈平衡上,cdecl类型是调用者用"add esp,0c"指令把12个字节参数空间清除,而PASCALL
和_stdcall类型则是子程序负责清除.
      函数对参数的存取和局部变量都是通过堆栈来定义的,非优化编译器用一个专门的寄存器(通常是ebp)对参数进行寻址.C/C++,PASCALL等高级语言的函数(子程序/过程)执行过程基本上都是一致的.情况如下:


  • 调用者将哈双女户(子程序)执行完毕时应返回的地址 参数压入堆栈;
  • 子程序使用"EBP指针+偏移量"对堆栈中的参数寻址,并取出,完成操作;
  • 子程序使用ret/retf指令返回.此时,CPU将eip置为堆栈中保存的地址,并继续予以执行.当前栈顶,堆栈操作的对象只能是字操作(占4个字节).例如,按stdcall约定调用函数test(var1,var2)(有两个参数),其汇编代码大致如下:

因为esp是堆栈指针,所以一般使用ebp来存取堆栈.其肚子喊建立情况如下:
   


  • 此例函数中有两个参数,架设执行函数前堆栈指针的esp为X;
  • 根据stdcall调用约定,先将参数var2压入堆栈,此时esp为X-04h
  • 再将参数var1压入堆栈,此时esp为K-08h
  • 参数进栈结束后,程序开始执行call指令,call指令把返回地址压入堆栈,这时候esp为X-0ch;
  • 这个时候已经在子程序中了,可以开始使用EBP来存取参数了,但为了在返回时回复ebp的值,用"push ebp"保存ebp的值,这时esp为X-10h
  • 在执行一句"mov ebp,esp",ebp被用来在堆栈中寻找调用者压入的参数,这时候[ebp+8]就是参数1,[ebp+c]就是参数2;
  • "sub esp,8",在堆栈中定义局部变量,局部变量1和2对应的地址分别是[ebp-4]和[ebp-8].函数结束时,调用"add eso,8"释放局部变量占用的堆栈.局部变量的范围从它的定义所在的代码块的结束为止,也就是说,当函数调用结束后局部变量也就消失了.
  • 最后调用"ret 8"指令来平衡堆栈,ret指令后面加一个操作数表示在ret后把堆栈指针esp加上操作数,完成同样的功能.
  • 处理完毕后,就可以开始用ebp存取参数和局部变量了.


        此外,还有一组指令,也就是enter和leave,他们可以帮助进行堆栈的维护.enter语句的作用就是"push  ebp/mov ebp,esp/sub esp,xxxx",而leave则是完成"add esp,xxxx/pop ebp"的功能.所以,上面的程序可以改成:
reter xxxx,0 ;0表示创建xxxx空间存放局部变量
........
leave ;恢复现场
ret 8 ;返回
        在许多情况下,编译器会按优化方式编译程序,堆栈寻址稍有不同,这时编译器为了把ebp寄存器省下来或尽可能减少代码以提高速度,会直接通过esp对参数进行寻址.esp的值在函数执行期间要发生变化,该变化出现在每次有数据进出堆栈时,要确定是对那个变量进行寻址,就需要知道程序当前位置的esp值是多少,为此必须从函数的开始部分跟踪.
举个用堆栈传递参数的例子:

我们在找CALL的时候,有时候不能确定我们要找的CALL是有参CALL还是无参,像这种[ebp-*]的这是局部变量,存放的数值不确定,也就是QB为什么要大家在找基址的时候不能找[ebp-*]的.
这时程序就用esp来传递参数了.


  • 假设执行函数欠堆栈指针esp的值为X;
  • 根据stdcall调用约定,先将参数var3压入堆栈,此时esp为X-04h
  • 再将var2压入堆栈,此时esp为X-08h
  • 最后将var3压入堆栈,此时esp是X-0Ch
  • 参数进栈结束后,程序开始执行call指令,call指令把返回地址压入堆栈,这个时候esp为X-10h
  • 这个时候易经在子程序中了,可以开始使用esp来存取参数了.
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值