(译)MSVC++ 逆向(二)—— 类、方法和 RTTI

原文: http://www.openrce.org/articles/full_view/23

这是本系列第二篇(第一篇 👉: MSVC++ 逆向(1)——异常处理 ),本篇将介绍 MSVC 中实现的 C++ 底层机制,包括逆向过程中的 类结构内存布局虚函数RTTIRun-Time Type Information,运行时类型识别)。阅读本文需要有 C++ 基础知识以及汇编和逆向相关基础。

类的内存布局基础

为了方便阐述接下来的内容,先看一段简单的 C++ 代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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++ 的 中个元素在内存布局中的顺序如下:

  • 指向虚函数表指针_vtable_ 或者 _vftable_ ),仅当类中有虚函数、并且基类中没有相应的虚函数表的时候才有此指针元素;
  • 基类
  • 类成员

虚函数表中囊括了类中的各个虚函数,以虚函数声明的顺序排列。其中,重载函数地址 覆盖基类中相应函数的地址。如此一来,上面 3 个类在内存中的布局大概如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 里有两个 虚函数表,这是因为它继承了两个基类,而两个基类均有自己的虚函数成员。在类 C 的第一个虚函数表中,虚函数 C::A_virt2() 的地址覆盖了基类 A 在 C 中派生的 A_virt2() 的地址;类似地,在类 C 的第二个虚函数表中,虚函数 C::B_virt2() 的地址覆盖了基类 B 在 C 中派生的 B_virt2() 的地址。

调用约定与类方法

MSVC++ 中的类方法调用时,默认遵守 _thiscall_ 调用约定。通过类的对象调用非静态成员函数非全局函数时,类的对象自身的地址(即 *this 指针的值)会以隐含参数的形式传递给被调用的类的成员函数,通常,这个 *this 指针的值,存储在寄存器 ecx 中。在函数体的实现中,编译器通常把这个指针值塞在其他寄存器中(比如 esiedi ),或者直接存入栈中的某个变量,然后对其他所有类成员的访问,都基于这个地址进行相对寻址来实现。然而,当实现 COM 类的时候,对类成员函数的调用则遵循 _stdcall_ 的调用约定。下面详述几种不同的类成员方法调用时的底层细节:

1) 静态成员函数

调用静态成员函数不需要类的实例对象,可以直接通过类名来调用,在底层看来就跟调用普通非成员函数差不多,并不涉及 *this 指针的隐式传递。不过,也正因如此,逆向过程中不容易区分类的静态成员函数和普通的非成员函数。比如:

1
2
A::A_static1();
call A::A_static1

2) 普通成员函数

普通成员函数的调用,就需要通过类的实例对象来调用了,这种情况下 *this 指针会以隐含参数的形式作为被调函数的第一个参数传递进去,并遵循 _thiscall_ 调用约定,在底层会存储在 ecx 寄存器中。另外,如果存在类继承的情况,基类对象的地址可能与派生类的对象的地址不同,这时候如果在派生类的对象中调用基类的成员函数, *this 指针的值需要调整到基类对象的起始地址,然后才能调用基类中的普通成员函数。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
;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] ;调整 *this 指针的值
push 3
push 2
mov ecx, edi
call B::B_simple1

如上所示,在调用 B 类的成员函数之前, *this 指针的值调整为 B 类子对象的起始地址。

3) 虚函数

为了调用虚函数,编译器首先需要从虚函数表中取出相应虚函数的起始地址,然后就按照类似普通成员函数调用的方式去调用它(把 *this 指针以隐含参数的方式传递),示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
;pC->A_virt2()
;esi = pC
mov eax, [esi] ;获取虚函数表的地址
mov ecx, esi
call [eax+4] ;调用虚函数表中的第二个虚函数
;pC->B_virt1()
;edi = pC
lea edi, [esi+8] ;调整 *this 指针的值
mov eax, [edi] ;获取虚函数表的地址
mov ecx, edi
call [eax] ;调用第一个虚函数

4) 构造函数和析构函数

构造函数析构函数的调用过程,与普通成员函数类似。不同的是,即使按惯例来说构造函数并没有返回值,它仍然会把构造好的类的实例对象的起始地址隐式地返回(return 到寄存器 eax 中)。

RTTI 的实现

RTTI(Run-Time Type Identification,运行时类型识别)是编译器为了支持 C++ 中 dynamic_cast<>typeid() 两个操作符操作符以及 C++ 异常而生成的特殊编译信息。RTTI 的特性只有当类涉及多态的时候才会用到,比如类中声明了虚函数

在类的内存布局中,MSVC 编译器会把一个指向 COL(Complete Object Locator,完整对象定位符)结构体的指针放在虚函数表之前。之所以叫完整对象定位符,是因为它允许编译器根据一个特定的虚函数表指针(一个类中可能有多个虚函数表指针)定位到整个对象。COL的结构定义如下:

1
2
3
4
5
6
7
8
struct RTTICompleteObjectLocator
{
DWORD signature; // 一直为 0 ?
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)
};

RTTIClassHierarchyDescriptor 描述整个类的继承关系,它对类的所有 COL 都是通用的。

1
2
3
4
5
6
7
struct RTTIClassHierarchyDescriptor
{
DWORD signature; // 一直为 0 ?
DWORD attributes; //bit 0 set = 多重继承, bit 1 set = 虚继承
DWORD numBaseClasses; // pBaseClassArray 中的基类数量(number of classes in pBaseClassArray)
struct RTTIBaseClassArray* pBaseClassArray;
};

Base Class Array 定义了在执行 _dynamic_cast_派生类可以动态映射成的所有基类的信息,其中每一个基类描述符(Base Class Descriptor)的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct RTTIBaseClassDescriptor
{
struct TypeDescriptor* pTypeDescriptor; // 类的类型描述符(type descriptor of the class)
DWORD numContainedBases; // Base Class Array 中的基类数量(number of nested classes following in the Base Class Array)
struct PMD where; // 内部成员偏移信息(pointer-to-member displacement info)
DWORD attributes; // 标志位, 通常置 0
};
struct PMD
{
int mdisp; // 内部成员偏移(member displacement)
int pdisp; // 虚函数表的偏移(vbtable displacement)
int vdisp; // 虚函数表的内部偏移(displacement inside vbtable)
};

PMD 结构描述一个基类在其派生类中的位置信息。简单继承的时候,基类相对于其派生类偏移量是固定的,偏移量的值即 _mdisp_ 的值;如果涉及到虚继承,就需要先从虚函数表中取出一个额外的偏移量一起计算出基类的偏移,在函数调用的时候则需要重新调整 *this 指针的值。整个过程的伪码示例如下:

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

举例来说,文章开头 3 个类的继承关系中 RTTI 的信息图下图所示:

示例l类中的 RTTI 继承关系

信息提取

1) RTTI

如果存在 RTTI,那么 RTTI 能为逆向工作提供很多有价值的信息。根据 RTTI,我们可能还原类名类的继承关系,甚至有时候能还原部分类的内存布局信息。在 附录 1 中,我写了一个 RTTI 信息扫描器,可以做进一步参考。

2) 静态初始化 和 全局初始化

全局静态的对象会在 main() 函数前面初始化。在 MSVC++ 中,编译器会为全局静态函数生成相应的初始化器,并把他们的地址放在一个table)中,这个会在 _cinit() 初始化 CRT 的时候生成。在 PE 结构中,这个通常在 .data 段的起始位置。典型的初始化器结构示例如下:

1
2
3
4
5
6
7
8
9
10
11
_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

这样,从上面这个表里我们可以看出:

  • 全局/静态对象的地址;
  • 它们的构造函数
  • 它们的析构函数

更多细节可以参考 _#pragma_directive_init_seg_ [5]。

3) 栈展开处理函数(Unwind Funclets)

一个函数中生成任何动态的对象时,VC++ 编译器总会生成一个相关的异常处理结构,以便在遇到异常时进行栈展开、销毁该动态对象。VC++ 中异常处理的底层细节可以参考本系列前一篇。典型的 Unwind Funclets 结构如下:

1
2
3
unwind_1tobase: ; state 1 -> -1
lea ecx, [ebp+a1]
jmp A::~A()

通过在函数体中寻找相反的状态变化,或者在第一次访问栈中的同一个变量,我们也可以找到其构造函数

1
2
3
lea ecx, [ebp+a1]
call A::A()
mov [ebp+__$EHRec$.state], 1

对与那些用 new() 方法创建的对象,栈展开处理函数 保证即使在析构函数失效的情况下,也能删除掉分配给这些对象的内存:

1
2
3
4
5
6
unwind_0tobase: ; state 0 -> -1
mov eax, [ebp+pA1]
push eax
call operator delete(void *)
pop ecx
retn

在函数体中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
;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_ 指针:

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

上面这个例子中,栈展开处理函数 销毁了 B1 在偏移 0x4c 处的成员。总的来说,通过 栈展开处理函数,我们可以获取一下信息:

  • 栈中保存的通过 _operator_new_ 创建的 C++ 对象,或指向对象的指针;
  • 类的构造函数;
  • 类的析构函数;
  • new() 创建出来的对象的 size

4) 递归构造/析构函数

这个规则很简单:递归构造函数递归地调用其他构造函数(比如基类的构造函数、其他成员的构造函数);递归析构函数 递归地调用他们所有的析构函数。典型的构造函数具有以下功能:

  • 调用基类的构造函数;
  • 调用其他嵌套对象所属类的构造函数;
  • 如果类中声明了虚函数,则初始化虚函数表指针vfptr );
  • 执行程序员定义的构造函数函数体。

典型的析构函数则具有相对应的以下功能:

  • 如果类中声明了虚函数,则初始化虚函数表指针vfptr );
  • 执行程序员定义的析构函数函数体;
  • 调用其他嵌套对象所属类的析构函数
  • 调用基类的析构函数。

不过, MSVC 编译器创建的 析构函数 还有一个特性:_state_最大值初始化,并随着对成员对象的析构行为而递减。这样一来反而方便分析析构函数的执行。另外需要注意的是,在 MSVC 中,简单的 构造/析构函数通常是以内联形式存在的,所以经常会在同一个函数中看到虚函数表指针被不同指针重复调用。

5) 数组构造与析构

MSVC 用辅助函数来完成一个对象数组的构造与析构。用以下代码为例:

1
2
A* pA = new A[n];
delete [] pA;

用 C++ 伪码详细还原一下,大概如下所示:

1
2
3
4
5
6
7
8
9
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 包含虚函数,删除对象数组的时候会调用一个 vector deleting destructor

1
2
3
4
;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析构函数是个虚函数,那么析构的时候会以调用虚函数的方式调用析构函数:

1
2
3
4
mov ecx, pA
push 3
mov eax, [ecx] ;fetch vtable pointer
call [eax] ;call deleting destructor

因此,通常来说通过构造/析构的数组迭代调用,我们可以发掘以下信息:

  • 对象数组的地址;
  • 数组里各对象的构造函数;
  • 数组里各对象的析构函数;
  • 类的 size

6) 删除析构函数( deleting destructor

当类中含有虚析构函数virtual destructor )时,编译器会生成一个辅助函数——删除析构函数,这样便能确保销毁一个类实例的时候合适的 _operator delete_ 被调用。删除析构函数 的伪码如下:

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

该函数的地址会被放在虚函数表( vftable) 中,并覆盖原有的析构函数地址。这样一来,如果另外一个类覆盖了这个虚析构函数,那么它的 _delete_ 将被调用。然而实际代码中 _delete_ 几乎不会被覆盖,所以你通常只看到调用默认的delete()。有时候,编译器也生成一个删除析构函数向量,伪码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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],它非常相近(请忽略那看着脑仁疼的匈牙利命名法)。文章[2]描述了一个MSVC实现虚继承的实现。更多细节还可以看 MS 专利[3]。

附录 1: ms_rtti4.idc

这是我为解析 RTTI虚函数表写的一个 IDA 脚本,读者可以从 Microsoft VC++ Reversing Helpers 下载到该脚本以及本系列两篇文章。该脚本的功能特性有以下几个:

  • 解析 RTTI 结构、用相应的类名重命名虚函数表
  • 在相对简单的分析工作中重命名构造函数析构函数
  • 把所有的虚函数表以及引用函数和类的继承关系输出到文件中。

Usage
IDA 的初始化分析结束之后,载入 ms_rtti4.idc ,它会询问你是否要扫描 PE 文件中的虚函数表(vftables)。需要注意的是,这个过程可能需要比较长的时间。如果你选择跳过扫描,后续仍然可以手动解析虚函数表。如果你选择让脚本帮你执行扫描,脚本会识别 PE 文件中所有使用 RTTI虚函数表,并且会重命名虚函数表、识别和重命名构造/析构函数。也有可能脚本会解析失败,尤其是涉及到虚继承的情况。扫描结束后,脚本会自动打开存放扫描结果的文件。

另外,脚本载入以后,可以使用以下 快捷键 来对 MSVC 生成的结构进行手动解析:

  • Alt+F8:解析一个虚函数表。游标应该会停在虚函数表的起始位置。如果里面用到了 RTTI,脚本会使用里面的类名来重命名虚函数表。如果没有涉及到 RTTI,你可以手动输入类名来自定义。如果脚本扫描到了虚析构函数**,一样也会把它重命名。
  • Alt+F7:解析 FuncInfo 结构。FuncInfo 是一个描述在栈上创建对象或使用异常处理句柄的函数信息的结构。它的地址在异常处理句柄中通常被解析为 _CxxFrameHandler

    1
    2
    mov eax, offset FuncInfo1
    jmp _CxxFrameHandler

    多数情况下它会被 IDA 直接识别并解析,但是我提供的脚本可以解析出更多的信息,你可以用 ms_ehseh.idc 解析文件中的所有 FuncInfo
    游标放到 FuncInfo 起始位置的,此快捷键有效。

  • Alt+F9:解析 throw 信息。Throw info_CxxThrowException 在实现 _throw 操作符时用到的辅助结构,它通常作为 _CxxThrowException 的第二个参数被调用:

    1
    2
    3
    4
    5
    6
    lea ecx, [ebp+e]
    call E::E()
    push offset ThrowInfo_E
    lea eax, [ebp+e]
    push eax
    call _CxxThrowException

    游标放在 throw info 起始位置的时候次快捷键才有效。该脚本会解析 throw info 并为调用 throw 操作符的类添加注释。它还可以识别和重命名异常的析构函数和拷贝构造函数。

附录 2:实战恢复一个类的结构

我们练手的对象是 MSN Messenger 7.5msnmsgr.exe 版本是 7.5.324.0 , 大小 7094272 Bytes ),它主要由 C++ 实现,并且里面用到了很多 RTTI 的结构,正符合我们的需求。先看一下位于 .0040EFD8.0040EFE0 的两处虚函数表。其中完整的 RTTI 结构及其继承关系如下所示:
RTTI hierarchy for MSN Messenger 7.5

这样一来,就有了两个虚函数表属于同一个 —— CContentMenuItem ,再看它们的基类描述符我们可以发现:

  • CContentMenuItem 里面包含 3 个基类 —— CDownloader/CNativeEventSink/CNativeEventSource;
  • CDownloader 包含 1 个基类 —— CNativeEventSink
  • 因此 CContentMenuItem 继承自 CDownloaderCNativeEventSource,而 CDownloader 继承自 CNativeEventSink
  • CDownloader 位于整个对象的起始位置,CNativeEventSource 则位于偏移为 0x24 的位置。

据此,我们可以得出这么一个结论:第一个虚函数表列出了 CNativeEventSource 里的方法,第二个虚函数表列出了 CDownloader 或者 CNativeEventSink 里的方法(如果这两者都不是,CContentMenuItem 会重用 CNativeEventSource虚函数表)。我们再来看都有谁引用了这两个虚函数表,它们被位于 .052B5E0.052B547 的两个函数引用(这样进一步印证了它们属于同一个)。如果我们仔细查看 .052B547 处函数的开头,可以发现 _state_ 被初始化为 6,这表明该函数是一个 析构函数;由于一个类只能有 1 个析构函数,我们可以推断 .052B5E0 处的函数是一个构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
CContentMenuItem::CContentMenuItem proc near
this = esi
push this
push edi
mov this, ecx
call sub_4CA77A
lea edi, [this+24h]
mov ecx, edi
call sub_4CBFDB
or dword ptr [this+48h], 0FFFFFFFFh
lea ecx, [this+4Ch]
mov dword ptr [this], offset const CContentMenuItem::'vftable'{for 'CContentMenuItem'}
mov dword ptr [edi], offset const CContentMenuItem::'vftable'{for 'CNativeEventSource'}
call sub_4D8000
lea ecx, [this+50h]
call sub_4D8000
lea ecx, [this+54h]
call sub_4D8000
lea ecx, [this+58h]
call sub_4D8000
lea ecx, [this+5Ch]
call sub_4D8000
xor eax, eax
mov [this+64h], eax
mov [this+68h], eax
mov [this+6Ch], eax
pop edi
mov dword ptr [this+60h], offset const CEventSinkList::'vftable'
mov eax, this
pop this
retn
sub_52B5E0 endp

编译器在预言(prolog) 之后要做的第一件事就是把 _this_ 指针的值从 ecx 拷贝到 esi,继而后续所有的寻址都是相对于 esi 作为基址。在初始化虚函数表指针(vfptrs) 之前调用了两个其他函数,这一定是基类的构造函数——本例中即 CDownloaderCNativeEventSource 的构造函数。进一步深入函数跟踪分析可以帮助我们确认这一点:第一个虚函数表指针vfptf )用 CDownloader::'vftable' 来初始化, 第二个虚函数表指针CNativeEventSource::'vftable' 来初始化。我们也可以进一步检查 CDownloader构造函数 —— 它调用了其基类 CNativeEventSink 的构造函数。

类似的,_this_ 指针的值通过 edi 传入,这时它被重置为 _this_ + 24h ,根据我们上面的类结构图来看,这是 CNativeEventSource 子对象的位置。这是另一个证明被调用的第二个函数是 CNativeEventSource 的构造函数的证据。

结束了基类的构造函数调用过程之后,基类中的虚函数指针CContentMenuItem 中自己的实现所覆盖,即 CContentMenuItem 实现了基类中的部分虚函数(或者增加了自己的虚函数)。有必要的话,我们可以对比这些表、检查那些指针被修改过或被添加了——新添加的指针就是 CContentMenuItem 中新实现的虚函数。

接下来我们就看到几个对地址 .04D8000 的调用,调用之前 ecx 的值被设置为 this+4Chthis+5Ch —— 这很明显是在初始化成员对象。一个问题是,我们如何分辨初始化函数是编译器自动生成的构造函数,还是程序员编写的自定义构造函数呢?这里有两个关键点可以参考:

  • 函数使用 _thiscall_调用约定,而且是第一次访问这些字段;
  • 字段的初始化顺序是按照地址增长的方向进行的。

为了确定这些点,我们可以查看析构函数中的栈展开处理函数(Unwind Funclets),在那里我们可以看到编译器为这些成员变量生成的构造函数。

这个新的类并没有虚函数,因此也没有 RTTI,所以我们也不知道它的名字,不妨先命名为 RefCountedPtr。根据前面的分析,位于.4D8000的函数是**构造函数**,那么在CContentMenuItem我们可以看到析构函数中的**栈展开处理函数**——在.63CCB4` 处。

回过头去看 CContentMenuItem构造函数,可以看到 3 个字段初始化为 0,另外一个初始化为一个 虚函数表指针vftable pointer )。这个看起来想一个成员变量的内联构造函数(不是基类,因为基类会出现在继承关系树中)。从一个使用了的虚函数表的 RTTI 中我们可以看出这是一个 CEventSinkList 模板的实例。

根据上面的分析,我们可以大概勾勒出类的结构声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
class CContentMenuItem: public CDownloader, public CNativeEventSource
{
/* 00 CDownloader */
/* 24 CNativeEventSource */
/* 48 */ DWORD m_unknown48;
/* 4C */ RefCountedPtr m_ptr4C;
/* 50 */ RefCountedPtr m_ptr50;
/* 54 */ RefCountedPtr m_ptr54;
/* 58 */ RefCountedPtr m_ptr58;
/* 5C */ RefCountedPtr m_ptr5C;
/* 60 */ CEventSinkList m_EventSinkList;
/* size = 70? */
};

我们不确定偏移为 0x48 处的变量是否为 CNativeEventSource 的一部分,但由于它并没有被 CNativeEventSource 的构造函数访问到,那么它很可能属于 CContentMenuItem。包含被重命名函数的构造函数与类的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public: __thiscall CContentMenuItem::CContentMenuItem(void) proc near
push this
push edi
mov this, ecx
call CDownloader::CDownloader(void)
lea edi, [this+CContentMenuItem._CNativeEventSource]
mov ecx, edi
call CNativeEventSource::CNativeEventSource(void)
or [this+CContentMenuItem.m_unknown48], -1
lea ecx, [this+CContentMenuItem.m_ptr4C]
mov [this+CContentMenuItem._CDownloader._vfptr], offset const CContentMenuItem::'vftable'{for 'CContentMenuItem'}
mov [edi+CNativeEventSource._vfptr], offset const CContentMenuItem::'vftable'{for 'CNativeEventSource'}
call RefCountedPtr::RefCountedPtr(void)
lea ecx, [this+CContentMenuItem.m_ptr50]
call RefCountedPtr::RefCountedPtr(void)
lea ecx, [this+CContentMenuItem.m_ptr54]
call RefCountedPtr::RefCountedPtr(void)
lea ecx, [this+CContentMenuItem.m_ptr58]
call RefCountedPtr::RefCountedPtr(void)
lea ecx, [this+CContentMenuItem.m_ptr5C]
call RefCountedPtr::RefCountedPtr(void)
xor eax, eax
mov [this+CContentMenuItem.m_EventSinkList.field_4], eax
mov [this+CContentMenuItem.m_EventSinkList.field_8], eax
mov [this+CContentMenuItem.m_EventSinkList.field_C], eax
pop edi
mov [this+CContentMenuItem.m_EventSinkList._vfptr], offset const CEventSinkList::'vftable'
mov eax, this
pop this
retn
public: __thiscall CContentMenuItem::CContentMenuItem(void) endp

参考资料:

  1. http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/dnarvc/html/jangrayhood.asp
  2. http://www.lrdev.com/lr/c/virtual.html
  3. 微软关于 C++ 各部分实现的专利材料:

  4. http://members.ozemail.com.au/~geoffch@ozemail.com.au/samples/programming/msvc/language/predefined/index.html

  5. http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclang/html/_predir_init_seg.asp