成员函数调用

c++的成员函数根据其调用的不同,大致可以分为4类:内联成员函数,静态成员函数,虚成员函数和上述3种以外的普通成员函数。

普通的成员函数:

    

普通的成员函数在被调用时有两大特征:

1 普通的成员函数是静态绑定的, 
2 普通的成员函数调用时编译器隐式传入this指针的值。

#include <iostream>
using namespace  std;
class Test
{
public:
void Print(int i);
};
void Test::Print(int i)
{
cout<<i<<endl;
}
int main()
{
Test *p=new Test();
p->Print(2);
system("pause");
}

所谓的静态绑定实质是c++源代码编译时,编编译器在p->Print();处翻译成直接调用Test类中Print()的汇编代码,也就是编译期编译器就确定了被调函数的相对地址。而所谓的动态绑定实质是,源码在编译的时候,编译器不是翻译成直接调用Test类中Print()的汇编代码,而是翻译成一个查找虚表,得到到函数的相对地址的过程。编译器调用Print()时是根据p类型来确定调用哪个类的Print()函数时,也就是说根据->(或者.)左边对象的类型来确定调用的函数,同时编译器也是根据对象的类型来确定该成员函数是否能够被合法的调用,而这个校验是发生在编译期的类型静态检查的,也就是只是一个代码级的检查的。不管对象的真正类型是什么,只要被强制转化成了Test类型,编译器就会接受p->Print(2);的调用,从而翻译成调用Print的代码。

int i=0;
 ((Test*)&i)->Print(2);

((Test*)0)->Print(2);

再说第二点,函数参数入栈后,this指针的值也会入栈或者存入ecx寄存器。而this指针的值可以认为是p的值,也就是->左边对象的值。传入this值的目的是为了操作对象里的数据,通过类的声明,编译器可以确定对象内成员变量的相对于类对象起始地址的偏移,即相对this值的偏移。而成员函数调用时隐式传入的this值,编译器是不对this值进行检查,编译器只是简单生成this+偏移操作对象的汇编代码,所以->左边对象的类型正确,编译器就会找到相应的成员函数,不管传入this值是否正确,只要this+偏移访问的地址是合法的,os也不会抱怨,一旦this+偏移不合法,激活os的异常机制,程序才会宕了

学过c++一段时间都会知道,c++是依靠虚函数实现多态的,如下代码:

  1. #include <iostream>  
  2. using namespace  std;  
  3. class Base  
  4. {  
  5. public:  
  6.     virtual void Print()  
  7.     {  
  8.         cout<<"^-^"<<endl;  
  9.     }  
  10. };  
  11. class Derive:public Base  
  12. {  
  13. public:  
  14.     virtual void Print()  
  15.     {  
  16.         cout<<"T-T"<<endl;  
  17.     }  
  18. };  
  19. int main()  
  20. {  
  21.     Base *p=new Derive();  
  22.     p->Print();  
  23. }  

 呵呵,输出T-T~~~~~~

对于理解虚函数的实现原理,历来是一个c++新手到中手的必经之路之一,关于其实现原理,个人推荐《深入探索c++对象模型》这本书,

原理讲的很透彻的。现在分析一下一些主流的编译器的具体实现方式,并从汇编的角度来分析编译器的虚函数的实现原理(最近找c/c++工作,估计虚函数被问到可能性很高~~)。

首先c++标准仅仅规定了虚函数的行为,并没有规定这种行为的具体实现,但目前主流的编译器(vc,g++)在实现上达成了一定默契,都是通过在对象前4个字节安插一个虚表指针,

这个虚表指针指向对应类的虚表,在调用虚函数时,通过虚表指针查找虚表最终获得要调用的函数的,这也就是动态绑定的底层实现方式。

以下是vc10默认编译选项debug下上面程序的反汇编:

 

  1.    257: int main()  
  2.    258: {  
  3. 01031500  push        ebp    
  4. 01031501  mov         ebp,esp    
  5. 01031503  sub         esp,0DCh    
  6. 01031509  push        ebx    
  7. 0103150A  push        esi    
  8. 0103150B  push        edi    
  9. 0103150C  lea         edi,[ebp-0DCh]    
  10. 01031512  mov         ecx,37h    
  11. 01031517  mov         eax,0CCCCCCCCh    
  12. 0103151C  rep stos    dword ptr es:[edi];开启堆栈帧(/RTCs)后,就会有类似的汇编,将未  
  13.                                              ;初始化的局部变量值初始化为cc,一个int 3指令  
  14.                                              ;如果输出未初始化的一个int 变量,值就是-858993460  
  15.                                         ;因为数据以补码保存,-858993460补码就是0xcccccccc  
  16.                                         ;输出未初始化的字符则输出 烫,这就是我们debug时,  
  17.                                              ;内存里老多 烫烫烫烫烫烫的原因。  
  18.   
  19.   
  20.    259:     Base *p=new Derive();  
  21. 0103151E  push        4   ;operator new函数参数入栈,即要为Derive对象分配四个字节的空间。  
  22. 01031520  call        operator new (1031208h) ;调用operator new函数  
  23. 01031525  add         esp,4 ;__cdecl调用约定,函数调用者,调整栈帧,   
  24. 01031528  mov         dword ptr [ebp-0D4h],eax;将operator new 函数返回结果存入dword ptr[ebp-0D4h]  
  25.                                               ;这段空间,operator new结果返回一个指针,指向分配的内  
  26.                                                     ;存的地址,vc中整形或者能隐式转化成整形的返回值放入eax  
  27. 0103152E  cmp         dword ptr [ebp-0D4h],0  ;测试返回值是否为0  
  28. 01031535  je          main+4Ah (103154Ah)  ;为0则调转  
  29. 01031537  mov         ecx,dword ptr [ebp-0D4h];将operator new 分配的内存地址放入ecx,vc中成员函数  
  30.                                                    ;调用时this指针存入ecx的。   
  31. 0103153D  call        Derive::Derive (1031127h) ;调用构造函数,在构造函数里完成虚表指针的初始化,  
  32.                                                 ;由于没有显时定义默认构造函数,所以编译器负责生成一个    
  33. 01031542  mov         dword ptr [ebp-0DCh],eax  ;编译器生成的默认构造函数中,将构造好虚表指针的对象的地  
  34.                                                       ;址放入了eax,所以这句相当于取对象的地址。编译器生成的  
  35.                                                       ;默认构造函数代码稍后介绍  
  36. 01031548  jmp         main+54h (1031554h)       ;跳过下一条指令的执行。  
  37. 0103154A  mov         dword ptr [ebp-0DCh],0;如果走这条指令说明是je main+4Ah (103154Ah)      
  38.                                             ;跳转过来的,说明内存分配失败,这条指令的作用就是将p值设为0,  
  39.                                             ;也就是this值设为0,以期望this+偏移访问数据时触发一个异常。  
  40. 01031554  mov         eax,dword ptr [ebp-0DCh] ;如果内存分配没有问题的话,那么dword ptr [ebp-0DCh]  
  41.                                                ;保存的是对象的地址值。   
  42. 0103155A  mov         dword ptr [p],eax   ;把对象的地址值赋给dword ptr [p]这段空间,下面的代码就是  
  43.                                                 ;就是通过虚表指针查找虚表的关键代码了,要说关键点了  
  44.    260:     p->Print();  
  45. 0103155D  mov         eax,dword ptr [p]  ;将对象的地址值存入eax,现在eax=p(p指向对象的起始地址)  
  46. 01031560  mov         edx,dword ptr [eax] ;通过eax寻址,对应的操作是从eax对应的地址值开始往高地址涵盖  
  47.                                                ;双字,即4个字节,将这4个字节里的数据按照整形方式读出赋给edx  
  48.                                           ;相当于edx=*(int*)p,前面说了对象的前4个字节是为虚表指针所分  
  49.                                                ;配的空间,这句指令相当于获取虚表指针的值。   
  50. 01031562  mov         esi,esp    
  51. 01031564  mov         ecx,dword ptr [p]  ;this指针存入ecx  
  52. 01031567  mov         eax,dword ptr [edx];查找虚表的操作,跟上面的分析一样,从edx对应地址值开始,往高  
  53.                                               ;地址涵盖4个字节的内存,读出这段内存里的数据,可以知道edx的值  
  54.                                               ;即是虚表指针的值,虚表指针指向一个虚表,虚表的地址假设为  
  55.                                               ;0x00100000,那么0x00100000~0x00100003是存储第一个虚函数的  
  56.                                               ;地址,0x00100004~0x00100007是存储第二个虚函数的地址.....  
  57.                                          ;这条指令即是获取第一个虚函数的地址,eax=*(int*)*(int*)p  
  58.                                          ;现在eax值是一个合法函数指针的值了    
  59. 01031569  call        eax                ;进行函数调用  
  60. 0103156B  cmp         esi,esp    
  61. 0103156D  call        @ILT+435(__RTC_CheckEsp) (10311B8h)    
  62.    261: }  
  63. 01031572  xor         eax,eax    
  64. 01031574  pop         edi    
  65. 01031575  pop         esi    
  66. 01031576  pop         ebx    
  67. 01031577  add         esp,0DCh    
  68. 0103157D  cmp         ebp,esp    
  69. 0103157F  call        @ILT+435(__RTC_CheckEsp) (10311B8h)    
  70. 01031584  mov         esp,ebp    
  71. 01031586  pop         ebp    
  72. 01031587  ret    

 

通过上述分析,类似*(int*)*(int*)p这样的表达式来获取虚表中函数方法大家应该明白了吧,这个地方确实是考察指针应用的基本功的。

void(*f)()=(void(*)())*(int*)*(int*)p;

f();

最终调用的是Derive::Print();很显然*(int*)(*(int*)p+4)是虚表中第二个函数地址地址值,如果有的话~~~~~

下面来看下,编译器生成的构造函数里到底做了些什么,

Derive::Derive:
01031127  jmp         Derive::Derive (10315B0h)

找到内存10315B0h处的汇编指令:

 

  1. Derive::Derive:  
  2. 010315B0  push        ebp    
  3. 010315B1  mov         ebp,esp    
  4. 010315B3  sub         esp,0CCh    
  5. 010315B9  push        ebx    
  6. 010315BA  push        esi    
  7. 010315BB  push        edi    
  8. 010315BC  push        ecx    
  9. 010315BD  lea         edi,[ebp-0CCh]    
  10. 010315C3  mov         ecx,33h    
  11. 010315C8  mov         eax,0CCCCCCCCh    
  12. 010315CD  rep stos    dword ptr es:[edi]    
  13. 010315CF  pop         ecx    
  14. 010315D0  mov         dword ptr [ebp-8],ecx;将ecx中保存的this指针值存入dword ptr [ebp-8]  
  15. 010315D3  mov         ecx,dword ptr [this] ;this指针存入ecx,调用成员函数用,单继承下  
  16.                                                 ;dword ptr [this]和dword ptr [ebp-8]值是一样  
  17. 010315D6  call        Base::Base (1031131h);调用基类构造函数    
  18. 010315DB  mov         eax,dword ptr [this] ;dword ptr [this]指向对象已通过Base::Base   
  19.                                            ;进行了初始化,此时虚表指针指向了父类的虚表  
  20. 010315DE  mov         dword ptr [eax],offset Derive::`vftable' (1037834h)   
  21.                                            ;将dword ptr [this]指向对象的虚表指针修改成  
  22.                                                 ; Derive::`vftable' ,dword ptr [this]相当于  
  23.                                                 ;一个对象指针,假设为p,这句指令相当于*(int*)p=  
  24.                                            ;Derive::`vftable' .  
  25. 010315E4  mov         eax,dword ptr [this] ;将初始化好的对象地址存入eax,相当于设置返回值   
  26. 010315E7  pop         edi    
  27. 010315E8  pop         esi    
  28. 010315E9  pop         ebx    
  29. 010315EA  add         esp,0CCh    
  30. 010315F0  cmp         ebp,esp    
  31. 010315F2  call        @ILT+435(__RTC_CheckEsp) (10311B8h)    
  32. 010315F7  mov         esp,ebp    
  33. 010315F9  pop         ebp    
  34. 010315FA  ret    

看一下Base::Base (1031131h);汇编代码

 

  1. Base::Base:  
  2. 01031690  push        ebp    
  3. 01031691  mov         ebp,esp    
  4. 01031693  sub         esp,0CCh    
  5. 01031699  push        ebx    
  6. 0103169A  push        esi    
  7. 0103169B  push        edi    
  8. 0103169C  push        ecx    
  9. 0103169D  lea         edi,[ebp-0CCh]    
  10. 010316A3  mov         ecx,33h    
  11. 010316A8  mov         eax,0CCCCCCCCh    
  12. 010316AD  rep stos    dword ptr es:[edi]    
  13. 010316AF  pop         ecx    
  14. 010316B0  mov         dword ptr [ebp-8],ecx    
  15. 010316B3  mov         eax,dword ptr [this]    
  16. 010316B6  mov         dword ptr [eax],offset Base::`vftable' (1037844h)    
  17. 010316BC  mov         eax,dword ptr [this] ;和Derive类似,也有一个设置虚表指针的操作  
  18. 010316BF  pop         edi    
  19. 010316C0  pop         esi    
  20. 010316C1  pop         ebx    
  21. 010316C2  mov         esp,ebp    
  22. 010316C4  pop         ebp    
  23. 010316C5  ret    

分析到这里,相信大家对虚函数调用有个基本的认识了,编译器在实现虚函数时,主要有以下步骤:

1 编译时,根据类的声明,生成一个虚函数表

2 创建对象时,编译器会在类的构造函数内安插一部分代码,用来初始化对象的虚表指针,一般(vc g++)在进入构造函数

  开始部分便安插代码。

3 当以指针或引用来调用虚函数时便激活动态绑定,实质是一个通过虚表指针查找函数的过程

 所以类似这样代码Derive(){memset(this,0,sizeof(Derive));}将是灾难性的~~~

由于虚函数的实现要借助构造函数,所以构造函数不能是虚拟函数~~~

 

最后介绍两个关于c++虚函数的hack的简单程序,以加深编对译器实现虚函数机制的了解~~~~

  1. #include <iostream>  
  2. #include <vector>  
  3. using namespace  std;  
  4.   
  5. class Base  
  6. {  
  7. public:  
  8.     virtual void PrintA()  
  9.     {  
  10.         cout<<"^-^"<<endl;  
  11.     }  
  12.     virtual void PrintB()  
  13.     {  
  14.         cout<<"T-T"<<endl;  
  15.     }  
  16.   
  17. };  
  18. class Derive:public Base  
  19. {  
  20. public:  
  21.     virtual void PrintA()  
  22.     {  
  23.         cout<<":)"<<endl;  
  24.     }  
  25.     virtual void PrintB()  
  26.     {  
  27.         cout<<":("<<endl;  
  28.     }  
  29. };  
  30. void Hack1()  
  31. {  
  32.     cout<<"Hack1"<<endl;  
  33. }  
  34. void Hack2()  
  35. {  
  36.     cout<<"Hack2"<<endl;  
  37. }  
  38. int main()  
  39. {  
  40.     Base *p=new Derive();  
  41.     int *pVtable[2]={(int*)Hack1,(int*)Hack2};//构造一个虚表  
  42.     *(int*)p=(int)pVtable;//设置虚表指针  
  43.     p->PrintA();  
  44.          p->PrintB();  
  45.     system("pause");  
  46. }  

 很显然通过修改虚表指针来劫持程序,下面来通过修改虚表来劫持程序~~~~~~~~~~~~~~

 

 

  1. #include <iostream>  
  2. #include <Windows.h>  
  3. using namespace  std;  
  4.   
  5. class Base  
  6. {  
  7. public:  
  8.     virtual void PrintA()  
  9.     {  
  10.         cout<<"^-^"<<endl;  
  11.     }  
  12.     virtual void PrintB()  
  13.     {  
  14.         cout<<"T-T"<<endl;  
  15.     }  
  16.   
  17. };  
  18. class Derive:public Base  
  19. {  
  20. public:  
  21.     virtual void PrintA()  
  22.     {  
  23.         cout<<":)"<<endl;  
  24.     }  
  25.     virtual void PrintB()  
  26.     {  
  27.         cout<<":("<<endl;  
  28.     }  
  29. };  
  30. void Hack1()  
  31. {  
  32.     cout<<"Hack1"<<endl;  
  33. }  
  34. int main()  
  35. {  
  36.     Base *p=new Derive();  
  37.     int PrintAAdress=*(int*)*(int*)p;//获取PrintA在虚表中的地址值  
  38.     int PrintBAdress=*(int*)(*(int*)p+4);//获取PrintB在虚表中的地址值  
  39.     //vc debug下函数指针值和函数名对应的地址开始,存放是一个jmp指令  
  40.     //对应机器吗是0xe9  
  41.     if (*(unsigned char*)PrintAAdress==0xe9)  
  42.     {  
  43.         DWORD d;  
  44.         int PrintBOffset=*(int*)(PrintBAdress+1);//获取jmp指令后立即数的值  
  45.         int Hack1Offset=*(int*)((int)Hack1+1);  
  46.         //jmp 后立即数是相对于本条jmp指令的偏移,这里想把虚表的PrintA地址修  
  47.         //改成PrintB,所以重新计算偏移  
  48.         int diff=PrintBOffset-(PrintAAdress-PrintBAdress);  
  49.         WriteProcessMemory(GetCurrentProcess(),(int*)(PrintAAdress+1), &diff, 4, &d);  
  50.   
  51.         diff=Hack1Offset-(PrintBAdress-(int)Hack1);  
  52.         WriteProcessMemory(GetCurrentProcess(),(int*)(PrintBAdress+1), &diff, 4, &d);  
  53.     //release下函数指针和函数名的值就是函数对应汇编指令的起始地址  
  54.     }else{   
  55.         DWORD dwIdOld;  
  56.         HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,1,GetCurrentProcessId());   
  57.         //把对应的内存页修改成可读写的,debug下权限比较大,所以可以直接读写  
  58.         VirtualProtectEx(hProcess,(int*)*(int*)p,4,PAGE_READWRITE,&dwIdOld);  
  59.         WriteProcessMemory(hProcess,(int*)*(int*)p, &PrintBAdress, 4, 0);  
  60.         VirtualProtectEx(hProcess,(int*)*(int*)p,4,dwIdOld,&dwIdOld);  
  61.   
  62.         int Hack1Adress= (int)Hack1;  
  63.         VirtualProtectEx(hProcess,(int*)(*(int*)p+4),4,PAGE_READWRITE,&dwIdOld);  
  64.         WriteProcessMemory(hProcess,(int*)(*(int*)p+4), &Hack1Adress, 4, 0);  
  65.         VirtualProtectEx(hProcess,(int*)(*(int*)p+4),4,dwIdOld,&dwIdOld);  
  66.     }  
  67.     //现在成功改写了虚表,所有Derive对象动态绑定,都会转到PrintB和Hack1上  
  68.     p->PrintA();  
  69.     p->PrintB();  
  70.   
  71. }  

 上面程序就是成功修改了编译器创建的虚表,可以真正算得上一个hack了~~~,上面程序vc9/10+win7 debug/release默认编译选项通过~~~~

如果您看懂上述两个程序,相信您对虚表的编译器实现的认识更加深刻了~~~~

原理就是这个样子了,在多继承情况下,可能麻烦一些,因为对象可能产生多个虚表指针,另外虚析构函数在虚表中布局,各个编译器差异也比较大,

也就是为什么com在实现时要有一个类似release的接口~~~~


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值