现在的位置: 首页 > 综合 > 正文

VC2008多重继承下的Virtual Functions:Adjustor Thunk技术

2013年07月19日 ⁄ 综合 ⁄ 共 9185字 ⁄ 字号 评论关闭
文章目录

VC2008多重继承下的Virtual Functions:Adjustor Thunk技术

一、多重继承中Virtual Functions的复杂性

在多重继承中支持virtual functions,其复杂度围绕在第二个及后继的base class身上,以及“必须在执行期间调整this指针”这一点上。看下例:

#include<iostream>

using namespace std;

class Base1  {

public:

virtual void ShareFunc() = 0;

virtual void Base1Only(){}

virtual Base1Clone( )=0;

virtual ~Base1( ){}

};

class Base2  {

public:

virtual void ShareFunc() = 0;

virtual void Base2Only(){

cout<<this<<endl;

}

virtual Base2Clone( )=0;

virtual ~Base2( ){}

};

class Derived : public Base1public Base2 {

public:

virtual void ShareFunc(){}

virtual DerivedClone( ){return NULL;}

virtual DerivedDerivedOnly( )

{return  this;}

public:

Derived(): m_iValue(0) {}

private:

int m_iValue;

};

int _tmain(int argc_TCHARargv[])

{

Derived obj;

cout<<&obj<<endl;

obj.Base2Only();

Base1pDerive1 = (Base1*)&obj;

pDerive1->ShareFunc();

pDerive1->Base1Only();

Base2pDerive2 = (Base2*)&obj;

pDerive2->ShareFunc();

pDerive2->Base2Only();

return 0;

}

Derived支持virtual functions”的困难度,都落在了Base2 Subobject身上。主要有三个问题:

虚析构函数的调用virtual destructor(如何通过第二或后继之base class的指针或应用来调用派生类的虚函数)。

 被继承下来的Base2::Base2Only( )函数。(通过一个指向派生类的指针,调用第二个base class中一个继承而来的virtual function)。

一组clone( )函数实体。( 允许一个virtual function的返回值类型有所变化,可能是base type,也可能是publicly derived type)。

看下面的式子:

Base2* pbase2=new Derived;

Delete pbase2;

编译器可能参数的代码如下:

Derived* temp=new Derived;

Base2* pbase2=temp?temp+sizeof(Base1):0;

//必须首先调用正确的virtual destructor函数实体

//然后实施delete运算符

//pbase2可能需要调整,已指出完整对象的起始点

Delete pbase2;

pbase2必须被调整,以求在一次指向derived对象的起始处。然而上述的offset加法不能够在编译时期直接设定(这里的意思是不能够确定为某一个常量(eg34),当可以用一个变量来表示),因为pbase2所指的真正对象只有在执行期才能确定。

Delete pbase2该调用操作所连带的“必要的this指针调整”操作,必须在执行期完成。也就是说,offset的大小,以及把offset加到this指针上头的那小段程序代码,必须由编译器在某个地方插入。

二、支持多重继承Virtual Function的方法

1、CFront方法

CFront编译器中的方法是将virtual table加大。每一个virtual table slot不再只是一个指针,而是一个聚合体,内含可能的offset(偏移量)以及地址(虚函数地址)。于是virtual function的调用操作由:

(*pbase2->vptr[1])(pbase2);

改变为:

*pbase2->vptr[1].faddr)

(pbase2+pbase2->vptr[1].offset)

其中faddr内含virtual function地址,offset内含this指针调整值。

这个做法的缺点是:1>改变了每一个virtual table slot的大小。

                      2>offset的额外存取和加法。

                      3>不能为虚拟继承中的虚函数提供同样的结局方案。

2、Adjustor Thunk

所谓thunk是一段assembly代码,用来完成如下的任务:

以适当的offset值调整this指针。

跳转到对应的virtual function去。

Thunk技术允许virtual table slot继续内含一个简单的指针,因此多重继承不需要任何空间上的额外负担Slots中的地址可以直接指向Virtual function,也可以指向一个相关的thunk(如果需要调整this指针的话)。于是,对于不需要调整this指针的virtual function,也就不需要承载效率上的额外负担。同时Thunk技术也可以为虚拟继承中的虚函数提供同样的解决方案

三、VC2008 THUNK实例

下面我们来看VC2008THUNK的具体实现:

启动调试,当Derived obj执行完毕后,我们看到obj的内存布局如下图所示:

VC2008多重继承下的Virtual Functions:Adjustor Thunk技术 - henry - 风云之巅

obj有两个虚函数表,一个是for 'base1',我们称为base1vtbl;一个是for 'base2',我们称为base2vtbl

Derivedsubobject Base1共用一个虚函数表,而Base2使用另一个虚函数表,到目前为止这和我们想象中的一值。

Derived类有一个非继承和覆盖的虚函数Derived::DerivedOnly( ),它也应该在base1vtbl中占有一个slot,很明显,在obj的对象布局中图中,我们没有看到。由于限于语义上的限制,上述的调试窗口没有显示出DerivedOnlyslot。我们可以到Base1.__vfptr所指向的内存区区看看,如下图所示:

VC2008多重继承下的Virtual Functions:Adjustor Thunk技术 - henry - 风云之巅

第一行的前16字节分别对应base1vtbl的前四项,接下来的四个字节0x0041102d对应的就是DerivedOnlyslot

下面看看Base2.__vfptr所指向的内存区:

VC2008多重继承下的Virtual Functions:Adjustor Thunk技术 - henry - 风云之巅

数据结构建立起来后,我们看程序的运行。

Base1* pDerive1 = (Base1*)&obj;

0041152C  lea         eax,[ebp-1Ch] 

0041152F  mov         dword ptr [ebp-28h],eax

直接将obj的地址复制给pDerive1

1pDerive1->ShareFunc();

pDerive1->ShareFunc();

00411532  mov         eax,dword ptr [ebp-28h] 

00411535  mov         edx,dword ptr [eax] 

00411537  mov         esi,esp 

00411539  mov         ecx,dword ptr [ebp-28h] 

0041153C  mov         eax,dword ptr [edx] 

0041153E  call        eax (跳转到00411e5处)

004111E5  jmp         Derived::ShareFunc (4117A0h) (调用Derived::ShareFunc()函数)

class Derived : public Base1, public Base2 {

public:

virtual void ShareFunc(){}

004117A0  push        ebp  

004117A1  mov         ebp,esp 

......

00411540  cmp         esi,esp 

00411542  call        @ILT+440(__RTC_CheckEsp) (4111BDh)

由于base1 subobject的起始地址和derived object的起始地址相同,所以通过pDerive1调用派生类的虚函数时,没有必要调整this指针,所以这里没有使用thunk技术。 

2pDerive1->Base1Only();

pDerive1->Base1Only();

00411547  mov         eax,dword ptr [ebp-28h] 

0041154A  mov         edx,dword ptr [eax] 

0041154C  mov         esi,esp 

0041154E  mov         ecx,dword ptr [ebp-28h] 

00411551  mov         eax,dword ptr [edx+4] 

00411554  call        eax  (跳转到00411e5处)

00411127  jmp         Base1::Base1Only (411760h)(调用Base1::ShareFunc()函数)

class Base1  {

public:

virtual void ShareFunc() = 0;

virtual void Base1Only(){}

00411760  push        ebp  

00411761  mov         ebp,esp 

00411763  sub         esp,0CCh 

.......

00411556  cmp         esi,esp 

00411558  call        @ILT+440(__RTC_CheckEsp) (4111BDh) 

通过base1调用自己的函数,当然用不到thunk技术。

3Base2* pDerive2 = (Base2*)&obj;

Base2* pDerive2 = (Base2*)&obj;

0041155D  lea         eax,[ebp-1Ch] 

00411560  test        eax,eax (检测&obj是否为零)

00411562  je          wmain+92h (411572h) 

00411564  lea         ecx,[ebp-1Ch] 

00411567  add         ecx,4 (调整地址的值)

0041156A  mov         dword ptr [ebp-114h],ecx 

00411570  jmp         wmain+9Ch (41157Ch) 

00411572  mov         dword ptr [ebp-114h],0 

0041157C  mov         edx,dword ptr [ebp-114h] 

00411582  mov         dword ptr [ebp-34h],edx 

将派生类对象的地址复制第二个基类的地址时,不仅检测地址是否为零,同时还将地址的值做出相应的调整,使其指向base2 suboubject。

4、pDerive2->ShareFunc();

pDerive2->ShareFunc();

00411585  mov         eax,dword ptr [ebp-34h] 

00411588  mov         edx,dword ptr [eax] 

0041158A  mov         esi,esp 

0041158C  mov         ecx,dword ptr [ebp-34h] 

0041158F  mov         eax,dword ptr [edx] 

00411591  call        eax  (跳转到thunk处)

0041114A  jmp         [thunk]:Derived::ShareFunc`adjustor{4}' (411C40h) 

[thunk]:Derived::ShareFunc`adjustor{4}':

00411C40  sub         ecx,4 (调整this指针的值,指向derived object)

00411C43  jmp         Derived::ShareFunc (4111E5h) (调用derived virtual function)

004111E5  jmp         Derived::ShareFunc (4117A0h)

class Derived : public Base1, public Base2 {

public:

virtual void ShareFunc(){}

004117A0  push        ebp  

004117A1  mov         ebp,esp 

                               004117A3  sub         esp,0CCh 

                               ......

00411593  cmp         esi,esp 

00411595  call        @ILT+440(__RTC_CheckEsp) (4111BDh) 

pDerived2指向base2 subobject,它和Derived object的起始地址不一样,而现在要调用Derived的函数,相应的this指针必须指向Derived object,所以必须调整this指针,以符合成员函数的this指针必须指向该成员函数所属的对象。

5、pDerive2->Base2Only();

pDerive2->Base2Only();

0041159A  mov         eax,dword ptr [ebp-34h] 

0041159D  mov         edx,dword ptr [eax] 

0041159F  mov         esi,esp 

004115A1  mov         ecx,dword ptr [ebp-34h] 

004115A4  mov         eax,dword ptr [edx+4] 

004115A7  call        eax  

00411023  jmp         Base2::Base2Only (411690h) 

class Base2  {

public:

virtual void ShareFunc() = 0;

virtual void Base2Only(){}

00411690  push        ebp  

00411691  mov         ebp,esp 

00411693  sub         esp,0CCh 

004115A9  cmp         esi,esp 

004115AB  call        @ILT+440(__RTC_CheckEsp) (4111BDh) 

虽然pDerived2指向base2 subobject,它和Derived object的起始地址不一样,但现在要调用的是base2的函数,所以没有必要调整this指针,也就没有必要使用thunk技术。

6obj.Base2Only();

obj.Base2Only();

004115B0  lea         ecx,[ebp-18h] (调整this指针,obj地址为[ebp-1ch])

004115B3  call        Base2::Base2Only (411023h) 

00411023  jmp         Base2::Base2Only (411690h) 

class Base2  {

public:

virtual void ShareFunc() = 0;

virtual void Base2Only(){}

00411690  push        ebp  

00411691  mov         ebp,esp

ObjDerived对象,但调用的却是Base2::Base2Only函数,所以有必要调整this指针,让它指向Base2 subobject

7、Base2pb2=pDerive2->Clone();

Base2* pb2=pDerive2->Clone();

004115B8  mov         eax,dword ptr [ebp-34h] 

004115BB  mov         edx,dword ptr [eax] 

004115BD  mov         esi,esp 

004115BF  mov         ecx,dword ptr [ebp-34h] 

004115C2  mov         eax,dword ptr [edx+8] 

004115C5  call        eax  

0041119A  jmp         [thunk]:Derived::Clone`adjustor{4}' (411B90h) 

[thunk]:Derived::Clone`adjustor{4}':

00411B90  sub         ecx,4 

00411B93  jmp         Derived::Clone (411168h) 

00411168  jmp         Derived::Clone (411BA0h) 

Derived::Clone:

00411BA0  push        ebp  

00411BA1  mov         ebp,esp 

00411BA3  sub         esp,0D4h 

00411BA9  push        ebx  

00411BAA  push        esi  

00411BAB  push        edi  

00411BAC  push        ecx  

00411BAD  lea         edi,[ebp-0D4h] 

00411BB3  mov         ecx,35h 

00411BB8  mov         eax,0CCCCCCCCh 

00411BBD  rep stos    dword ptr es:[edi] 

00411BBF  pop         ecx  

00411BC0  mov         dword ptr [ebp-8],ecx 

00411BC3  mov         ecx,dword ptr [this] 

00411BC6  call        Derived::Clone (4110FFh) 

00411BCB  mov         dword ptr [ebp-0D0h],eax 

00411BD1  cmp         dword ptr [ebp-0D0h],0 

00411BD8  je          Derived::Clone+4Bh (411BEBh) 

00411BDA  mov         eax,dword ptr [ebp-0D0h] 

00411BE0  add         eax,4 (调整返回值Derived地址加4)

00411BE3  mov         dword ptr [ebp-0D4h],eax 

00411BE9  jmp         Derived::Clone+55h (411BF5h) 

00411BEB  mov         dword ptr [ebp-0D4h],0 

00411BF5  mov         eax,dword ptr [ebp-0D4h] 

00411BFB  pop         edi  

00411BFC  pop         esi  

00411BFD  pop         ebx  

00411BFE  add         esp,0D4h 

00411C04  cmp         ebp,esp 

00411C06  call        @ILT+440(__RTC_CheckEsp) (4111BDh) 

00411C0B  mov         esp,ebp 

00411C0D  pop         ebp  

00411C0E  ret           

004115C7  cmp         esi,esp 

004115C9  call        @ILT+440(__RTC_CheckEsp) (4111BDh) 

004115CE  mov         dword ptr [ebp-40h],eax 

小结

C++成员函数调用语意:成员函数中的this指针必须指向该成员函数所属的对象。单继承中虚函数的调用会始终保持这种语意,因为派生类对象的地址和基类子对象的地址一致,无论是通过派生类调用基类的函数还是通过基类调用派生类的函数,他们的this指针都只有一个,那就是派生类对象的起始地址。但是在多重继承中,第二以及其之后的基类子对象的起始地址和派生类对象的启示地址存在偏差,所以在通过基类指针调用派生类函数时(多态),由于其不满足成员函数调用语意,所以必须调整this指针。同理,通过派生类对象调用基类虚函数时(继承来的虚函数),也必须调整this指针。

抱歉!评论已关闭.