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 Base1* Clone( )=0;
virtual ~Base1( ){}
};
class Base2 {
public:
virtual void ShareFunc() = 0;
virtual void Base2Only(){
cout<<this<<endl;
}
virtual Base2* Clone( )=0;
virtual ~Base2( ){}
};
class Derived : public Base1, public Base2 {
public:
virtual void ShareFunc(){}
virtual Derived* Clone( ){return NULL;}
virtual Derived* DerivedOnly( )
{return this;}
public:
Derived(): m_iValue(0) {}
private:
int m_iValue;
};
int _tmain(int argc, _TCHAR* argv[])
{
Derived obj;
cout<<&obj<<endl;
obj.Base2Only();
Base1* pDerive1 = (Base1*)&obj;
pDerive1->ShareFunc();
pDerive1->Base1Only();
Base2* pDerive2 = (Base2*)&obj;
pDerive2->ShareFunc();
pDerive2->Base2Only();
return 0;
}
“Derived支持virtual functions”的困难度,都落在了Base2 Subobject身上。主要有三个问题:
n 虚析构函数的调用virtual destructor。(如何通过第二或后继之base class的指针或应用来调用派生类的虚函数)。
n 被继承下来的Base2::Base2Only( )函数。(通过一个指向派生类的指针,调用第二个base class中一个继承而来的virtual function)。
n 一组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加法不能够在编译时期直接设定(这里的意思是不能够确定为某一个常量(eg:3,4),当可以用一个变量来表示),因为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代码,用来完成如下的任务:
n 以适当的offset值调整this指针。
n 跳转到对应的virtual function去。
Thunk技术允许virtual table slot继续内含一个简单的指针,因此多重继承不需要任何空间上的额外负担。Slots中的地址可以直接指向Virtual function,也可以指向一个相关的thunk(如果需要调整this指针的话)。于是,对于不需要调整this指针的virtual function,也就不需要承载效率上的额外负担。同时Thunk技术也可以为虚拟继承中的虚函数提供同样的解决方案。
三、VC2008 THUNK实例
下面我们来看VC2008下THUNK的具体实现:
启动调试,当Derived obj执行完毕后,我们看到obj的内存布局如下图所示:
obj有两个虚函数表,一个是for 'base1',我们称为base1vtbl;一个是for 'base2',我们称为base2vtbl。
Derived和subobject Base1共用一个虚函数表,而Base2使用另一个虚函数表,到目前为止这和我们想象中的一值。
但Derived类有一个非继承和覆盖的虚函数Derived::DerivedOnly( ),它也应该在base1vtbl中占有一个slot,很明显,在obj的对象布局中图中,我们没有看到。由于限于语义上的限制,上述的调试窗口没有显示出DerivedOnly的slot。我们可以到Base1.__vfptr所指向的内存区区看看,如下图所示:
第一行的前16字节分别对应base1vtbl的前四项,接下来的四个字节0x0041102d对应的就是DerivedOnly的slot。
下面看看Base2.__vfptr所指向的内存区:
数据结构建立起来后,我们看程序的运行。
Base1* pDerive1 = (Base1*)&obj;
0041152C lea eax,[ebp-1Ch]
0041152F mov dword ptr [ebp-28h],eax
直接将obj的地址复制给pDerive1。
1、pDerive1->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技术。
2、pDerive1->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技术。
3、Base2* 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技术。
6、obj.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
Obj是Derived对象,但调用的却是Base2::Base2Only函数,所以有必要调整this指针,让它指向Base2 subobject。
7、Base2* pb2=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指针。