您的位置:首页 > 编程语言 > C语言/C++

深入探索c/c++函数(3)---虚成员函数调用的基本过程

2011-08-18 21:00 453 查看
学过c++一段时间都会知道,c++是依靠虚函数实现多态的,如下代码:

view
plain

#include <iostream>

using namespace std;

class Base

{

public:

virtual void Print()

{

cout<<"^-^"<<endl;

}

};

class Derive:public Base

{

public:

virtual void Print()

{

cout<<"T-T"<<endl;

}

};

int main()

{

Base *p=new Derive();

p->Print();

}

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

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

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

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

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

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



view
plain

257: int main()

258: {

01031500 push ebp

01031501 mov ebp,esp

01031503 sub esp,0DCh

01031509 push ebx

0103150A push esi

0103150B push edi

0103150C lea edi,[ebp-0DCh]

01031512 mov ecx,37h

01031517 mov eax,0CCCCCCCCh

0103151C rep stos dword ptr es:[edi];开启堆栈帧(/RTCs)后,就会有类似的汇编,将未

;初始化的局部变量值初始化为cc,一个int 3指令

;如果输出未初始化的一个int 变量,值就是-858993460

;因为数据以补码保存,-858993460补码就是0xcccccccc

;输出未初始化的字符则输出 烫,这就是我们debug时,

;内存里老多 烫烫烫烫烫烫的原因。





259: Base *p=new Derive();

0103151E push 4 ;operator new函数参数入栈,即要为Derive对象分配四个字节的空间。

01031520 call operator new (1031208h) ;调用operator new函数

01031525 add esp,4 ;__cdecl调用约定,函数调用者,调整栈帧,

01031528 mov dword ptr [ebp-0D4h],eax;将operator new 函数返回结果存入dword ptr[ebp-0D4h]

;这段空间,operator new结果返回一个指针,指向分配的内

;存的地址,vc中整形或者能隐式转化成整形的返回值放入eax

0103152E cmp dword ptr [ebp-0D4h],0 ;测试返回值是否为0

01031535 je main+4Ah (103154Ah) ;为0则调转

01031537 mov ecx,dword ptr [ebp-0D4h];将operator new 分配的内存地址放入ecx,vc中成员函数

;调用时this指针存入ecx的。

0103153D call Derive::Derive (1031127h) ;调用构造函数,在构造函数里完成虚表指针的初始化,

;由于没有显时定义默认构造函数,所以编译器负责生成一个

01031542 mov dword ptr [ebp-0DCh],eax ;编译器生成的默认构造函数中,将构造好虚表指针的对象的地

;址放入了eax,所以这句相当于取对象的地址。编译器生成的

;默认构造函数代码稍后介绍

01031548 jmp main+54h (1031554h) ;跳过下一条指令的执行。

0103154A mov dword ptr [ebp-0DCh],0;如果走这条指令说明是je main+4Ah (103154Ah)

;跳转过来的,说明内存分配失败,这条指令的作用就是将p值设为0,

;也就是this值设为0,以期望this+偏移访问数据时触发一个异常。

01031554 mov eax,dword ptr [ebp-0DCh] ;如果内存分配没有问题的话,那么dword ptr [ebp-0DCh]

;保存的是对象的地址值。

0103155A mov dword ptr [p],eax ;把对象的地址值赋给dword ptr [p]这段空间,下面的代码就是

;就是通过虚表指针查找虚表的关键代码了,要说关键点了

260: p->Print();

0103155D mov eax,dword ptr [p] ;将对象的地址值存入eax,现在eax=p(p指向对象的起始地址)

01031560 mov edx,dword ptr [eax] ;通过eax寻址,对应的操作是从eax对应的地址值开始往高地址涵盖

;双字,即4个字节,将这4个字节里的数据按照整形方式读出赋给edx

;相当于edx=*(int*)p,前面说了对象的前4个字节是为虚表指针所分

;配的空间,这句指令相当于获取虚表指针的值。

01031562 mov esi,esp

01031564 mov ecx,dword ptr [p] ;this指针存入ecx

01031567 mov eax,dword ptr [edx];查找虚表的操作,跟上面的分析一样,从edx对应地址值开始,往高

;地址涵盖4个字节的内存,读出这段内存里的数据,可以知道edx的值

;即是虚表指针的值,虚表指针指向一个虚表,虚表的地址假设为

;0x00100000,那么0x00100000~0x00100003是存储第一个虚函数的

;地址,0x00100004~0x00100007是存储第二个虚函数的地址.....

;这条指令即是获取第一个虚函数的地址,eax=*(int*)*(int*)p

;现在eax值是一个合法函数指针的值了

01031569 call eax ;进行函数调用

0103156B cmp esi,esp

0103156D call @ILT+435(__RTC_CheckEsp) (10311B8h)

261: }

01031572 xor eax,eax

01031574 pop edi

01031575 pop esi

01031576 pop ebx

01031577 add esp,0DCh

0103157D cmp ebp,esp

0103157F call @ILT+435(__RTC_CheckEsp) (10311B8h)

01031584 mov esp,ebp

01031586 pop ebp

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处的汇编指令:



view
plain

Derive::Derive:

010315B0 push ebp

010315B1 mov ebp,esp

010315B3 sub esp,0CCh

010315B9 push ebx

010315BA push esi

010315BB push edi

010315BC push ecx

010315BD lea edi,[ebp-0CCh]

010315C3 mov ecx,33h

010315C8 mov eax,0CCCCCCCCh

010315CD rep stos dword ptr es:[edi]

010315CF pop ecx

010315D0 mov dword ptr [ebp-8],ecx;将ecx中保存的this指针值存入dword ptr [ebp-8]

010315D3 mov ecx,dword ptr [this] ;this指针存入ecx,调用成员函数用,单继承下

;dword ptr [this]和dword ptr [ebp-8]值是一样

010315D6 call Base::Base (1031131h);调用基类构造函数

010315DB mov eax,dword ptr [this] ;dword ptr [this]指向对象已通过Base::Base

;进行了初始化,此时虚表指针指向了父类的虚表

010315DE mov dword ptr [eax],offset Derive::`vftable' (1037834h)

;将dword ptr [this]指向对象的虚表指针修改成

; Derive::`vftable' ,dword ptr [this]相当于

;一个对象指针,假设为p,这句指令相当于*(int*)p=

;Derive::`vftable' .

010315E4 mov eax,dword ptr [this] ;将初始化好的对象地址存入eax,相当于设置返回值

010315E7 pop edi

010315E8 pop esi

010315E9 pop ebx

010315EA add esp,0CCh

010315F0 cmp ebp,esp

010315F2 call @ILT+435(__RTC_CheckEsp) (10311B8h)

010315F7 mov esp,ebp

010315F9 pop ebp

010315FA ret

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



view
plain

Base::Base:

01031690 push ebp

01031691 mov ebp,esp

01031693 sub esp,0CCh

01031699 push ebx

0103169A push esi

0103169B push edi

0103169C push ecx

0103169D lea edi,[ebp-0CCh]

010316A3 mov ecx,33h

010316A8 mov eax,0CCCCCCCCh

010316AD rep stos dword ptr es:[edi]

010316AF pop ecx

010316B0 mov dword ptr [ebp-8],ecx

010316B3 mov eax,dword ptr [this]

010316B6 mov dword ptr [eax],offset Base::`vftable' (1037844h)

010316BC mov eax,dword ptr [this] ;和Derive类似,也有一个设置虚表指针的操作

010316BF pop edi

010316C0 pop esi

010316C1 pop ebx

010316C2 mov esp,ebp

010316C4 pop ebp

010316C5 ret

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

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

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

开始部分便安插代码。

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

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

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



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

view
plain

#include <iostream>

#include <vector>

using namespace std;



class Base

{

public:

virtual void PrintA()

{

cout<<"^-^"<<endl;

}

virtual void PrintB()

{

cout<<"T-T"<<endl;

}



};

class Derive:public Base

{

public:

virtual void PrintA()

{

cout<<":)"<<endl;

}

virtual void PrintB()

{

cout<<":("<<endl;

}

};

void Hack1()

{

cout<<"Hack1"<<endl;

}

void Hack2()

{

cout<<"Hack2"<<endl;

}

int main()

{

Base *p=new Derive();

int *pVtable[2]={(int*)Hack1,(int*)Hack2};//构造一个虚表

*(int*)p=(int)pVtable;//设置虚表指针

p->PrintA();

p->PrintB();

system("pause");

}

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





view
plain

#include <iostream>

#include <Windows.h>

using namespace std;



class Base

{

public:

virtual void PrintA()

{

cout<<"^-^"<<endl;

}

virtual void PrintB()

{

cout<<"T-T"<<endl;

}



};

class Derive:public Base

{

public:

virtual void PrintA()

{

cout<<":)"<<endl;

}

virtual void PrintB()

{

cout<<":("<<endl;

}

};

void Hack1()

{

cout<<"Hack1"<<endl;

}

int main()

{

Base *p=new Derive();

int PrintAAdress=*(int*)*(int*)p;//获取PrintA在虚表中的地址值

int PrintBAdress=*(int*)(*(int*)p+4);//获取PrintB在虚表中的地址值

//vc debug下函数指针值和函数名对应的地址开始,存放是一个jmp指令

//对应机器吗是0xe9

if (*(unsigned char*)PrintAAdress==0xe9)

{

DWORD d;

int PrintBOffset=*(int*)(PrintBAdress+1);//获取jmp指令后立即数的值

int Hack1Offset=*(int*)((int)Hack1+1);

//jmp 后立即数是相对于本条jmp指令的偏移,这里想把虚表的PrintA地址修

//改成PrintB,所以重新计算偏移

int diff=PrintBOffset-(PrintAAdress-PrintBAdress);

WriteProcessMemory(GetCurrentProcess(),(int*)(PrintAAdress+1), &diff, 4, &d);



diff=Hack1Offset-(PrintBAdress-(int)Hack1);

WriteProcessMemory(GetCurrentProcess(),(int*)(PrintBAdress+1), &diff, 4, &d);

//release下函数指针和函数名的值就是函数对应汇编指令的起始地址

}else{

DWORD dwIdOld;

HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,1,GetCurrentProcessId());

//把对应的内存页修改成可读写的,debug下权限比较大,所以可以直接读写

VirtualProtectEx(hProcess,(int*)*(int*)p,4,PAGE_READWRITE,&dwIdOld);

WriteProcessMemory(hProcess,(int*)*(int*)p, &PrintBAdress, 4, 0);

VirtualProtectEx(hProcess,(int*)*(int*)p,4,dwIdOld,&dwIdOld);



int Hack1Adress= (int)Hack1;

VirtualProtectEx(hProcess,(int*)(*(int*)p+4),4,PAGE_READWRITE,&dwIdOld);

WriteProcessMemory(hProcess,(int*)(*(int*)p+4), &Hack1Adress, 4, 0);

VirtualProtectEx(hProcess,(int*)(*(int*)p+4),4,dwIdOld,&dwIdOld);

}

//现在成功改写了虚表,所有Derive对象动态绑定,都会转到PrintB和Hack1上

p->PrintA();

p->PrintB();



}

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

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

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

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

声明:转自 demon__hunter
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: