RTTI实现

       RTTIRun-Time Type Identification)运行时类型识别是由编译器生成的特殊信息,用于支持像dynamic_cast<>typeid()这样的C++运算符,以及C++异常。基于这个本质,RTTI只为多态类生成,例如带虚函数的类。

       MSVC编译器在vftable前设置了一个指针,指向叫做“Complete Object
Locator
”(完整对象定位器)的结构。这样称呼是因为它允许编译器从特定的vftable指针(因为一个类可能有若干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
};

 

 

       类层次描述符描述了类的继承层次。对于一个类,所有COL共享一个。

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_过程中编译器是否允许强制转换派生类到这些基类的信息。基类描述符中每一项都包含如下结构:

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中取得一个额外的偏移量加上。从派生类到基类调整_this_指针的伪码如下:

 

    

//char* pThis; struct PMD pmd;
    pThis+=pmd.mdisp;
    if (pmd.pdisp!=-1)
    {
      char *vbtable = pThis+pmd.pdisp;
      pThis += *(int*)(vbtable+pmd.vdisp);
}

 

       例如,我们的三个类的RTTI层次关系是:


RTTI hierarchy for our example classes

提取信息

1)      RTTI

如果存在,RTTI对于逆向工作来说是无价的信息。从RTTI,有可能恢复类的名字,继承层次,有时候也能恢复部分的类布局。我的RTTI扫描器脚本可以显示大多数此类信息。(参考附录I

2)      静态和全局初始化例程(initializer

全局和静态对象需要在main主程序开始前初始化。MSVC通过生成初始化例程函数(funclet)来实现,并把这些函数地址放入一个表中,当_cinit初始化CRT时,会调用它们。这个表通常位于.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]

3)      Unwind Funclets

若在函数中创建了自动类型的对象,VC++编译器会自动生成异常处理代码以确保在异常发生时会删除这些对象。请参看Part I以了解对C++异常实现的细节。一个典型的unwind funclet在栈上销毁一个对象的过程是:

 

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
funclet
确保了万一构造失败也能删除分配的内存:

 

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

    

 

另一种类型的unwind funclets用于构造函数和析构函数中。它确保了万一发生异常时删除类成员。这时候,funclets要使用保存在一个栈变量的_this_指针,

 

unwind_2to1:
        mov     ecx, [ebp+_this] ; state 2 -> 1
        add     ecx, 4Ch
        jmp     B1::~B1

    

 

这是funclet析构类型B1位于偏移4Ch处一个类成员的代码。从这里我们可以找到:

·        
栈变量代表了C++对象或者指向用new分配的对象的指针

·        
它们的构造函数

·        
它们的析构函数

·        
new创建的对象的大小

4)      构造/析构函数的递归调用

规则很简单:构造函数调用其他的构造函数(其他基类和成员变量的构造函数),析构函数调用其它的析构函数。一个典型的构造函数按下列顺序执行:

·        
调用基类构造函数

·        
调用复杂的类成员的构造函数

·        
若类有虚函数,初始化vfptr

·        
执行当前的构造函数代码(即由程序员写得构造代码)

 

典型的析构函数几乎按照反序执行:

·        
若有虚函数,初始化vfptr

·        
执行当前的析构函数代码

·        
调用复杂类成员的析构函数

·        
调用基类的析构函数

 

MSVC生成的析构函数另一个独特的特征是它们的_state_变量通常初始化为最大值,每次析构一个子对象就减一,这样使得识别它们更容易。要注意简单的构造/析构函数经常被MSVC内联(inline)。那就是为什么你经常看到vftable指针在同一个函数中被不同指针重复的调用。

5)      数组的构造和析构

MSVC使用一个辅助函数来构造和析构数组。思考下面的代码:

 

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);

 

如果A有一个vftable,当删除数组时,相应的会以调用一个删除析构函数的向量来替代:

 

;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

    

 

因此,从向量构造/析构函数叠代子调用我们可以知道:

·        
对象数组的地址

·        
它们的构造函数

·        
它们的析构函数

·        
类的大小

6)      删除析构函数

当类有虚析构函数时,编译器生成一个辅助函数来删除它。其目的是当析构一个类时确保_delete_操作符被调用。删除析构函数的伪码如下:

virtual void * A::'scalar deleting destructor'(uint flags)
    {
      this->~A();
      if (flags&1) A::operator delete(this);
};

 

这个函数的地址被放入vftable替换析构函数地址。通过这种方式,如果另外一个类覆盖了这个虚析构函数,那么它的_delete_将被调用。然而实际代码中_delete_几乎不会被覆盖,所以你通常只看到调用默认的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);
      }
};

 

我跳过了有虚基类的类的大部分实现细节,因为它们使得事情更复杂了,而且在现实生活中很少用到。请参考Jan Gray写的文章[1]。它已经很详尽了,只是用匈牙利命名法有点头痛。

打赏