澳门威尼斯人网址零基础逆向工程24_C,关于使用类成员函数作为回调的艺术

1 类内的分子函数和平时函数的自查自纠

干什么类成员函数无法一贯做为回调函数?

在C语言中,假使大家有那般的叁个函数:

1.1 首固然从参数字传送递、压栈顺序、堆栈平衡来总计.

1.参数字传送递:成员函数多传1个this指针
2.压栈顺序:成员函数会将this指针压栈,在函数调用取出
3.堆栈平衡:普通函数是外平栈
相比较图如下:
澳门威尼斯人网址 1

因为windows中,回调函数都是显式使用CALLBACk修饰符修饰,也正是_stdcall参数字传送递格局。_stdcall修饰的函数,参数从右至左依次压入堆栈,被调用者负责平衡堆栈。

int function(int a,int b)

1.2 一段C++代码的分析

那段代码单步会到哪个地方分外?为啥?

struct Person
{
    int x ;
    void Fn_1()
    {
        printf("Person:Fn_1()\n");
    }
    void Fn_2()
    {
        x = 10;
        printf("Person:Fn_2()%x\n");
    }
};


int main(int argc, char* argv[])
{
    Person* p = NULL;

    p->Fn_1();
    p->Fn_2();

    return 0;
}

剖析:此段代码会中标输出Person:Fn_1(),但在运转p->Fn_2();时出现很是,原因是,在展开x=10;的赋值操作时,类指针为NULL,访问空指针而招致错误,见下图:
澳门威尼斯人网址 2

而全体类的分子函数在概念的时候都被隐式(implicit)定义为__thiscall参数字传送递格局。__thiscall
修饰的函数参数从右至左依次压入堆栈,被调用者负责平衡堆栈。与拥有参数字传送递格局均不等同的少数:成员函数所在类的this指针被存入ecx寄存器(那么些特点只针对AMDx86架构)。

调用时一旦用result =
function(1,2)那样的办法就足以选择那一个函数。不过,当高级语言被编写翻译成总结机能够分辨的机器码时,有1个标题就可知出来:在CPU中,计算机没有艺术知道1个函数调用供给有个别个、什么样的参数,也未尝硬件能够保留这几个参数。也正是说,计算机不晓得怎么给这几个函数字传送递参数,传递参数的做事务必由函数调用者和函数本人来协调。为此,总括机提供了一种被称为栈的数据结构来协助参数字传送递。

1.3 this指针的性状

1.this指针不可能做++ — 等运算,无法再度被赋值.
2.this指针不占用结构体的宽度.

 

栈是一种先进后出的数据结构,栈有3个存款和储蓄区、二个栈顶指针。栈顶指针指向堆栈中率先个可用的数据项(被称呼栈顶)。用户可以在栈顶上倾向栈中加入数据,那么些操作被誉为压栈(Push),压栈以后,栈顶自动成为新投入数据项的地点,栈顶指针也随之修改。用户也得以从仓库中取走栈顶,称为弹出栈(pop),弹出栈后,栈顶下的2个因素变为栈顶,栈顶指针随之修改。

2 继承

怎么让类成员函数成为回调函数

函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以往,在库房中赢得数据,并实行总结。函数计算停止以后,只怕调用者、或然函数自身修改堆栈,使堆栈苏醒原装。

2.1 小结

1.继承是数据复制的技巧
2.回落重复代码的编撰
3.三个父类指针能够本着子类对象是被允许的,是安全的
4.多重继承扩展了先后的复杂度,不难出错
5.微软提议采用单继承,假诺需求多重继承能够改为多层继承

基于第二节对回调函数与类成员函数各自特色的解析。简单察觉,只要能想方法在类成员函数被调用从前设置好ecx寄存器,就能在__stdcall调用的底子上模拟出贰个总体的__thiscall调用。 

在参数字传送递中,有多少个很关键的难题必须获得明显表达:

2.2 代码分析

澳门威尼斯人网址 3

澳门威尼斯人网址 4

澳门威尼斯人网址 5

澳门威尼斯人网址 6

 

  • 当参数个数多于多个时,根据什么顺序把参数压入堆栈
  • 函数调用后,由什么人来把库房恢复生机原装

 

在高档语言中,通过函数调用约定来表明这多少个难题。常见的调用约定有:

想转手,假如大家对象的法子也是二个stdcall调用约定的办法,那么和回调函数还差什么吧?

  • stdcall
  • cdecl
  • fastcall
  • thiscall
  • naked call

只差多个参数,第3个参数对象实例的指针,在Delphi,Pascal,艾达中叫Self,C++,java,C#中叫this.VB中叫ME.

stdcall调用约定

stdcall很多时候被称呼pascal调用约定,因为pascal是早期很广阔的一种教学用总括机程序设计语言,其语法严格,使用的函数调用约定正是stdcall。在Microsoft
C++体系的C/C++编写翻译器中,平常用PASCAL宏来表明那个调用约定,类似的宏还有WINAPI和CALLBACK。

stdcall调用约定表明的语法为(在此以前文的百般函数为例):

int __stdcall function(int
a,int b)

stdcall的调用约定意味着:1)参数从右向左压入堆栈,2)函数自己修改堆栈
3)函数名自动加带领的下划线,前面紧跟一个@符号,其后紧跟着参数的尺码

以上述那几个函数为例,参数b首先被压栈,然后是参数a,函数调用function(1,2)调用处翻译成汇编语言将成为:

push 2 第二个参数入栈 push 1 第八个参数入栈 call function
调用参数,注意此时自动把cs:eip入栈

而对此函数自个儿,则足以翻译为:

push ebp
保存ebp寄存器,该寄存器将用来保存堆栈的栈顶指针,能够在函数退出时回升mov ebp,esp 保存堆栈指针 mov eax,[ebp + 8H]
堆栈中ebp指向地点从前依次保存有ebp,cs:eip,a,b,ebp +8指向a add
eax,[ebp + 0CH] 堆栈中ebp + 12处保存了b mov esp,ebp 恢复生机esp pop ebp
ret 8

而在编译时,那个函数的名字被翻译成_function@8

小心不一致编写翻译器会插入自身的汇编代码以提供编写翻译的通用性,不过大体代码如此。当中在函数开首处保留esp到ebp中,在函数甘休恢复是编译器常用的主意。

从函数调用看,2和1依次被push进堆栈,而在函数中又通过相对于ebp(即刚进函数时的堆栈指针)的偏移量存取参数。函数停止后,ret
8表示清理九个字节的库房,函数本身回复了储藏室。

那么我们要是塞给它这么些指标的地址不就行了吗.幸好stdcall约定参数是由右向左传递的,也便是说第一个参数是最终传递的,又由于stdccall约定

cdecl调用约定

cdecl调用约定又称为C调用约定,是C语言缺省的调用约定,它的定义语法是:

int function (int a ,int b) //不加修饰正是C调用约定 int __cdecl
function(int a,int b)//显明提议C调用约定

在写本文时,出乎作者的预想,发现cdecl调用约定的参数压栈顺序是和stdcall是均等的,参数首先由有向左压入堆栈。所例外的是,函数自个儿不清理堆栈,调用者负责清理堆栈。由于那种变动,C调用约定允许函数的参数的个数是不稳定的,那也是C语言的第一次全国代表大会特色。对于日前的function函数,使用cdecl后的汇编码变成:

调用处 push 1 push 2 call function add esp,8
专注:那里调用者在回复堆栈 被调用函数_function处 push ebp
保存ebp寄存器,该寄存器将用来保存堆栈的栈顶指针,能够在函数退出时上涨mov ebp,esp 保存堆栈指针 mov eax,[ebp + 8H]
堆栈中ebp指向地方在此之前依次保存有ebp,cs:eip,a,b,ebp +8指向a add
eax,[ebp + 0CH] 堆栈中ebp + 12处保存了b mov esp,ebp 复苏esp pop ebp
ret 专注,那里没有改动堆栈

MSDN中说,该修饰自动在函数名前加前导的下划线,由此函数名在符号表中被记录为_function,不过本人在编写翻译时仿佛从未看出那种变更。

是因为参数依据从右向左顺序压栈,因而最起先的参数在最相仿栈顶的地方,因而当使用不定个数参数时,第①个参数在栈中的地方一定能清楚,只要不定的参数个数能够根据第5个后者继续的明白的参数鲜明下来,就足以应用不定参数,例如对于C奥迪Q3T中的sprintf函数,定义为:

int sprintf(char* buffer,const char* format,…)

由于具有的骚乱参数都能够经过format分明,由此利用不定个数的参数是一直不难题的。

参数全部是由栈传递的.所以我们只要把目的指针间接压入栈中就行了.

fastcall

fastcall调用约定和stdcall类似,它意味着:

  • 函数的第二个和第①个DWO福特ExplorerD参数(可能尺寸更小的)通过ecx和edx传递,别的参数通过从右向左的各类压栈
  • 被调用函数清理堆栈
  • 函数名修改规则同stdcall

其声称语法为:int fastcall function(int a,int b)

 

thiscall

thiscall是绝无仅有贰个不能够明白指明的函数修饰,因为thiscall不是第三字。它是C++类成员函数缺省的调用约定。由于成员函数调用还有3个this指针,由此必须尤其处理,thiscall意味着:

  • 参数从右向左入栈
  • 要是参数个数显著,this指针通过ecx传递给被调用者;假如参数个数不分明,this指针在具有参数压栈后被压入堆栈。
  • 对参数个数不定的,调用者清理堆栈,不然函数自个儿清理堆栈

为了印证这几个调用约定,定义如下类和动用代码:

class A
{
public:
   int function1(int a,int b);
   int function2(int a,...);
};
int A::function1 (int a,int b)
{
   return a+b;
}
#include 
int A::function2(int a,...)
{
   va_list ap;
   va_start(ap,a);
   int i;
   int result = 0;
   for(i = 0 ; i < a ; i ++)
   {
      result += va_arg(ap,int);
   }
   return result;
}
void callee()
{
   A a;
   a.function1 (1,2);
   a.function2(3,1,2,3);
}

callee函数被翻译成汇编后就变成:

//函数function1调用 0401C1D push 2 00401C1F push 1 00401C21 lea
ecx,[ebp-8] 00401C24 call function1 留意,那里this没有被入栈
//函数function2调用 00401C29 push 3 00401C2B push 2 00401C2D push 1
00401C2F push 3 00401C31 lea eax,[ebp-8] 那里引入this指针 00401C34
push eax 00401C35 call function2 00401C3A add esp,14h

看得出,对于参数个数固定状态下,它就好像于stdcall,不定时则接近cdecl

但别忽视了一点,

naked call

那是一个很少见的调用约定,一般程序设计者提出并非选拔。编写翻译器不会给那种函数扩充起首化和清理代码,更奇特的是,你不能够用return再次回到再次回到值,只可以用插队汇编再次回到结果。这一般用来实形式驱动程序设计,假诺定义二个求和的加法程序,能够定义为:

__declspec(naked) int  add(int a,int b)
{
   __asm mov eax,a
   __asm add eax,b
   __asm ret 
}

留神,那个函数没有显式的return重回值,再次来到经过修改eax寄存器完毕,而且连淡出函数的ret指令都必须显式插入。上边代码被翻译成汇编以往变成:

mov eax,[ebp+8] add eax,[ebp+12] ret 8

小心那么些修饰是和__stdcall及cdecl结合使用的,前边是它和cdecl结合使用的代码,对于和stdcall结合的代码,则成为:

__declspec(naked) int __stdcall function(int a,int b)
{
    __asm mov eax,a
    __asm add eax,b
    __asm ret 8        //注意后面的8
}

关于那种函数被调用,则和日常的cdecl及stdcall调用函数一致。

call指令相当于

函数调用约定导致的周边难点

澳门威尼斯人网址,即使定义的约定和行使的预约差异等,则将招致堆栈被破坏,导致严重难题,上边是三种常见的标题:

  1. 函数原型注脚和函数体定义差异
  2. DLL导入函数时声称了不相同的函数约定

现在者为例,若是大家在dll种注明了一种函数为:

__declspec(dllexport) int func(int a,int b);//注意,这里没有stdcall,使用的是cdecl

行使时期码为:

      typedef int (*WINAPI DLLFUNC)func(int a,int b);
      hLib = LoadLibrary(...);
      DLLFUNC func = (DLLFUNC)GetProcAddress(...)//这里修改了调用约定
      result = func(1,2);//导致错误

由于调用者没有精晓WINAPI的意义错误的扩张了那么些修饰,上述代码必然导致堆栈被弄坏,MFC在编写翻译时插入的checkesp函数将告诉你,堆栈被毁坏了。

http://blog.csdn.net/adcxf/article/details/2699323

     Push 重回地址

     Jmp  函数

ret指令也就是

     pop  再次来到地址

     Jmp  重临地址

约等于说实际上在调用函数的时候栈顶保留的是回来地址,假设大家间接压入实例指针的话原来,当跳到函数体中,函数会把再次回到地址当Self,而Self则

会被当成重返地址,具体会有如何的结局大家温馨去想像一下

因而大家做的工作正是弹出再次回到地址,压入实例地址,压入重回地址,跳到目的方法去执行.

实际大家就是要布局那样一段代码当回调用,那段代码插入对象实例参数到第三个参数,然后跳到目的方法:

     pop    eax            //弹出重返地址到eax

     push   对象实例       //压入对象实例

     push   eax            //压入再次来到地址

     jmp    对应的指标方法 //跳转到相应的靶子方法

切切实实贯彻如下 

 

澳门威尼斯人网址 7

function Cunk(Obj: TObject; CallBackProc: Pointer): Pointer;
const
  PageSize = 4096;
  SizeOfJmpCode = 5;
type
  TCode = packed record
    Int3: Byte;
    PopEAX: Byte;
    Push: Byte;
    AddrOfSelf: TObject;
    PushEAX: Byte;
    Jmp: Byte;
    AddrOfJmp: Cardinal;
  end;
var
  LCode: ^TCode;
begin
  Result := VirtualAlloc(nil, PageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  LCode := Result;
  LCode^.Int3 := $90; // nop
  // LCode^.Int3 := $CC; //Int 3
  LCode^.PopEAX := $58;
  LCode^.Push := $68;
  LCode^.AddrOfSelf := Obj;
  LCode^.PushEAX := $50;
  LCode^.Jmp := $E9;
  LCode^.AddrOfJmp := DWO昂CoraD(CallBackProc) – (DWO卡宴D(@LCode^.Jmp) + SizeOfJmpCode); // 总括相对地址
end;

procedure Runk(Thunk: Pointer);
begin
  VirtualFree(Thunk, 0, MEM_RELEASE);

end;

 

http://www.cnblogs.com/toosuo/archive/2012/01/07/2315574.html

相关文章