VC2008多重繼承下的Virtual Functions:Adjustor Thunk技術(shù)一、多重繼承中Virtual Functions的復(fù)雜性在多重繼承中支持virtual functions,其復(fù)雜度圍繞在第二個(gè)及后繼的base class身上,以及“必須在執(zhí)行期間調(diào)整this指針”這一點(diǎn)上??聪吕?/font> #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身上。主要有三個(gè)問(wèn)題: n 虛析構(gòu)函數(shù)的調(diào)用virtual destructor。(如何通過(guò)第二或后繼之base class的指針或應(yīng)用來(lái)調(diào)用派生類(lèi)的虛函數(shù))。 n 被繼承下來(lái)的Base2::Base2Only( )函數(shù)。(通過(guò)一個(gè)指向派生類(lèi)的指針,調(diào)用第二個(gè)base class中一個(gè)繼承而來(lái)的virtual function)。 n 一組clone( )函數(shù)實(shí)體。( 允許一個(gè)virtual function的返回值類(lèi)型有所變化,可能是base type,也可能是publicly derived type)。 看下面的式子: Base2* pbase2=new Derived; Delete pbase2; 編譯器可能參數(shù)的代碼如下: Derived* temp=new Derived; Base2* pbase2=temp?temp+sizeof(Base1):0; //必須首先調(diào)用正確的virtual destructor函數(shù)實(shí)體 //然后實(shí)施delete運(yùn)算符 //pbase2可能需要調(diào)整,已指出完整對(duì)象的起始點(diǎn) Delete pbase2; pbase2必須被調(diào)整,以求在一次指向derived對(duì)象的起始處。然而上述的offset加法不能夠在編譯時(shí)期直接設(shè)定(這里的意思是不能夠確定為某一個(gè)常量(eg:3,4),當(dāng)可以用一個(gè)變量來(lái)表示),因?yàn)?font style="font-family: 'times new roman';">pbase2所指的真正對(duì)象只有在執(zhí)行期才能確定。 Delete pbase2該調(diào)用操作所連帶的“必要的this指針調(diào)整”操作,必須在執(zhí)行期完成。也就是說(shuō),offset的大小,以及把offset加到this指針上頭的那小段程序代碼,必須由編譯器在某個(gè)地方插入。 二、支持多重繼承Virtual Function的方法1、CFront方法在CFront編譯器中的方法是將virtual table加大。每一個(gè)virtual table slot不再只是一個(gè)指針,而是一個(gè)聚合體,內(nèi)含可能的offset(偏移量)以及地址(虛函數(shù)地址)。于是virtual function的調(diào)用操作由: (*pbase2->vptr[1])(pbase2); 改變?yōu)椋?/span> (*pbase2->vptr[1].faddr) (pbase2+pbase2->vptr[1].offset) 其中faddr內(nèi)含virtual function地址,offset內(nèi)含this指針調(diào)整值。 這個(gè)做法的缺點(diǎn)是:1>改變了每一個(gè)virtual table slot的大小。 2>offset的額外存取和加法。 3>不能為虛擬繼承中的虛函數(shù)提供同樣的結(jié)局方案。 2、Adjustor Thunk所謂thunk是一段assembly代碼,用來(lái)完成如下的任務(wù): n 以適當(dāng)?shù)?font style="font-family: 'times new roman';">offset值調(diào)整this指針。 n 跳轉(zhuǎn)到對(duì)應(yīng)的virtual function去。 Thunk技術(shù)允許virtual table slot繼續(xù)內(nèi)含一個(gè)簡(jiǎn)單的指針,因此多重繼承不需要任何空間上的額外負(fù)擔(dān)。Slots中的地址可以直接指向Virtual function,也可以指向一個(gè)相關(guān)的thunk(如果需要調(diào)整this指針的話)。于是,對(duì)于不需要調(diào)整this指針的virtual function,也就不需要承載效率上的額外負(fù)擔(dān)。同時(shí)Thunk技術(shù)也可以為虛擬繼承中的虛函數(shù)提供同樣的解決方案。 三、VC2008 THUNK實(shí)例下面我們來(lái)看VC2008下THUNK的具體實(shí)現(xiàn): 啟動(dòng)調(diào)試,當(dāng)Derived obj執(zhí)行完畢后,我們看到obj的內(nèi)存布局如下圖所示: obj有兩個(gè)虛函數(shù)表,一個(gè)是for 'base1',我們稱(chēng)為base1vtbl;一個(gè)是for 'base2',我們稱(chēng)為base2vtbl。 Derived和subobject Base1共用一個(gè)虛函數(shù)表,而Base2使用另一個(gè)虛函數(shù)表,到目前為止這和我們想象中的一值。 但Derived類(lèi)有一個(gè)非繼承和覆蓋的虛函數(shù)Derived::DerivedOnly( ),它也應(yīng)該在base1vtbl中占有一個(gè)slot,很明顯,在obj的對(duì)象布局中圖中,我們沒(méi)有看到。由于限于語(yǔ)義上的限制,上述的調(diào)試窗口沒(méi)有顯示出DerivedOnly的slot。我們可以到Base1.__vfptr所指向的內(nèi)存區(qū)區(qū)看看,如下圖所示: 第一行的前16字節(jié)分別對(duì)應(yīng)base1vtbl的前四項(xiàng),接下來(lái)的四個(gè)字節(jié)0x0041102d對(duì)應(yīng)的就是DerivedOnly的slot。 下面看看Base2.__vfptr所指向的內(nèi)存區(qū): 數(shù)據(jù)結(jié)構(gòu)建立起來(lái)后,我們看程序的運(yùn)行。 Base1* pDerive1 = (Base1*)&obj; 0041152C lea eax,[ebp-1Ch] 0041152F mov dword ptr [ebp-28h],eax 直接將obj的地址復(fù)制給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 (跳轉(zhuǎn)到00411e5處) 004111E5 jmp Derived::ShareFunc (4117A0h) (調(diào)用Derived::ShareFunc()函數(shù)) 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的起始地址相同,所以通過(guò)pDerive1調(diào)用派生類(lèi)的虛函數(shù)時(shí),沒(méi)有必要調(diào)整this指針,所以這里沒(méi)有使用thunk技術(shù)。 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 (跳轉(zhuǎn)到00411e5處) 00411127 jmp Base1::Base1Only (411760h)(調(diào)用Base1::ShareFunc()函數(shù)) 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) 通過(guò)base1調(diào)用自己的函數(shù),當(dāng)然用不到thunk技術(shù)。 3、Base2* pDerive2 = (Base2*)&obj; Base2* pDerive2 = (Base2*)&obj; 0041155D lea eax,[ebp-1Ch] 00411560 test eax,eax (檢測(cè)&obj是否為零) 00411562 je wmain+92h (411572h) 00411564 lea ecx,[ebp-1Ch] 00411567 add ecx,4 (調(diào)整地址的值) 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 將派生類(lèi)對(duì)象的地址復(fù)制到第二個(gè)基類(lèi)的地址時(shí),不僅檢測(cè)地址是否為零,同時(shí)還將地址的值做出相應(yīng)的調(diào)整,使其指向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 (跳轉(zhuǎn)到thunk處) 0041114A jmp [thunk]:Derived::ShareFunc`adjustor{4}' (411C40h) [thunk]:Derived::ShareFunc`adjustor{4}': 00411C40 sub ecx,4 (調(diào)整this指針的值,指向derived object) 00411C43 jmp Derived::ShareFunc (4111E5h) (調(diào)用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的起始地址不一樣,而現(xiàn)在要調(diào)用Derived的函數(shù),相應(yīng)的this指針必須指向Derived object,所以必須調(diào)整this指針,以符合成員函數(shù)的this指針必須指向該成員函數(shù)所屬的對(duì)象。 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的起始地址不一樣,但現(xiàn)在要調(diào)用的是base2的函數(shù),所以沒(méi)有必要調(diào)整this指針,也就沒(méi)有必要使用thunk技術(shù)。 6、obj.Base2Only(); obj.Base2Only(); 004115B0 lea ecx,[ebp-18h] (調(diào)整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對(duì)象,但調(diào)用的卻是Base2::Base2Only函數(shù),所以有必要調(diào)整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 (調(diào)整返回值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 小結(jié) C++成員函數(shù)調(diào)用語(yǔ)意:成員函數(shù)中的this指針必須指向該成員函數(shù)所屬的對(duì)象。單繼承中虛函數(shù)的調(diào)用會(huì)始終保持這種語(yǔ)意,因?yàn)榕缮?lèi)對(duì)象的地址和基類(lèi)子對(duì)象的地址一致,無(wú)論是通過(guò)派生類(lèi)調(diào)用基類(lèi)的函數(shù)還是通過(guò)基類(lèi)調(diào)用派生類(lèi)的函數(shù),他們的this指針都只有一個(gè),那就是派生類(lèi)對(duì)象的起始地址。但是在多重繼承中,第二以及其之后的基類(lèi)子對(duì)象的起始地址和派生類(lèi)對(duì)象的啟示地址存在偏差,所以在通過(guò)基類(lèi)指針調(diào)用派生類(lèi)函數(shù)時(shí)(多態(tài)),由于其不滿足成員函數(shù)調(diào)用語(yǔ)意,所以必須調(diào)整this指針。同理,通過(guò)派生類(lèi)對(duì)象調(diào)用基類(lèi)虛函數(shù)時(shí)(繼承來(lái)的虛函數(shù)),也必須調(diào)整this指針。 |
|
來(lái)自: legionDataLib > 《vc編程》