zoukankan      html  css  js  c++  java
  • Visual C++ 里的 Classes, Methods and RTTI

    类的基本布局

    为了说明以下内容,让我们考虑这个简单的例子:

    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按以下顺序排列类:

    1. 指向虚拟函数表(vtable或vftable)的指针,仅当类具有虚拟方法且基类中没有合适的表可重用时添加
    2. 基类
    3. 类成员

    虚拟函数表由虚拟方法的地址按其第一次出现的顺序组成。重载函数的地址替换基类中函数的地址。因此,我们三个类的布局如下所示:

    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”约定。下面是各种类方法类型的概述。

    1. Static Methods
      静态方法不需要类实例,因此它们的工作方式与公共函数相同。不,这个指针被传递给他们。因此,不可能可靠地区分静态方法和简单函数。例子:
      A::A_static1();
      call    A::A_static1
    2. 简单方法
      简单方法需要一个类实例,因此这个指针作为一个隐藏的第一个参数传递给它们,通常使用thiscall约定,即在ecx寄存器中。如果基对象不在派生类的开头,则需要调整此指针,使其在调用函数之前指向基子对象的实际开头。例子:
      ;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子对象。

    3. 虚方法
      要调用虚拟方法,编译器首先需要从vftable中获取函数地址,然后以与简单方法相同的方式调用该地址的函数(即将此指针作为隐式参数传递)。例子:
         ;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
    4. 构造函数和析构函数
      构造函数和析构函数的工作方式类似于一个简单的方法:它们得到一个隐式的this指针作为第一个参数(例如,在thiscall约定的情况下是ecx)。构造函数返回eax中的this指针,即使它在形式上没有返回值。

    RTTI实现

    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
    };
    分类描述描述描述分类的Inheritance Hierarchy。这是所有Cols for a class共享的。
    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层次结构如下所示:

    更多信息

    RTTI

    如果有,RTTI是一个有价值的信息来源,可以用来逆转。从RTTI可以恢复类名、继承层次结构,在某些情况下还可以恢复类布局的某些部分。我的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可以发现:

    • 表示C++对象的堆栈变量,或指向用“ _operator new_”分配的对象的指针。
    • 析构器
    • 构造器
    • 如果是新的物体,它们的大小

    构造函数/析构函数递归

    这个规则很简单:构造函数调用其他构造函数(基类和成员变量的构造函数),析构函数调用其他析构函数。典型的构造函数执行以下操作:
    • 调用基类的构造函数。
    • 调用复杂类成员的构造函数。
    • 如果类具有虚拟函数,则初始化vfptr
    • 执行程序员编写的构造函数体。

    典型的析构函数几乎以相反的顺序工作:

    • 如果类具有虚拟函数,则初始化vfptr
    • 执行程序员编写的析构函数体。
    • 调用复杂类成员的析构函数
    • 调用基类的析构函数

    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

    因此,通过向量构造函数/析构函数迭代器调用,我们可以确定:

    • 对象数组的地址
    • 构造器
    • 毁灭者
    • 类size

    删除析构函数

    当类具有虚拟析构函数时,编译器生成一个帮助函数-删除析构函数。其目的是确保在销毁类时调用正确的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);
          }
        };
  • 相关阅读:
    开课博客
    第二周学习进度
    django新建项目
    装饰器执行顺序问题
    Python3 图片转字符画
    python项目
    jdbc url写法(集群)
    gradle执行test任务报错
    java.lang.NoClassDefFoundError: org/junit/runner/manipulation/Filter
    django 403问题
  • 原文地址:https://www.cnblogs.com/yilang/p/11853976.html
Copyright © 2011-2022 走看看