九天雁翎的博客
如果你想在软件业获得成功,就使用你知道的最强大的语言,用它解决你知道的最难的问题,并且等待竞争对手的经理做出自甘平庸的选择。 -- Paul Graham

C++中的虚函数调用原理的反汇编实例分析(2)


C++中的虚函数调用原理的反汇编实例分析(2)

write by 九天雁翎(JTianLing) -- www.jtianling.com

因为昨天的第一节中我其实感觉并没有太透彻的理解其原理,也对VC6的实现是否会继续抱有怀疑态度,所以今天特意用VS2005编译并分析了一下,只能说,从反汇编的角度来看都可以看出微软的进步实在是很大的,有钱嘛:)呵呵,也许Stanley B.Lippman的功劳不是白费的吧。速度上我是没有好好的去测试了,从逻辑上看,可读性都强了很多(呵呵,汇编代码要可读性干什么。。。。。)

为了可读性。。。。我个人就麻烦一点了,以后汇编代码也不是全黑出镜了。。。。我还是用gvim给大家上上色吧。。。。

 

示例程序:

#include

#include

#include

 

class CTestThisPointer

{

public:

    CTestThisPointer(int ai):mi(ai) { }

    virtual int Add(int ai)

    {

        mi += ai;

        return mi;

    }

 

private:

    int mi;

};

 

 

int main()

{

    CTestThisPointer loTest(10);

    CTestThisPointer *lp = &loTest;

 

    printf("%d/n",lp->Add(5));

    printf("%s/n",typeid(lp).name());

    return 0;

}

和在第一节中的源代码是一样的,只是这次我用VS2005最大速度优化release编译

 

反汇编:

主函数:

 1 .text:00401010 ; int __cdecl main(int argc, const char **argv, const char *envp)
 2 .text:00401010 _main           proc near               ; CODE XREF: __tmainCRTStartup+10A^Yp
 3 .text:00401010
 4 .text:00401010 var_8           = dword ptr -8
 5 .text:00401010 var_4           = dword ptr -4
 6 .text:00401010 argc            = dword ptr  4
 7 .text:00401010 argv            = dword ptr  8
 8 .text:00401010 envp            = dword ptr  0Ch
 9 .text:00401010
10 .text:00401010                 sub     esp, 8          ;
此处为临时变量var_8,var_4预留空间,但是因为直接使用esp
11 .text:00401010                                         ;
所以我感觉后面的的堆栈有点混乱,比如后面的esp+10h,
12 .text:00401010                                         ;
这里有个有意思的优化在于5竟然是只有2个字节的使用,
13 .text:00401010                                         ;
所以esp-8以后,push esi(保存esi)然后又-4=0ch,然后
14 .text:00401010                                         ;
因为push 5(add参数)然后-2=010h,所以以后的临时变量
15 .text:00401010                                         ;
不得不先+10h以获取正确的地址,看到下面的汇编代码
16 .text:00401010                                         ;
应该就好理解了
17 .text:00401010                                         ;
18 .text:00401013                 push    esi
19 .text:00401014                 push    5               ;
这是Add函数的参数了,这里是先push参数,然后再正确为
20 .text:00401014                                         ; CTestThisPointer
赋值(从构造函数优化而来),再调用
21 .text:00401014                                         ; Add
函数,中间断开了,但是从栈的走向还是可以看出
22 .text:00401014                                         ;
此处实际是Add函数的参数
23 .text:00401016                 lea     ecx, [esp+10h+var_8] ;
此处也是Add函数的参数,即this指针了
24 .text:0040101A                 mov     [esp+10h+var_8], offset const CTestThisPointer::`vftable'
25 .text:00401022                 mov     [esp+10h+var_4], 0Ah ;
构造函数没有调用,直接优化为赋值操作
26 .text:0040102A                 call    ds:const CTestThisPointer::`vftable' ;
27 .text:0040102A                                         ;
一个虚函数的调用还是解释call this所在的地址就好了
28 .text:00401030                 mov     esi, ds:__imp__printf
29 .text:00401036                 push    eax
30 .text:00401037                 push    offset aD       ; "%d/n"
31 .text:0040103C                 call    esi ; __imp__printf
32 .text:0040103E                 add     esp, 8          ;
add esp,8为调整printf函数的栈平衡使用
33 .text:00401041                 push    offset __type_info_root_node ;
此处不明,可能需要多个RTTI对象才能知道
34 .text:00401041                                         ;
因为就命名上来推测,可能是维护了一个RTTI对象的链表
35 .text:00401041                                         ;
而因为元素只有一个,所以没有办法肯定了
36 .text:00401046                 mov     ecx, offset CTestThisPointer * `RTTI Type Descriptor' ;
此处相当于构建一个type_info的类,当时由于优化,没有
37 .text:00401046                                         ;
使用栈来创建局部变量和使用构造函数等,直接将type_info
38 .text:00401046                                         ;
的虚表赋值给ecx了,ecxthiscall调用约定中就是this
39 .text:00401046                                         ;
指针传递的参数啊,下面就调用了此对象的name成员函数
40 .text:0040104B                 call    ds:type_info::name(__type_info_node *)
41 .text:00401051                 push    eax
42 .text:00401052                 push    offset aS       ; "%s/n"
43 .text:00401057                 call    esi ; __imp__printf
44 .text:00401059                 add     esp, 8          ;
add esp,8也为调整printf函数的栈平衡使用
45 .text:0040105C                 xor     eax, eax
46 .text:0040105E                 pop     esi
47 .text:0040105F                 add     esp, 8          ;
此处才退出函数,CTestThisPointer结束生命周期,用
48 .text:0040105F                                         ; add esp,8
清空临时变量
49 .text:00401062                 retn                    ;
对了,才注意到main函数遵循的也是__cdecl调用约定。。。
50 .text:00401062 _main           endp                    ;
其实自然啦,因为main函数的参数个数也是不定的

 

虚表:

1 .rdata:00402114                 dd offset const CTestThisPointer::`RTTI Complete Object Locator' ;
2 .rdata:00402114                                         ;
这就是CTestThisPointerRTTI信息
3 .rdata:00402114                                         ;
正好在虚表前,由编译器静态分配,但是动态获取
4 .rdata:00402114                                         ; this
指针的指向还是vtbl的第一个虚函数,此例中
5 .rdata:00402114                                         ;
即为Add
6 .rdata:00402118 const CTestThisPointer::`vftable' dd offset CTestThisPointer__Add
7 .rdata:00402118                                         ; DATA XREF: _main+A^Xo
8 .rdata:00402118                                         ; _main+1A^Xr

 

而虚表的前4个字节就是一个指针,指向了表示正确类型的type_info类的子类。

到这里,一切就慢慢符合《inside C++ Object》中描述的虚表了,呵呵,有了lippman,微软总算走上正轨了,慢慢的。。。充满技术性天才的可怜borland也走入了末途。

 

继续,例2:

#include

#include

#include

 

class CTestThisPointer

{

public:

    CTestThisPointer(int ai):mi(ai) { }

    virtual int Add(int ai)

    {

        mi += ai;

        return mi;

    }

 

    // 纯废的析构函数

    virtual ~CTestThisPointer() { mi = 0;}

private:

    int mi;

};

 

// 无继承关系,仅为分析RTTI

class CTestBase

{

public:

    CTestBase(int ai):mi(ai) { }

    virtual int Add(int ai)

    {

        mi += ai;

        mi += ai;

        return mi;

    }

 

    // 纯废的析构函数

    ~CTestBase() { mi = 0;}

private:

    int mi;

};

 

    

int main()

{

    CTestThisPointer loTest(10);

    CTestThisPointer *lp = &loTest;

 

    printf("%d/n",lp->Add(5));

    printf("%s/n",typeid(lp).name());

 

 

 

    CTestBase loTestBase(20);

    CTestBase* lpTestBase = &loTestBase;

 

    printf("%s/n",typeid(CTestBase).name());

    printf("%s/n",typeid(loTestBase).name());

    printf("%s/n",typeid(lpTestBase).name());

    return 0;

}

 

反汇编代码:

不重复注释重复内容

主程序:

.text:00401070 ; int __cdecl main(int argc, const char **argv, const char *envp)
.text:00401070 _main           proc near               ; CODE XREF: __tmainCRTStartup+10A^Yp
.text:00401070
.text:00401070 var_1C          = dword ptr -1Ch
.text:00401070 var_18          = dword ptr -18h
.text:00401070 var_14          = dword ptr -14h
.text:00401070 var_10          = dword ptr -10h
.text:00401070 var_C           = dword ptr -0Ch
.text:00401070 var_4           = dword ptr -4
.text:00401070 argc            = dword ptr  4
.text:00401070 argv            = dword ptr  8
.text:00401070 envp            = dword ptr  0Ch
.text:00401070
.text:00401070                 push    0FFFFFFFFh
.text:00401072                 push    offset loc_401A50 ;
因为程序足够大了。。。。MS自动的加入了SEH异常处理
.text:00401077                 mov     eax, large fs:0 ; SEH
TEB(thread Environment Block)总是在FS:0
.text:0040107D                 push    eax
.text:0040107E                 sub     esp, 10h
.text:00401081                 push    esi
.text:00401082                 push    edi
.text:00401083                 mov     eax, __security_cookie
.text:00401088                 xor     eax, esp
.text:0040108A                 push    eax
.text:0040108B                 lea     eax, [esp+28h+var_C]
.text:0040108F                 mov     large fs:0, eax ;
之前的代码都是在初始化TEB结构。。。。。。。
.text:00401095                 mov     [esp+28h+var_1C], offset const CTestThisPointer::`vftable'
.text:0040109D                 mov     [esp+28h+var_18], 0Ah
.text:004010A5                 push    5
.text:004010A7                 lea     ecx, [esp+2Ch+var_1C]
.text:004010AB                 mov     [esp+2Ch+var_4], 0
.text:004010B3                 call    ds:const CTestThisPointer::`vftable'
.text:004010B9                 mov     esi, ds:__imp__printf
.text:004010BF                 push    eax
.text:004010C0                 push    offset aD       ; "%d/n"
.text:004010C5                 call    esi ; __imp__printf
.text:004010C7                 mov     edi, ds:type_info::name(__type_info_node *)
.text:004010CD                 add     esp, 8
.text:004010D0                 push    offset __type_info_root_node
.text:004010D5                 mov     ecx, offset CTestThisPointer * `RTTI Type Descriptor'
.text:004010DA                 call    edi ; type_info::name(__type_info_node *)
.text:004010DC                 push    eax
.text:004010DD                 push    offset aS       ; "%s/n"
.text:004010E2                 call    esi ; __imp__printf
.text:004010E4                 add     esp, 8
.text:004010E7                 mov     [esp+28h+var_14], offset const CTestBase::`vftable'
.text:004010EF                 mov     [esp+28h+var_10], 14h
.text:004010F7                 push    offset __type_info_root_node
.text:004010FC                 mov     ecx, offset CTestBase `RTTI Type Descriptor'
.text:00401101                 mov     byte ptr [esp+2Ch+var_4], 1
.text:00401106                 call    edi ; type_info::name(__type_info_node *)
.text:00401108                 push    eax
.text:00401109                 push    offset aS       ; "%s/n"
.text:0040110E                 call    esi ; __imp__printf
.text:00401110                 add     esp, 8
.text:00401113                 push    offset __type_info_root_node
.text:00401118                 mov     ecx, offset CTestBase `RTTI Type Descriptor'
.text:0040111D                 call    edi ; type_info::name(__type_info_node *)
.text:0040111F                 push    eax
.text:00401120                 push    offset aS       ; "%s/n"
.text:00401125                 call    esi ; __imp__printf
.text:00401127                 add     esp, 8
.text:0040112A                 push    offset __type_info_root_node
.text:0040112F                 mov     ecx, offset CTestBase * `RTTI Type Descriptor'
.text:00401134                 call    edi ; type_info::name(__type_info_node *)
.text:00401136                 push    eax
.text:00401137                 push    offset aS       ; "%s/n"
.text:0040113C                 call    esi ; __imp__printf
.text:0040113E                 add     esp, 8
.text:00401141                 xor     eax, eax
.text:00401143                 mov     ecx, [esp+28h+var_C]
.text:00401147                 mov     large fs:0, ecx
.text:0040114E                 pop     ecx
.text:0040114F                 pop     edi
.text:00401150                 pop     esi
.text:00401151                 add     esp, 1Ch
.text:00401154                 retn
.text:00401154 _main           endp
.text:00401154
.text:00401154 ; ---------------------------------------------------------------------------

贴出来仅仅是为了完整性,其实已经没有什么新意了,无非就是多一些,唯一有价值提起的就是SEH机制的加入,这又是另外一个话题了,在此不详述了,可以查阅相关书籍,推荐的有《windows核心编程》和《加密与解密》相关章节,虽然内容都并不是很多。

虚表。。。这才是符合主题的部分:

.rdata:00402124                 dd offset const CTestThisPointer::`RTTI Complete Object Locator'
.rdata:00402128 const CTestThisPointer::`vftable' dd offset CTestThisPointer__Add
.rdata:00402128                                         ; DATA XREF: CTestThisPointer___CTestThisPointer^Xo
.rdata:00402128                                         ; CTestThisPointer___scalar_deleting_destructor_+8^Xo ...
.rdata:0040212C                 dd offset CTestThisPointer___scalar_deleting_destructor_ ;
.rdata:0040212C                                         ;
哈雷卤鸭。。。。。好吃吗?-_-!果然虚表果然扩张了,
.rdata:0040212C                                         ;
顺序排列的函数分别是Add,destructor
.rdata:00402130                 dd offset const CTestBase::`RTTI Complete Object Locator'
.rdata:00402134 const CTestBase::`vftable' dd offset CTestBase__Add
.rdata:00402134                                         ; DATA XREF: CTestBase___CTestBase^Xo
.rdata:00402134                                         ; _main+77^Xo
.rdata:00402134                                         ;
.rdata:00402134                                         ;
此处也可以看出来,在虚表前的一个4字节结构,的确是
.rdata:00402134                                         ; RTTI
使用的。。。。只是this指针直接指向的是虚表

 

 

以下是__type_info_node的链表。。。。。。。。。,没有上色了。。。

.rdata:00402274 dd offset CTestBase::`RTTI Class Hierarchy Descriptor'

.rdata:00402278 const CTestThisPointer::`RTTI Complete Object Locator' db 0

.rdata:00402278 ; DATA XREF: .rdata:00402124o

.rdata:00402279 db 0

.rdata:0040227A db 0

.rdata:0040227B db 0

.rdata:0040227C db 0

.rdata:0040227D db 0

.rdata:0040227E db 0

.rdata:0040227F db 0

.rdata:00402280 db 0

.rdata:00402281 db 0

.rdata:00402282 db 0

.rdata:00402283 db 0

.rdata:00402284 dd offset CTestThisPointer `RTTI Type Descriptor'

.rdata:00402288 dd offset CTestThisPointer::`RTTI Class Hierarchy Descriptor'

.rdata:0040228C CTestThisPointer::`RTTI Class Hierarchy Descriptor' db 0

.rdata:0040228C ; DATA XREF: .rdata:00402288o

.rdata:0040228C ; .rdata:004022BCo

.rdata:0040228D db 0

.rdata:0040228E db 0

.rdata:0040228F db 0

.rdata:00402290 db 0

.rdata:00402291 db 0

.rdata:00402292 db 0

.rdata:00402293 db 0

.rdata:00402294 db 1

.rdata:00402295 db 0

.rdata:00402296 db 0

.rdata:00402297 db 0

.rdata:00402298 dd offset CTestThisPointer::`RTTI Base Class Array'

.rdata:0040229C CTestThisPointer::`RTTI Base Class Array' dd offset CTestThisPointer::`RTTI Base Class Descriptor at (0,-1,0,64)'

.rdata:0040229C ; DATA XREF: .rdata:00402298o

 

 

__type_info_node的链表。。。。可以在VS2005的头文件中找到依据:

struct __type_info_node {

void *memPtr;

__type_info_node* next;

};

 

extern __type_info_node __type_info_root_node;

呵呵,不就是这个吗?

 

最后,在VS2005的头文件中可以看到type_info类的声明和反汇编代码上看到的完全一致。。。。

 

 

class type_info {

public:

virtual ~type_info();

_CRTIMP_PURE bool __CLR_OR_THIS_CALL operator==(const type_info& rhs) const;

_CRTIMP_PURE bool __CLR_OR_THIS_CALL operator!=(const type_info& rhs) const;

_CRTIMP_PURE int __CLR_OR_THIS_CALL before(const type_info& rhs) const;

_CRTIMP_PURE const char* __CLR_OR_THIS_CALL name(__type_info_node* __ptype_info_node = &__type_info_root_node) const;

_CRTIMP_PURE const char* __CLR_OR_THIS_CALL raw_name() const;

private:

void *_m_data;

char _m_d_name[1];

__CLR_OR_THIS_CALL type_info(const type_info& rhs);

type_info& __CLR_OR_THIS_CALL operator=(const type_info& rhs);

_CRTIMP_PURE static const char *__CLRCALL_OR_CDECL _Name_base(const type_info *,__type_info_node* __ptype_info_node);

_CRTIMP_PURE static void __CLRCALL_OR_CDECL _Type_info_dtor(type_info *);

};

 

至此。。。。C++中虚函数调用的机制差不多可以知道了。。。。顺便还搞定了RTTI....加上继承不过就是换个虚表的问题了。虚表的结构又不会变。。。。。不深究了,生命是有限的。。。。。

 

write by 九天雁翎(JTianLing) -- www.jtianling.com

分类:  汇编和反汇编 
标签:  C++  虚函数 

Posted By 九天雁翎 at 九天雁翎的博客 on 2009年06月14日

前一篇: C++中的虚函数调用原理的反汇编实例分析(1) 后一篇: C++中通过指针,引用方式做返回值的汇编代码分析