为了说明以下内容,让我们考虑这个简单的例子:
class A { int a1; public: virtual int A_virt1(); virtual int A_virt2(); static void A_static1(); void A_simple1(); }; class B { int b1; int b2; public: virtual int B_virt1(); virtual int B_virt2(); }; class C: public A, public B { int c1; public: virtual int A_virt2(); virtual int B_virt2(); };
在大多数情况下,MSVC按以下顺序排列类:
虚拟函数表由虚拟方法的地址按其第一次出现的顺序组成。重载函数的地址替换基类中函数的地址。因此,我们三个类的布局如下所示:
class A size(8): +--- 0 | {vfptr} 4 | a1 +--- A‘s vftable: 0 | &A::A_virt1 4 | &A::A_virt2 class B size(12): +--- 0 | {vfptr} 4 | b1 8 | b2 +--- B‘s vftable: 0 | &B::B_virt1 4 | &B::B_virt2 class C size(24): +--- | +--- (base class A) 0 | | {vfptr} 4 | | a1 | +--- | +--- (base class B) 8 | | {vfptr} 12 | | b1 16 | | b2 | +--- 20 | c1 +--- C‘s vftable for A: 0 | &A::A_virt1 4 | &C::A_virt2 C‘s vftable for B: 0 | &B::B_virt1 4 | &C::B_virt2
上面的图表是由VC8编译器使用未记录的开关生成的。若要查看编译器生成的类布局,请使用-d1reportSingleClassLayout查看单个类的布局-d1reportAllClassLayout查看所有类(包括内部CRT类)的布局,这些布局将转储到stdout。如您所见,C有两个vftable,因为它继承了两个类,这两个类都已经有了虚拟函数。A的地址替换C的vftable中A::A的地址,C::B的地址替换另一个表中B::B的地址。
默认情况下,MSVC中的所有类方法都使用thiscall约定。类实例地址(this指针)在ecx寄存器中作为隐藏参数传递。在方法体中,编译器通常会立即将其放入其他寄存器(如esi或edi)和/或堆栈变量中。类成员的所有进一步寻址都是通过该寄存器和/或变量完成的。但是,在实现COM类时,使用了“stdcall”约定。下面是各种类方法类型的概述。
A::A_static1();
call A::A_static1
;pC->A_simple1(1); ;esi = pC push 1 mov ecx, esi call A::A_simple1 ;pC->B_simple1(2,3); ;esi = pC lea edi, [esi+8] ;adjust this push 3 push 2 mov ecx, edi call B::B_simple1
如您所见,在调用B的方法之前,会将this指针调整为指向B子对象。
;pC->A_virt2() ;esi = pC mov eax, [esi] ;fetch virtual table pointer mov ecx, esi call [eax+4] ;call second virtual method ;pC->B_virt1() ;edi = pC lea edi, [esi+8] ;adjust this pointer mov eax, [edi] ;fetch virtual table pointer mov ecx, edi call [eax] ;call first virtual method
RTTI(运行时类型标识)是一种特殊的编译器生成信息,用于支持C++类操作符,如dynamic_cast<> 和 typeid(),也适用于C++异常。由于RTTI的性质,它只需要(并生成)多态类,即具有虚拟函数的类。
MSVC编译器在vftable前面放置一个指向名为“Complete Object Locator”的结构的指针。之所以调用这个结构,是因为它允许编译器从一个特定的vftable指针中找到完整对象的位置(因为一个类可以有几个指针)。COL如下所示:
struct RTTICompleteObjectLocator { DWORD signature; //always zero ? DWORD offset; //offset of this vtable in the complete class DWORD cdOffset; //constructor displacement offset struct TypeDescriptor* pTypeDescriptor; //TypeDescriptor of the complete class struct RTTIClassHierarchyDescriptor* pClassDescriptor; //describes inheritance hierarchy };
struct RTTIClassHierarchyDescriptor { DWORD signature; //always zero? DWORD attributes; //bit 0 set = multiple inheritance, bit 1 set = virtual inheritance DWORD numBaseClasses; //number of classes in pBaseClassArray struct RTTIBaseClassArray* pBaseClassArray; };
基类数组描述所有基类以及允许编译器在执行“ _dynamic_cast_ operator”转换运算符期间将派生类转换为其中任何一个的信息。每个条目(基类描述符)具有以下结构:
struct RTTIBaseClassDescriptor { struct TypeDescriptor* pTypeDescriptor; //type descriptor of the class DWORD numContainedBases; //number of nested classes following in the Base Class Array struct PMD where; //pointer-to-member displacement info DWORD attributes; //flags, usually 0 }; struct PMD { int mdisp; //member displacement int pdisp; //vbtable displacement int vdisp; //displacement inside vbtable };
PMD结构描述基类是如何放置在完整类中的。在简单继承的情况下,它位于距对象开头的固定偏移处,该值是“_mdisp_ ”字段。如果它是虚拟基,则需要从vbtable获取额外的偏移量。用于调整从派生类到基类的指针的伪代码如下所示:
//char* pThis; struct PMD pmd; pThis+=pmd.mdisp; if (pmd.pdisp!=-1) { char *vbtable = pThis+pmd.pdisp; pThis += *(int*)(vbtable+pmd.vdisp); }
例如,我们三个类的RTTI层次结构如下所示:
全局和静态对象需要在主程序启动之前初始化。MSVC通过生成初始化函数并将它们的地址放在表中来实现这一点,该表在CRT启动期间由cinit函数处理。表通常位于.data节的开头。典型的初始值设定项如下所示:
_init_gA1: mov ecx, offset _gA1 call A::A() push offset _term_gA1 call _atexit pop ecx retn _term_gA1: mov ecx, offset _gA1 call A::~A() retn
因此,从这个表中我们可以发现:
另请参见MSVC _#pragma_ directive _init_seg_ [5]
如果在函数中创建了任何自动对象,则VC++编译器自动生成异常处理结构,以确保在发生异常时删除这些对象。
unwind_1tobase: ; state 1 -> -1 lea ecx, [ebp+a1] jmp A::~A()
通过在函数体中找到相反的状态更改,或者只找到对同一堆栈变量的第一次访问,我们还可以找到构造函数
lea ecx, [ebp+a1] call A::A() mov [ebp+__$EHRec$.state], 1
对于使用new()运算符构造的对象,展开函数确保在构造函数失败时删除已分配的内存:
unwind_0tobase: ; state 0 -> -1 mov eax, [ebp+pA1] push eax call operator delete(void *) pop ecx retn
在函数体中:
;A* pA1 = new A(); push call operator new(uint) add esp, 4 mov [ebp+pA1], eax test eax, eax mov [ebp+__$EHRec$.state], 0; state 0: memory allocated but object is not yet constructed jz short @@new_failed mov ecx, eax call A::A() mov esi, eax jmp short @@constructed_ok @@new_failed: xor esi, esi @@constructed_ok: mov [esp+14h+__$EHRec$.state], -1 ;state -1: either object was constructed successfully or memory allocation failed ;in both cases further memory management is done by the programmer
另一种类型的展开函数用于构造函数和析构函数。它确保在发生异常时销毁类成员。在这种情况下,函数使用保存在堆栈变量中的this指针:
unwind_2to1: mov ecx, [ebp+_this] ; state 2 -> 1 add ecx, 4Ch jmp B1::~B1
在这里,functlet在偏移量4Ch处销毁B1类型的类成员。因此,从展开functlet可以发现:
典型的析构函数几乎以相反的顺序工作:
MSVC生成的析构函数的另一个显著特点是,它们的状态变量通常以最大值初始化,然后与每个被析构函数化的子对象一起递减,这使得它们的识别更容易。请注意,简单的构造函数/析构函数通常由MSVC内联。这就是为什么您经常可以看到vftable指针在同一个函数中用不同的指针重复加载。
MSVC编译器使用helper函数来构造和销毁对象数组。请考虑以下代码:
A* pA = new A[n]; delete [] pA;
它被转换为以下伪代码:
array = new char(sizeof(A)*n+sizeof(int)) if (array) { *(int*)array=n; //store array size in the beginning ‘eh vector constructor iterator‘(array+sizeof(int),sizeof(A),count,&A::A,&A::~A); } pA = array; ‘eh vector destructor iterator‘(pA,sizeof(A),count,&A::~A);
如果有vftable,则在删除数组时将调用“vector deleting destructor‘析构函数”:
;pA->‘vector deleting destructor‘(3); mov ecx, pA push 3 ; flags: 0x2=deleting an array, 0x1=free the memory call A::‘vector deleting destructor‘
如果A的析构函数是虚拟的,则它实际上被调用:
mov ecx, pA push 3 mov eax, [ecx] ;fetch vtable pointer call [eax] ;call deleting destructor
因此,通过向量构造函数/析构函数迭代器调用,我们可以确定:
当类具有虚拟析构函数时,编译器生成一个帮助函数-删除析构函数。其目的是确保在销毁类时调用正确的delete运算符。删除析构函数的伪代码如下所示:
virtual void * A::‘scalar deleting destructor‘(uint flags) { this->~A(); if (flags&1) A::operator delete(this); };
这个函数的地址放在vftable中,而不是析构函数的地址中。这样,如果另一个类重写虚拟析构函数,将调用该类的运算符delete。尽管在实际代码中,delete运算符很少被重写,所以通常会看到对default delete()的调用。有时编译器还可以生成向量删除析构函数。它的代码如下:
virtual void * A::‘vector deleting destructor‘(uint flags) { if (flags&2) //destructing a vector { array = ((int*)this)-1; //array size is stored just before the this pointer count = array[0]; ‘eh vector destructor iterator‘(this,sizeof(A),count,A::~A); if (flags&1) A::operator delete(array); } else { this->~A(); if (flags&1) A::operator delete(this); } };
Visual C++ 里的 Classes, Methods and RTTI
原文:https://www.cnblogs.com/yilang/p/11853976.html