上一篇只是初步的写了一下虚继承,很不清楚而且有的地方自己理解也不到位。这回详细总结一下。以下内容来自vs2008 默认设置下。类的布局可以通过-d1reportSingleClassLayout查看。让我们从最简单的类结构开始。
![]()
代码
class A { public : int a; void af(); void virtual vaf(); }; void A::vaf(){printf( " vaf\n " );} void A::af(){printf( " af\n " );} class B { public : int b; void bf(); void virtual vbf(); }; void B::vbf(){printf( " vbf\n " );}; void B::bf(){printf( " bf\n " );}; class C: public A, public B { public : int c; void cf(); void virtual vcf(); }; void C::vcf(){printf( " vcf\n " );} void C::cf(){printf( " cf\n " );}内存中这个例子是这样的。
![]()
代码
class A size( 8 ): +--- 0 | {vfptr} 4 | a +--- A::$vftable@: | & A_meta | 0 0 | & A::vaf class B size( 8 ): +--- 0 | {vfptr} 4 | b +--- B::$vftable@: | & B_meta | 0 0 | & B::vbf class C size( 20 ): +--- | +--- ( base class A) 0 | | {vfptr} 4 | | a | +--- | +--- ( base class B) 8 | | {vfptr} 12 | | b | +--- 16 | c +--- C::$vftable@A@: | & C_meta | 0 0 | & A::vaf 1 | & C::vcf C::$vftable@B@: | - 8 0 | & B::vbf
这里我们总结一下,类中有虚函数布局。
- 若是类中有虚函数,那么类中第一个元素是指向虚表的指针(这个情况只有vftable)。
- 基类数据成员
- 本身类成员
- 最左边的基类和本类公用同一个虚函数表,从而可以简化一些操作。
一个简单的例子,让我们看一下虚函数运行时的样子。
![]()
代码
C * pc = new C; pc -> af(); pc -> vaf(); pc -> vcf(); pc -> vbf(); delete pc;.text: 00401059 push offset aAf ; " af\n " ;这里调用非虚函数,之前有一个给ecx赋值语句 .text:0040105E call ds:__imp__printf .text: 00401064 mov eax, [esi] .text: 00401066 mov edx, [eax] .text: 00401068 add esp, 4 .text:0040106B mov ecx, esi ;这里ecx指向类A,这里因为A和C相同的开始地址 .text:0040106D call edx ;这里节省了一次类的转化 .text:0040106F mov eax, [esi] .text: 00401071 mov edx, [eax + 4 ] ;这里调用vcf,在虚表中我们看到了他的offset 4 .text: 00401074 mov ecx, esi .text: 00401076 call edx .text: 00401078 mov eax, [esi + 8 ] ;这里调用vbf,这里需要首先调整this指针 .text:0040107B mov edx, [eax] ;在找到相应的函数偏移量(这里为0) .text:0040107D lea ecx, [esi + 8 ] .text: 00401080 call edx
有了前面的铺垫,我们步入正题,依然是一个简单的例子。
![]()
代码
class D : virtual public A { int d; void df(); void virtual vdf(); }; void D::vdf(){printf( " vdf\n " );} void D::df(){printf( " df\n " );} class E : virtual public A { public : int e; void ef(); void virtual vef(); }; void E::vef(){printf( " vef\n " );} void E::ef(){printf( " ef\n " );} class F : public A, public B { public : int f; void ff(); void virtual vff(); }; void F::vff(){printf( " vff\n " );} void F::ff(){printf( " ff\n " );}
让我们再看一下class F在内存中的布局
![]()
代码
class F size( 36 ): +--- | +--- ( base class D) 0 | | {vfptr} 4 | | {vbptr} 8 | | d | +--- | +--- ( base class E) 12 | | {vfptr} 16 | | {vbptr} 20 | | e | +--- 24 | f +--- +--- ( virtual base A) 28 | {vfptr} 32 | a +--- F::$vftable@D@: | & F_meta | 0 0 | & D::vdf 1 | & F::vff F::$vftable@E@: | - 12 0 | & E::vef F::$vbtable@D@: 0 | - 4 1 | 24 (Fd(D + 4 )A) F::$vbtable@E@: 0 | - 4 1 | 12 (Fd(E + 4 )A) F::$vftable@A@: | - 28 0 | & A::vaf
这里又增加了一个指向虚基表的指针vbptr,我们可以看出这个指针的目的在于计算包含虚继承的类的位置(有直接虚继承和间接虚继承)。让我们总结下有虚继承下的布局。
- 将类中非虚继承的基类放置最前面。这样访问非虚继承函数不需再计算偏移量。
- 在派生类中若是没有vbtable则增加一个,除非能从原来的非虚继承类继承到了vbtable。
- 派生类数据成员
- 虚基类
可见,虚基类始终在类的尾部,那么当类生长的时候,也就是继续被继承时,则很有可能使虚基的偏移量变大。
比如在class D的虚基表中,D与A偏移量为0,而在class F中D与A偏移量变为了24,所以只能加入一个vbptr指向虚基表。
有了前面的知识,那么运行时的情况就好分析了。
![]()
代码
.text:0040104F mov dword ptr [eax + 4 ], offset ?? _8F@@7BD@@@ ; const F::`vbtable ' {for `D ' } .text: 00401056 mov dword ptr [eax + 10h], offset ?? _8F@@7BE@@@ ; const F::`vbtable ' {for `E ' } ;首先将虚基表初始化 eax = this .text:0040105D mov dword ptr [eax + 1Ch], offset ?? _7A@@6B@ ; const A::`vftable ' .text: 00401064 mov ecx, [eax + 4 ] ; * ecx = vbtableFD .text: 00401067 mov dword ptr [eax], offset ?? _7D@@6B0@@ ; const D::`vftable ' {for `D ' } .text:0040106D mov edx, [ecx + 4 ] ;获得vbtableFD表中第2项,也就是D和A虚函数表的offset .text: 00401070 mov dword ptr [edx + eax + 4 ], offset ?? _7D@@6BA@@@ ; const D::`vftable ' {for `A ' } ;根据和虚基表的offset + 虚基表中和虚函数的offset + this找到虚函数位置以下类推 .text: 00401078 mov ecx, [eax + 10h] .text:0040107B mov dword ptr [eax + 0Ch], offset ?? _7E@@6B0@@ ; const E::`vftable ' {for `E ' } .text: 00401082 mov edx, [ecx + 4 ] .text: 00401085 mov dword ptr [edx + eax + 10h], offset ?? _7E@@6BA@@@ ; const E::`vftable ' {for `A ' } .text:0040108D mov ecx, [eax + 4 ] .text: 00401090 mov dword ptr [eax], offset ?? _7F@@6BD@@@ ; const F::`vftable ' {for `D ' } .text: 00401096 mov dword ptr [eax + 0Ch], offset ?? _7F@@6BE@@@ ; const F::`vftable ' {for `E ' } .text:0040109D mov edx, [ecx + 4 ] .text:004010A0 mov dword ptr [edx + eax + 4 ], offset ?? _7F@@6BA@@@ ; const F::`vftable ' {for `A ' } .text:004010A8 mov esi, eax .text:004010AE mov eax, [esi + 4 ] ;eax =* vbtableFD .text:004010B1 mov ecx, [eax + 4 ] ;ecx = 虚基表中和虚函数的offset .text:004010B4 mov edx, [ecx + esi + 4 ] ; * edx = vftable .text:004010B8 mov eax, [edx] .text:004010BA lea ecx, [ecx + esi + 4 ] ; this = class A的开始 .text:004010BE call eax ;pf -> vaf(); .text:004010C0 mov edx, [esi] .text:004010C2 mov eax, [edx] .text:004010C4 mov ecx, esi ;classD和classF公用虚表 .text:004010C6 call eax .text:004010C8 mov edx, [esi + 0Ch] .text:004010CB mov eax, [edx] .text:004010CD lea ecx, [esi + 0Ch] ;修正this,指向class E .text:004010D0 call eax .text:004010D2 mov edx, [esi] .text:004010D4 mov eax, [edx + 4 ] .text:004010D7 mov ecx, esi .text:004010D9 call eax
再看下虚函数覆盖的问题。
![]()
代码
class G { public : int g; void gf(); void virtual vgf(); void virtual vaf(); }; void G::gf(){printf( " gf\n " );} void G::vgf(){printf( " vgf\n " );} void G::vaf(){printf( " vaf_g\n " );} class H: public A, public G { public : int h; void hf(); void vaf(); void vgf(); void virtual vhf(); }; void H::hf(){printf( " hf\n " );} void H::vaf(){printf( " vaf_H\n " );} void H::vgf(){printf( " vgf_h\n " );} void H::vhf(){printf( " vhf\n " );} class H size( 20 ): +--- | +--- ( base class A) 0 | | {vfptr} 4 | | a | +--- | +--- ( base class G) 8 | | {vfptr} 12 | | g | +--- 16 | h +--- H::$vftable@A@: | & H_meta | 0 0 | & H::vaf 1 | & H::vhf H::$vftable@G@: | - 8 0 | & H::vgf 1 | & thunk: this -= 8 ; goto H::vaf
由于A类和G类的函数vaf都被子类H覆盖,由于A和H共用虚函数表,那么如果在G类中依然保留被覆盖的函数则浪费空间。实际是通过以下代码实现的。
![]()
代码
.text:004010B0 ; [thunk]: public : virtual void __thiscall H::vaf`adjustor{ 8 } ' (void) .text:004010B0 ? vaf@H@@W7AEXXZ proc near ; DATA XREF: .rdata: 00402158 o .text:004010B0 sub ecx, 8 ;这里调整this指针,指向class G = class A .text:004010B3 jmp ? vaf@H@@UAEXXZ ; H::vaf( void );转向到G表中的vaf() .text:004010B3 ? vaf@H@@W7AEXXZ endp
可见要是要使用thunk,根本上是处理以达到节省函数表大小,通过修改this指针去调用子类表项,那么也就是当子类覆盖父类多个方法时,只保留一份,其他的则跳转执行。
![]()
代码
mov ecx, esi call edx ;调用vaf mov eax, [esi + 8 ] ; * eax = vftable_G mov edx, [eax] lea ecx, [esi + 8 ] call edx ;vgf mov eax, [esi] mov edx, [eax + 4 ] mov ecx, esi call edx ;vhf
虚函数中还有2个非常重要的部分一个纯虚函数,一个虚析构函数。由于析构函数和构造函数结合的实在是太紧密了。下一篇先总结下虚析构函数当然也包括构造函数的部分。