C++多态汇编分析

一:说明
        C++ 中由虚函数所引起的多态总让人有一种神秘感,本文通过对汇编代码的分析使整个调用过程一目了然,但前提是读者对C++想本身有所了解,且对虚函数的调用应该有一个清晰的概念,另外读者还应有一定的汇编基础。
        文章结构:
              一:说明
              二:C++ 源码
              三:分析
                  3.1:从内存中得到分析所用的一些有用的值
                  3.2:各类的成员函数(大家只需将函数名及其首地址对应起来即可)
                  3.3 函数调用分析
        建议大家先搞清楚源码,最好在自己的机器上也调试一下,然后根据程序的执行流程再去看分析。
 
      分析环境及工具:
           VC++.NET 7.1 Realease 版本;禁止优化
二:C++ 源码
 
#include "stdio.h"
#include <string.h>
 
class CBase1
{
public :
    CBase1(){};
    ~CBase1(){};
    virtual void virfun1();
    virtual void virfun2();
    virtual void base1();
    int num;
    int b1num;
};
 
class CBase2
{
public :
    int num;
    CBase2(){};
    ~CBase2(){};
    virtual void virfun1();
    virtual void virfun2();
    virtual void base2();
};
 
class CSub1 : public CBase1
{
public :
    CSub1(){};
    ~CSub1(){};
    virtual void virfun1( );
};
 
class CSub2 : public CBase1, public CBase2
{
public :
    CSub2(){};
    ~CSub2(){};
    virtual void virfun1( );
    virtual void virfun3( );
    int s2num;
};
 
class CSub3 : public CSub2
{
public :
    CSub3(){};
    ~CSub3(){};
    virtual void base2();
    virtual void virfun2( );
    virtual void virfun4( );
};
 
//------------------------------------------------------------------------------
void CBase1::virfun1()
{
    printf("run in CBase1::virfun1( ) /n");
}
 
void CBase1::virfun2()
{
    printf("run in CBase1::virfun2( ) /n");
}
 
void CBase1::base1()
{
    printf("run in CBase1::base1( ) /n");
}
 
//------------------------------------------------------------------------------
void CBase2::virfun1()
{
    printf("run in CBase2::virfun1( ) /n");
}
void CBase2::virfun2()
{
    printf("run in CBase2::virfun2( ) /n");
}
 
void CBase2::base2()
{
    printf("run in CBase2::base2( ) /n");
}
//------------------------------------------------------------------------------
void CSub1::virfun1()
{
    printf("run in CSub1::virfun1( ) /n");
}
 
void CSub2::virfun1()
{
    printf("run in CSub2::virfun1( ) /n");
}
void CSub2::virfun3()
{
    printf("run in CSub2::virfun3( ) /n");
}
 
void CSub3::virfun2()
{
    printf("run in CSub3::virfun2( ) /n");
}
void CSub3::virfun4()
{
    printf("run in CSub3::virfun4( ) /n");
}
void CSub3::base2()
{
    printf("run in CSub3::base2( ) /n");
}
 
 
CBase1 base1;
CBase2 base2;
CSub1 sub1;
CSub2 sub2;
CSub3 sub3;
 
 
CBase1 *pbase1 = 0;
CBase2 *pbase2 = 0;
CSub1 *psub1 = 0;
CSub2 *psub2 = 0;
CSub3 *psub3 = 0;
 
 
void test1()
{
    base1.virfun1();
 
    base1.num = 1;
 
    pbase1->virfun1();
 
    pbase1->num = 1;
}
 
void test2()
{
    CBase1 &tembase1 = sub1;
 
    CBase1 *ptembase1 = &sub1;
 
    sub1.virfun1();
 
    psub1->virfun1();
 
    psub1->virfun2();
 
    tembase1.virfun1();
 
    ptembase1->virfun1();
 
    sub1.virfun2();
 
    psub1->virfun2();
 
    tembase1.virfun2();
 
    ptembase1->virfun2();
 
   
    sub1.num = 1;
 
    psub1->num = 2;
   
}
 
void test3()
{
    CBase1 &tembase1 = sub2; // <<===>> CBase1 *ptembase1 = &sub2;
 
    CBase2 &tembase2 = sub2; // <<===>> CBase2 *ptembase2 = &sub2;
 
    //sub2.virfun3();
    //sub2.virfun2();    // <<===>> psub2->virfun2();
    //error C2385: 对 virfun2 (在 CSub2 中)的访问不明确
 
    psub2->virfun1();
 
    psub2->s2num = 2;
 
    tembase1.num = 3;
 
    tembase2.num = 4;
 
    psub2->virfun3();
 
    psub2->base1();
 
    psub2->base2();
 
    tembase1.virfun1();      // <<===>> ptembase1->virfun1();
 
    tembase2.virfun1();      // <<===>> ptembase2->virfun1();
   
    tembase1.virfun2();      // <<===>> ptembase1->virfun2();
 
    tembase2.virfun2();      // <<===>> ptembase2->virfun2();
}
 
void test4()
{
    CBase1 &tembase1 = sub3; // <<===>> CBase1 *ptembase1 = &sub2;
 
    CBase2 &tembase2 = sub3; // <<===>> CBase2 *ptembase2 = &sub2;
 
    CSub2 &temsub2 = sub3;
 
    psub3->virfun4();
 
    psub3->virfun2();
 
    tembase1.virfun2();
 
    tembase2.virfun2();
 
    tembase2.base2();
 
    temsub2.base2();
}
 
int main( void )
{
 
    pbase1 = &base1;
    pbase2 = &base2;
    psub1 = &sub1;
    psub2 = &sub2;
    psub3 = &sub3;
    printf("base1:%lX    base2:%lX /nsub1:%lX       sub2:%lX      sub3:%lX/n",
       (unsigned int)pbase1, (unsigned int)pbase2, (unsigned int)psub1, (int)psub2, (int)psub3);
    printf("sizeof(...) in byte/nbase1:%lu    base2:%lu /nsub1:%lu       sub2:%lu       sub3:%lu/n",
       sizeof(base1), sizeof(base2), sizeof(sub1), sizeof(sub2), sizeof(sub3) );
   
    test1();
    test2();
    test3();
    test4();
}
 
 
三:分析
 
3.1:从内存中得到分析所用的一些有用的值:
 
五个全局对象
    CBase1    base1;
    CBase2    base2;
    CSub1     sub1;
    CSub2     sub2;
    CSub3     sub3;
在内存中的初始分布如下:
 
0x0040A6F0 xxxxxxxx 00408314 sub2:40A6F4 size:24
0x0040A6F8 00000000 00000000 
0x0040A700 00408308 00000000 
0x0040A708 00000000 004082f0 base2:40A70C size:8
0x0040A710 00000000 004082e4 base1:40A714 size:12
0x0040A718 00000000 00000000  
0x0040A720 004082fc 00000000 sub1:40A720 size:12
0x0040A728 00000000 00408330 sub3:40A72C size:24
0x0040A730 00000000 00000000 
0x0040A738 00408324 00000000 
0x0040A740 00000000
如果派生类的第一个基类没有虚函数,而第二个基类有虚函数,编译器会把有虚函数的基类排在前面,没有虚函数的基类拍在后面。
 
五个类的虚表在内存中的存储情况如下:
 
CBase1:
0x004082E4 00401000 --> CBase1::virfun1()
0x004082E8 00401020 --> CBase1::virfun2()
0x004082EC 00401040 --> CBase1::base1()
 
CBase2:
0x004082F0 00401060 --> CBase2::virfun1()
0x004082F4 00401080 --> CBase2::virfun2()
0x004082F8 004010a0 --> CBase2::base2()
 
CSub1:
0x004082FC 004010c0 --> CSub1::virfun1()
0x00408300 00401020 --> CBase1::virfun2()
0x00408304 00401040 --> CBase1::base1()
 
CSub2 偏移为 0CH 处虚表指针所指向的内容:
0x00408308 00401540 --> CSub2::virfun1`adjustor{12}
0x0040830C 00401080 --> CBase2::virfun2()
0x00408310 004010a0 --> CBase2::base2()
 
CSub2 偏移为 0 处虚表指针所指向的内容:
0x00408314 004010e0 --> CSub2::virfun1()
0x00408318 00401020 --> CBase1::virfun2()
0x0040831C 00401040 --> CBase1::base1()
0x00408320 00401100 --> CSub2::virfun3()
 
CSub3 偏移为 0CH 处虚表指针所指向的内容:
0x00408324 00401540 --> CSub2::virfun1`adjustor{12}  
0x00408328 00401580 --> CSub3::virfun2`adjustor{12}
0x0040832C 00401160 --> CSub3::base2()
 
CSub3 偏移为 0 处虚表指针所指向的内容:
0x00408330 004010e0 --> CSub2::virfun1()
0x00408334 00401120 --> CSub3::virfun2()
0x00408338 00401040 --> CBase1::base1()
0x0040833C 00401100 --> CSub2::virfun3()
0x00408340 00401140 --> CBase3::virfun4()//
0x00408344  00000000 
0x00408348  ffffffff
 
笔记:
两个基类继承自同一基类,然后就有相同的虚函数,之后派生类再继承那两个基类。如果派生类没有重新实现那个虚函数,则不会adjustor,两个基类的虚函数在派生类虚函数表中的地址指向各自基类对这个虚函数的实现。如果派生类重新实现了那个虚函数,才会在虚函数表第2个基类部分adjustor。即只有两个虚函数如果是同名(不一定要继承自同一祖基类),而且实际调用时也是调用同一个类对其实现的代码(不一定是当前派生类,父派生类也可,如这里CSub3中virfun1就是CSub2的实现),才会adjustor。
所有同一类的对象拥有相同的虚函数表,即相对于类来说,虚函数表是const static的,一个类只有一张虚函数表。这里可以看到一个程序的所有类的虚函数表都是依照次序排在一起的。
一个派生类A继承自两个基类BC,B实际内容在A的对象中排在C的前面。然而B的虚函数表在A的虚函数表中的位置在C的下面。A自己有不继承自BC的虚函数的话(如这里CSub2中的virfun3就不是继承自 CBase1 和CBase2的)他不会有自己的vfptr成员变量。然而这个虚函数也必须在虚函数表中占有一席之地,编译器把它排在第一个基类B的虚函数表的后面,也就是虚函数表的最后,也就是A自己的虚函数与B共用B的虚函数表。由于A自己没有vfptr,由于vfptr都放在对象内存空间的第一个位置,所以A的this指针指向的既是A的地址,也是B的地址,也是B的vfptr的地址。这里CSub2中的virfun3的thunk代码,会把this代入exa,以B的vfptr作为A自己的vfptr,再调用exa指向的地址加12(第一个基类B的虚函数在虚函数表中占了12个字节)。
两个adjustor:
[thunk]:CSub2::virfun1`adjustor{12}':
00401540 83 E9 0C         sub         ecx,0Ch
00401543 E9 98 FB FF FF   jmp         CSub2::virfun1 (4010E0h)
 
[thunk]:CSub3::virfun2`adjustor{12}':
00401580 83 E9 0C         sub         ecx,0Ch
00401583 E9 98 FB FF FF   jmp         CSub3::virfun2 (401120h)
 
3.2:各类的成员函数(大家只需将函数名及其首地址对应起来即可)
 
    55: //------------------------------------------------------------------------------
    56: void CBase1::virfun1()
    57: {
00401000 55               push        ebp 
00401001 8B EC            mov         ebp,esp
00401003 51               push        ecx 
00401004 89 4D FC         mov         dword ptr [ebp-4],ecx
    58:    printf("run in CBase1::virfun1( ) /n");
00401007 68 10 81 40 00   push        offset KERNEL32_NULL_THUNK_DATA+30h (408110h)
0040100C E8 65 06 00 00   call        printf (401676h)
00401011 83 C4 04         add         esp,4
    59: }
00401014 8B E5            mov         esp,ebp
00401016 5D               pop         ebp 
00401017 C3               ret             
 
    60:
    61: void CBase1::virfun2()
    62: {
00401020 55               push        ebp 
00401021 8B EC            mov         ebp,esp
00401023 51               push        ecx 
00401024 89 4D FC         mov         dword ptr [ebp-4],ecx
    63:    printf("run in CBase1::virfun2( ) /n");
00401027 68 2C 81 40 00   push        offset KERNEL32_NULL_THUNK_DATA+4Ch (40812Ch)
0040102C E8 45 06 00 00   call        printf (401676h)
00401031 83 C4 04         add         esp,4
    64: }
00401034 8B E5            mov         esp,ebp
00401036 5D               pop         ebp 
00401037 C3               ret             
 
    65:
    66: void CBase1::base1()
    67: {
00401040 55               push        ebp 
00401041 8B EC            mov         ebp,esp
00401043 51               push        ecx 
00401044 89 4D FC         mov         dword ptr [ebp-4],ecx
    68:    printf("run in CBase1::base1( ) /n");
00401047 68 48 81 40 00   push        offset KERNEL32_NULL_THUNK_DATA+68h (408148h)
0040104C E8 25 06 00 00   call        printf (401676h)
00401051 83 C4 04         add         esp,4
    69: }
00401054 8B E5            mov         esp,ebp
00401056 5D               pop         ebp 
00401057 C3               ret             
 
    70:
    71: //------------------------------------------------------------------------------
    72: void CBase2::virfun1()
    73: {
00401060 55               push        ebp 
00401061 8B EC            mov         ebp,esp
00401063 51               push        ecx 
00401064 89 4D FC         mov         dword ptr [ebp-4],ecx
    74:    printf("run in CBase2::virfun1( ) /n");
00401067 68 64 81 40 00   push        offset KERNEL32_NULL_THUNK_DATA+84h (408164h)
0040106C E8 05 06 00 00   call        printf (401676h)
00401071 83 C4 04         add         esp,4
    75: }
00401074 8B E5            mov         esp,ebp
00401076 5D               pop         ebp 
00401077 C3               ret             
 
    76: void CBase2::virfun2()
    77: {
00401080 55               push        ebp 
00401081 8B EC            mov         ebp,esp
00401083 51               push        ecx 
00401084 89 4D FC         mov         dword ptr [ebp-4],ecx
    78:    printf("run in CBase2::virfun2( ) /n");
00401087 68 80 81 40 00   push        offset KERNEL32_NULL_THUNK_DATA+0A0h (408180h)
0040108C E8 E5 05 00 00   call        printf (401676h)
00401091 83 C4 04         add         esp,4
    79: }
00401094 8B E5            mov         esp,ebp
00401096 5D               pop         ebp 
00401097 C3               ret             
 
    80:
    81: void CBase2::base2()
    82: {
004010A0 55               push        ebp 
004010A1 8B EC            mov         ebp,esp
004010A3 51               push        ecx 
004010A4 89 4D FC         mov         dword ptr [ebp-4],ecx
    83:    printf("run in CBase2::base2( ) /n");
004010A7 68 9C 81 40 00   push        offset KERNEL32_NULL_THUNK_DATA+0BCh (40819Ch)
004010AC E8 C5 05 00 00   call        printf (401676h)
004010B1 83 C4 04         add         esp,4
    84: }
004010B4 8B E5            mov         esp,ebp
004010B6 5D               pop         ebp 
004010B7 C3               ret             
 
    85: //------------------------------------------------------------------------------
    86: void CSub1::virfun1()
    87: {
004010C0 55               push        ebp 
004010C1 8B EC            mov         ebp,esp
004010C3 51               push        ecx 
004010C4 89 4D FC         mov         dword ptr [ebp-4],ecx
    88:    printf("run in CSub1::virfun1( ) /n");
004010C7 68 B8 81 40 00   push        offset KERNEL32_NULL_THUNK_DATA+0D8h (4081B8h)
004010CC E8 A5 05 00 00   call        printf (401676h)
004010D1 83 C4 04         add         esp,4
    89: }
004010D4 8B E5            mov         esp,ebp
004010D6 5D               pop         ebp 
004010D7 C3               ret             
 
    90:
    91: void CSub2::virfun1()
    92: {
004010E0 55               push        ebp 
004010E1 8B EC            mov         ebp,esp
004010E3 51               push        ecx 
004010E4 89 4D FC         mov         dword ptr [ebp-4],ecx
    93:    printf("run in CSub2::virfun1( ) /n");
004010E7 68 D4 81 40 00   push        offset KERNEL32_NULL_THUNK_DATA+0F4h (4081D4h)
004010EC E8 85 05 00 00   call        printf (401676h)
004010F1 83 C4 04         add         esp,4
    94: }
004010F4 8B E5            mov         esp,ebp
004010F6 5D               pop         ebp 
004010F7 C3               ret             
 
    95: void CSub2::virfun3()
    96: {
00401100 55               push        ebp 
00401101 8B EC            mov         ebp,esp
00401103 51               push        ecx 
00401104 89 4D FC         mov         dword ptr [ebp-4],ecx
    97:    printf("run in CSub2::virfun3( ) /n");
00401107 68 F0 81 40 00   push        offset KERNEL32_NULL_THUNK_DATA+110h (4081F0h)
0040110C E8 65 05 00 00   call        printf (401676h)
00401111 83 C4 04         add         esp,4
    98: }
00401114 8B E5            mov         esp,ebp
00401116 5D               pop         ebp 
00401117 C3               ret             
 
    99:
   100: void CSub3::virfun2()
   101: {
00401120 55               push        ebp 
00401121 8B EC            mov         ebp,esp
00401123 51               push        ecx 
00401124 89 4D FC         mov         dword ptr [ebp-4],ecx
   102:    printf("run in CSub3::virfun2( ) /n");
00401127 68 0C 82 40 00   push        offset KERNEL32_NULL_THUNK_DATA+12Ch (40820Ch)
0040112C E8 45 05 00 00   call        printf (401676h)
00401131 83 C4 04         add         esp,4
   103: }
00401134 8B E5            mov         esp,ebp
00401136 5D               pop         ebp 
00401137 C3               ret             
 
   104: void CSub3::virfun4()
   105: {
00401140 55               push        ebp 
00401141 8B EC            mov         ebp,esp
00401143 51               push        ecx 
00401144 89 4D FC         mov         dword ptr [ebp-4],ecx
   106:    printf("run in CSub3::virfun4( ) /n");
00401147 68 28 82 40 00   push        offset KERNEL32_NULL_THUNK_DATA+148h (408228h)
0040114C E8 25 05 00 00   call        printf (401676h)
00401151 83 C4 04         add         esp,4
   107: }
00401154 8B E5            mov         esp,ebp
00401156 5D               pop         ebp 
00401157 C3               ret             
 
   108: void CSub3::base2()
   109: {
00401160 55               push        ebp 
00401161 8B EC            mov         ebp,esp
00401163 51               push        ecx 
00401164 89 4D FC         mov         dword ptr [ebp-4],ecx
   110:    printf("run in CSub3::base2( ) /n");
00401167 68 44 82 40 00   push        offset KERNEL32_NULL_THUNK_DATA+164h (408244h)
0040116C E8 05 05 00 00   call        printf (401676h)
00401171 83 C4 04         add         esp,4
   111: }
00401174 8B E5            mov         esp,ebp
00401176 5D               pop         ebp 
00401177 C3               ret             
 
3.3 函数调用分析
 
   128: void test1()
   129: {
00401180 55               push        ebp 
00401181 8B EC            mov         ebp,esp
   130:    base1.virfun1();
00401183 B9 14 A7 40 00   mov         ecx,offset base1 (40A714h)
00401188 E8 73 FE FF FF   call        CBase1::virfun1 (401000h)
       this指针存至 ECX 寄存器,对象直接调用成员函数时直接用 CALL 指令;
   131:
   132:    base1.num = 1;
0040118D C7 05 18 A7 40 00 01 00 00 00 mov         dword ptr [base1+4 (40A718h)],1
            对象直接访问简单成员变量:dword ptr [base1] 为 base1 的虚表指针,
                                      dword ptr [base1+4]为 base1 的第一个成员变量,即要访问的 base1.num
   133:
   134:    pbase1->virfun1();
00401197 A1 E0 A6 40 00   mov         eax,dword ptr [pbase1 (40A6E0h)]
            对象指针 pbase1 的内容即为所指对象的首地址,将 base1 的首地址存至 EAX 寄存器
0040119C 8B 10            mov         edx,dword ptr [eax]
            取得对象的虚表指针并存至 EDX 寄存器
0040119E 8B 0D E0 A6 40 00 mov         ecx,dword ptr [pbase1 (40A6E0h)]
            this指针存至 ECX 寄存器
004011A4 FF 12            call        dword ptr [edx]
            由虚表指针取得第一个虚函数,即所调用的virfun1()
   135:
   136:    pbase1->num = 1;
004011A6 A1 E0 A6 40 00   mov         eax,dword ptr [pbase1 (40A6E0h)]
            将 base1 的首地址存至 EAX 寄存器
004011AB C7 40 04 01 00 00 00 mov         dword ptr [eax+4],1
            访问对象的成员变量:首地址+4为成员变量的开始
   137: }
004011B2 5D               pop         ebp 
004011B3 C3               ret             
 
   138:
   139: void test2()
   140: {
004011C0 55               push        ebp 
004011C1 8B EC            mov         ebp,esp
004011C3 83 EC 08         sub         esp,8
            8字节的临时变量
   141:    CBase1 &tembase1 = sub1;
004011C6 C7 45 FC 20 A7 40 00 mov         dword ptr [tembase1],offset sub1 (40A720h)
   142:
   143:    CBase1 *ptembase1 = &sub1;
004011CD C7 45 F8 20 A7 40 00 mov         dword ptr [ptembase1],offset sub1 (40A720h)
            C++中的引用在存储处理上等价于指针,在32位机器上,每个引用各占4个字节,存储所引对象的首地址
            类 CSub1 的虚表如下:
            CSub1:
                  0x004082FC 004010c0 --> CSub1::virfun1()
                  0x00408300 00401020 --> CBase1::virfun2()
                  0x00408304 00401040 --> CBase1::base1()
   144:
   145:    sub1.virfun1();
004011D4 B9 20 A7 40 00   mov         ecx,offset sub1 (40A720h)
004011D9 E8 E2 FE FF FF   call        CSub1::virfun1 (4010C0h)
            对象直接调用函数,注意这里调用的是CSub1::virfun1(),而不是 CBase1::virfun1()
   146:
   147:    psub1->virfun1();
004011DE A1 E8 A6 40 00   mov         eax,dword ptr [psub1 (40A6E8h)]
004011E3 8B 10            mov         edx,dword ptr [eax]
004011E5 8B 0D E8 A6 40 00 mov         ecx,dword ptr [psub1 (40A6E8h)]
004011EB FF 12            call        dword ptr [edx]
            指针调用函数,分析参 test1()
   148:
   149:    psub1->virfun2();
004011ED A1 E8 A6 40 00   mov         eax,dword ptr [psub1 (40A6E8h)]
004011F2 8B 10            mov         edx,dword ptr [eax]
004011F4 8B 0D E8 A6 40 00 mov         ecx,dword ptr [psub1 (40A6E8h)]
004011FA FF 52 04         call        dword ptr [edx+4]
            指针调用函数,分析参 test1()
   150:
   151:    tembase1.virfun1();
004011FD 8B 45 FC         mov         eax,dword ptr [tembase1]
            将 sub1 的首地址存至 EAX 寄存器
00401200 8B 10            mov         edx,dword ptr [eax]
            将对象的虚表指针存至 EDX 寄存器
00401202 8B 4D FC         mov         ecx,dword ptr [tembase1]
            this指针存至 ECX 寄存器
00401205 FF 12            call        dword ptr [edx]
            引用调用函数,由生成的代码可知:C++中引用在本质上就是指针
   152:
   153:    ptembase1->virfun1();
00401207 8B 45 F8         mov         eax,dword ptr [ptembase1]
0040120A 8B 10            mov         edx,dword ptr [eax]
0040120C 8B 4D F8         mov         ecx,dword ptr [ptembase1]
0040120F FF 12            call        dword ptr [edx]
   154:
   155:    sub1.virfun2();
00401211 B9 20 A7 40 00   mov         ecx,offset sub1 (40A720h) this指针
00401216 E8 05 FE FF FF   call        CBase1::virfun2 (401020h)
   156:
   157:    psub1->virfun2();
0040121B A1 E8 A6 40 00   mov         eax,dword ptr [psub1 (40A6E8h)] 对象的首地址
00401220 8B 10            mov         edx,dword ptr [eax] 虚表指针
00401222 8B 0D E8 A6 40 00 mov         ecx,dword ptr [psub1 (40A6E8h)] this指针
00401228 FF 52 04         call        dword ptr [edx+4] 虚表中第二个函数
   158:
   159:    tembase1.virfun2();
0040122B 8B 45 FC         mov         eax,dword ptr [tembase1] 对象的首地址
0040122E 8B 10            mov         edx,dword ptr [eax] 虚表指针
00401230 8B 4D FC         mov         ecx,dword ptr [tembase1] this指针
00401233 FF 52 04         call        dword ptr [edx+4] 虚表中第二个函数
   160:
   161:    ptembase1->virfun2();
00401236 8B 45 F8         mov         eax,dword ptr [ptembase1] 对象的首地址
00401239 8B 10            mov         edx,dword ptr [eax] 虚表指针
0040123B 8B 4D F8         mov         ecx,dword ptr [ptembase1] this指针
0040123E FF 52 04         call        dword ptr [edx+4] 虚表中第二个函数
   162:
   163:   
   164:    sub1.num = 1;
00401241 C7 05 24 A7 40 00 01 00 00 00 mov         dword ptr [sub1+4 (40A724h)],1
   165:
   166:    psub1->num = 2;
0040124B A1 E8 A6 40 00   mov         eax,dword ptr [psub1 (40A6E8h)] this指针
00401250 C7 40 04 02 00 00 00 mov         dword ptr [eax+4],2
   167:   
   168: }
00401257 8B E5            mov         esp,ebp
00401259 5D               pop         ebp 
0040125A C3               ret             
 
   169:
   170: void test3()
   171: {
00401260 55               push        ebp 
00401261 8B EC            mov         ebp,esp
00401263 83 EC 0C         sub         esp,0Ch
            12字节的临时变量,其中8字节为我们声明的两个引用,另外4个字节编译器使用
   172:    CBase1 &tembase1 = sub2; // <<===>> CBase1 *ptembase1 = &sub2;
00401266 C7 45 FC F4 A6 40 00 mov         dword ptr [tembase1],offset sub2 (40A6F4h)
            类型为 CBase1 的引用 tembase1, 其内容为 sub2 的首地址
 
sub2 在内存中的初始存储情况:(从0x0040A6F4 开始,共24字节)
0x0040A6F0 xxxxxxxx 00408314
0x0040A6F8 00000000 00000000 
0x0040A700  00408308 00000000 
0x0040A708 00000000
可以看出:由于 sub2 有两个父类 CBase1 和 CBase2 (源程序中继承时这两个类的顺序对 sub2 各变量在内存中分布有很大影响,参下面分析)其中在偏移 0 处的虚表指针由 CBase1 继承而来,sub2 的非继承虚函数(CSub2 自己的虚函数)的地址会加到这个指针所指向的虚表中;在偏移 0Ch 处的虚表指针由 CBase2 继承而来,编译器会根据 CSub2 类中虚函数的实现情况修改虚表中的内容。
   173:
   174:    CBase2 &tembase2 = sub2; // <<===>> CBase2 *ptembase2 = &sub2;
0040126D B8 F4 A6 40 00   mov         eax,offset sub2 (40A6F4h)    取 sub2 的首地址
00401272 85 C0            test        eax,eax                     判断引用是否为0
00401274 74 09            je          test3+1Fh (40127Fh)         为 0 则跳转到(40127Fh) (这里不跳转)
00401276 C7 45 F4 00 A7 40 00 mov         dword ptr [ebp-0Ch],offset sub2+0Ch (40A700h)
            将对象 sub2 中偏移为 0CH 处的有效地址移到一临时变量中
0040127D EB 07            jmp         test3+26h (401286h)
0040127F C7 45 F4 00 00 00 00 mov         dword ptr [ebp-0Ch],0
            如果 sub2 的首地址 0,置临时变量也为 0
00401286 8B 4D F4         mov         ecx,dword ptr [ebp-0Ch]
00401289 89 4D F8         mov         dword ptr [tembase2],ecx
            将临时变量赋值给 tembase2,即 CBase2 类型的引用
由上分析可知,CBase2 类型的引用是将 sub2 中偏移为 0CH 处的有效地址作为所 引用 对象的首地址的,这和 sub2 各变量在内存中分布相一致
   175:
   176:    //sub2.virfun3();
   177:    //sub2.virfun2();    // <<===>> psub2->virfun2();
   178:    //error C2385: 对 virfun2 (在 CSub2 中)的访问不明确
            CSub2 没有实现自己的 virfun2(),而两个父类中都有名为 virfun2 的虚函数,所以 sub2.virfun2()这种调用对函数的访问不明确
   179:
   180:    psub2->virfun1();
0040128C 8B 15 EC A6 40 00 mov         edx,dword ptr [psub2 (40A6ECh)] 取对象的首地址
00401292 8B 02            mov         eax,dword ptr [edx] 取偏移为 0 的虚表指针
00401294 8B 0D EC A6 40 00 mov         ecx,dword ptr [psub2 (40A6ECh)] 取对象的首地址存到 ECX 中作为对象的this指针
0040129A FF 10            call        dword ptr [eax] 调用虚表中第一个虚函数
   181:
   182:    psub2->s2num = 2;
0040129C 8B 0D EC A6 40 00 mov         ecx,dword ptr [psub2 (40A6ECh)]
004012A2 C7 41 14 02 00 00 00 mov         dword ptr [ecx+14h],2
   183:
   184:    tembase1.num = 3;
004012A9 8B 55 FC         mov         edx,dword ptr [tembase1]
004012AC C7 42 04 03 00 00 00 mov         dword ptr [edx+4],3
   185:
   186:    tembase2.num = 4;
004012B3 8B 45 F8         mov         eax,dword ptr [tembase2]
004012B6 C7 40 04 04 00 00 00 mov         dword ptr [eax+4],4
            执行完上面三条赋值语句后 sub2 在内存中的样子:
以单字节为单位:
0x0040A6F0 xx xx xx xx 14 83 40 00 
0x0040A6F8 03 00 00 00 00 00 00 00 
0x0040A700 08 83 40 00 04 00 00 00 
0x0040A708 02 00 00 00
以四字节为单位:
0x0040A6F0 xxxxxxxx 00408314 
0x0040A6F8 00000003 00000000 
0x0040A700 00408308 00000004 
0x0040A708 00000002
由此可对 sub2 中各变量在内存中的分布有更进一步的认识
   187:
   188:    psub2->virfun3();
004012BD 8B 0D EC A6 40 00 mov         ecx,dword ptr [psub2 (40A6ECh)]
004012C3 8B 11            mov         edx,dword ptr [ecx]
004012C5 8B 0D EC A6 40 00 mov         ecx,dword ptr [psub2 (40A6ECh)]
004012CB FF 52 0C         call        dword ptr [edx+0Ch]
   189:
   190:    psub2->base1();
004012CE A1 EC A6 40 00   mov         eax,dword ptr [psub2 (40A6ECh)]
004012D3 8B 10            mov         edx,dword ptr [eax]
004012D5 8B 0D EC A6 40 00 mov         ecx,dword ptr [psub2 (40A6ECh)]
004012DB FF 52 08         call        dword ptr [edx+8]
   191:
   192:    psub2->base2();
004012DE 8B 0D EC A6 40 00 mov         ecx,dword ptr [psub2 (40A6ECh)] 取 sub2 首地址于 ECX 中
004012E4 83 C1 0C         add         ecx,0Ch 将 sub2 中偏移为 0CH 的有效地址作为调用 base2()时所用的this指针
004012E7 A1 EC A6 40 00   mov         eax,dword ptr [psub2 (40A6ECh)] 取 sub2 首地址
004012EC 8B 50 0C         mov         edx,dword ptr [eax+0Ch] 取偏移为 0CH 的虚表指针
004012EF FF 52 08         call        dword ptr [edx+8] 调用虚表中第三个虚函数
            注意,在调用 CBase1::base1() 和调用 CBase2::base2()函数时函数所使用的 this 指针,也就是 ECX 寄存器的内容是不一样的,请注意区别
笔记:真正当前调用的函数是实现在哪个类中,this指针也就是ecx就必须调到这个类的内存块在当前调用函数的对象中的位置。
有两种情况需要改this指针:
1.通过派生类指针调用函数,但是真正的函数代码是基类的,需要把this改成基类在派生类对象中的位置。这里就是这样,这里改this指针好像很简单,直接加偏移就好了。因为派生类对自己所有的基类都是了解的,他知道这个函数是肯定是哪个基类的及偏移多少。
2.通过基类指针调用函数,但是真正的函数代码是派生类的,需要吧this改成派生类在当前对象中的位置。如果基类是第一顺位基类,则指向基类等于指向对象,不用改this。这种改this有点麻烦,详见下面test3的196:    tembase2.virfun1();      // <<===>> ptembase2->virfun1();。由于基类的指针调用时,不知道当前指向基类还是派生类。开始这段代码(也是thunk的)是通用的,不管是指向基类还派生类。所以必须当指向派生类时,通过虚函数表指向的thunk代码来调整this指针。
   193:
            在 C++ 中,只有指针或引用支持 OO 程序设计中所需的多态性质。下面两个调用在代码上看似相同,执行结果也相同:都是调用 CSub2::virfun1(),但应该注意到引用 tembase1 与 tembase2 是不一样的:引用 tembase1 内容为 sub2 的首地址,引用 tembase2 内容为 sub2 中偏移为 0CH 处的地址。下面具体分析:
   194:    tembase1.virfun1();      // <<===>> ptembase1->virfun1();
004012F2 8B 45 FC         mov         eax,dword ptr [tembase1]
            sub2 的首地址移至 EAX
004012F5 8B 10            mov         edx,dword ptr [eax]
            sub2 中偏移为 0 的虚表指针移至 EDX
004012F7 8B 4D FC         mov         ecx,dword ptr [tembase1]
             sub2 的首地址移至 ECX 作为调用函数所用的 this 指针
004012FA FF 12            call        dword ptr [edx]
            调用虚表中第一个函数,即 CSub2::virfun1()
 
CSub2 偏移为 0 处虚表指针所指向的内容:
0x00408314 004010e0 --> CSub2::virfun1()
0x00408318 00401020 --> CBase1::virfun2()
0x0040831C 00401040 --> CBase1::base1()
0x00408320 00401100 --> CSub2::virfun3()
 
   195:
   196:    tembase2.virfun1();      // <<===>> ptembase2->virfun1();
004012FC 8B 45 F8         mov         eax,dword ptr [tembase2]
            sub2 中偏移为 0CH 处的地址移至 EAX
004012FF 8B 10            mov         edx,dword ptr [eax]
            sub2 中偏移为 0CH 的虚表指针移至 EDX
00401301 8B 4D F8         mov         ecx,dword ptr [tembase2]
            sub2 的偏移为 0CH 处的地址移至 ECX 作为调用函数所用的 this 指针吗?
00401304 FF 12            call        dword ptr [edx]
            调用偏移为 0CH 的虚表中第一个函数,但目前看函数所需的 this 指针不对呀:实际执行的是 CSub2::virfun1(),那么在执行前 ECX 的值应为 sub2 的首地址才对。让我们再看一下虚表中的内容吧:
 
 
 
 
CSub2 偏移为 0CH 处虚表指针所指向的内容:
0x00408308 00401540 --> CSub2::virfun1`adjustor{12}
0x0040830C 00401080 --> CBase2::virfun2()
0x00408310 004010a0 --> CBase2::base2()
这个虚表中第一个项不是 CSub2::virfun1() 的地址,而是一个所谓的 adjustor 的地址,让我们再看看这个 adjustor:

CSub3::virfun2有adjustor,说明A继承了BC,BC都有同一函数
virfun2,A实现了 virfun2,就会有adjustor。
CSub3::base2 没有 adjustor,说明 A继承了BC,C有B没有函数 base2 ,A实现了 base2 ,就没有adjustor。
而两者的实现都是CSub3,所以说实际代码是谁实现的,this指针就应该指哪是不对的。

某一类的指针(不管是指向他本身的类还是其继承类)调用一个函数,this (第一段thunk代码内的this,未经过虚函数表内adjustor的this) 设置的流程:
A继承了BC,C继承了DE,调用pA.test()。
1.如果函数
test ()在第一顺位基类(这里是B)的虚函数表里面有,调用这个函数的时候this就指向第一顺位虚函数表所在的类(即使C的虚函数表里面也有test函数)。
2.如果第一顺位的虚函数表内没有此函数this就顺延检查第二顺位的类,看C的虚函数表内有没有。由于C继承了DE,有两个虚函数表,先查在前面的D的。D的虚函数表在C的开头,包括了D的虚函数以及C自己添加的虚函数。先检查D的,再检查E的,如果E还有两个基类的话依次类推。
哪个类的虚函数第一个查到有这个test()函数,就把this指针从A调到那个类。

这里CSub2的指针不论指向CSub2还是CSub3调用
virfun2的第一段thunk代码是一样的,this指针在那段代码里面也都是调到CBase2的。
设完这个this后,就看虚函数表内是否有adjustor了,这就取决于实际指向什么类了。 但是如果 CSub2类型的指针 实际指向CSub3,this指针就会在虚函数表中指向的thunk  adjustor到CSub3,而如果指向CSub2就依旧还指向CBase2。

是否adjustor以及adjustor this指针到哪个指向那个类的流程:
A继承了BC,C继承了DE,E继承了FG。我们要设E中一个函数test()的adjustor要把this指到哪个类。
跟前面调第一段thunk代码内this指针类似,从前往后依顺位找,第一个发现
类(虚函数表中也有这个函数,同名同参数即可,不一定要继承自同一祖基类的)就把this指针adjustor到这个类。
如果直到顺位查到本身所在的虚函数表(F的虚函数表)还没有查到含有test()这个函数的虚函数表,
F(针对A类来说的类F部分)的虚函数表中test()被调用时就不需要adjustor。

这里的adjustor是针对A类这个对象了,不管是什么指针指向A的对象来调用F中的test()这个函数,都会被adjustor 把this调到A类内的同一个地方,如A类中的类D部分(也就是类C)。
 
[thunk]:CSub2::virfun1`adjustor{12}':
00401540 83 E9 0C         sub         ecx,0Ch
00401543 E9 98 FB FF FF   jmp         CSub2::virfun1 (4010E0h)
到此,一切都清楚了:在 adjustor 中, ECX 的值终于改成了我们所认为的 sub2 的首地址,而且最终执行的是 CSub2::virfun1()。下面两个函数的调用和上面类似,只不过 CSub2 偏移为 0CH 处虚表指针所指向的虚表中第二项不是adjustor 的地址,而是 CBase2::virfun2() 的地址,那么在调用tembase2.virfun2()时 ECX 的值就是所要的:sub2 的偏移为 0CH 处的地址
jmp与call的区别:CALL调用一段代码,通常这段代码执行到最后有个ret指令,执行该指令就返回到CALL的下一条指令了。 JMP跳转到另一个地方运行,通常不会再转回来了。
这里用jmp是因为前面  00401304 FF 12            call        dword ptr [edx]          call过了,这个call调用这段thunk,而这段thunk又没有ret指令,所以这个call与jmp CSub2::virfun1函数内的ret指令成对。所以不用call而用jmp。不知是否是这样。
   197:   
   198:    tembase1.virfun2();      // <<===>> ptembase1->virfun2();
00401306 8B 45 FC         mov         eax,dword ptr [tembase1]
00401309 8B 10            mov         edx,dword ptr [eax]
0040130B 8B 4D FC         mov         ecx,dword ptr [tembase1]
0040130E FF 52 04         call        dword ptr [edx+4]
   199:
   200:    tembase2.virfun2();      // <<===>> ptembase2->virfun2();
00401311 8B 45 F8         mov         eax,dword ptr [tembase2]
00401314 8B 10            mov         edx,dword ptr [eax]
00401316 8B 4D F8         mov         ecx,dword ptr [tembase2]
00401319 FF 52 04         call        dword ptr [edx+4]
   201: }
0040131C 8B E5            mov         esp,ebp
0040131E 5D               pop         ebp 
0040131F C3               ret             
   202:
 
test4()中各调用和上面分析无本质差别,有兴趣的朋友可自己分析一下。
 
   203: void test4()
   204: {
00401320 55               push        ebp 
00401321 8B EC            mov         ebp,esp
00401323 83 EC 10         sub         esp,10h
   205:    CBase1 &tembase1 = sub3; // <<===>> CBase1 *ptembase1 = &sub2;
00401326 C7 45 FC 2C A7 40 00 mov         dword ptr [tembase1],offset sub3 (40A72Ch)
   206:
   207:    CBase2 &tembase2 = sub3; // <<===>> CBase2 *ptembase2 = &sub2;
0040132D B8 2C A7 40 00   mov         eax,offset sub3 (40A72Ch)
00401332 85 C0            test        eax,eax
00401334 74 09            je          test4+1Fh (40133Fh)
00401336 C7 45 F0 38 A7 40 00 mov         dword ptr [ebp-10h],offset sub3+0Ch (40A738h)
0040133D EB 07            jmp         test4+26h (401346h)
0040133F C7 45 F0 00 00 00 00 mov         dword ptr [ebp-10h],0
00401346 8B 4D F0         mov         ecx,dword ptr [ebp-10h]
00401349 89 4D F8         mov         dword ptr [tembase2],ecx
   208:
   209:    CSub2 &temsub2 = sub3;
0040134C C7 45 F4 2C A7 40 00 mov         dword ptr [temsub2],offset sub3 (40A72Ch)
   210:
   211:    psub3->virfun4();
00401353 8B 15 F0 A6 40 00 mov         edx,dword ptr [psub3 (40A6F0h)]
00401359 8B 02            mov         eax,dword ptr [edx]
0040135B 8B 0D F0 A6 40 00 mov         ecx,dword ptr [psub3 (40A6F0h)]
00401361 FF 50 10         call        dword ptr [eax+10h]
   212:
   213:    psub3->virfun2();
00401364 8B 0D F0 A6 40 00 mov         ecx,dword ptr [psub3 (40A6F0h)]
0040136A 8B 11            mov         edx,dword ptr [ecx]
0040136C 8B 0D F0 A6 40 00 mov         ecx,dword ptr [psub3 (40A6F0h)]
00401372 FF 52 04         call        dword ptr [edx+4]
   214:
   215:    tembase1.virfun2();
00401375 8B 45 FC         mov         eax,dword ptr [tembase1]
00401378 8B 10            mov         edx,dword ptr [eax]
0040137A 8B 4D FC         mov         ecx,dword ptr [tembase1]
0040137D FF 52 04         call        dword ptr [edx+4]
   216:
   217:    tembase2.virfun2();
00401380 8B 45 F8         mov         eax,dword ptr [tembase2]
00401383 8B 10            mov         edx,dword ptr [eax]
00401385 8B 4D F8         mov         ecx,dword ptr [tembase2]
00401388 FF 52 04         call        dword ptr [edx+4]
   218:
   219:    tembase2.base2();
0040138B 8B 45 F8         mov         eax,dword ptr [tembase2]
0040138E 8B 10            mov         edx,dword ptr [eax]
00401390 8B 4D F8         mov         ecx,dword ptr [tembase2]
00401393 FF 52 08         call        dword ptr [edx+8]
   220:
   221:    temsub2.base2();
00401396 8B 4D F4         mov         ecx,dword ptr [temsub2]
00401399 83 C1 0C         add         ecx,0Ch
0040139C 8B 45 F4         mov         eax,dword ptr [temsub2]
0040139F 8B 50 0C         mov         edx,dword ptr [eax+0Ch]
004013A2 FF 52 08         call        dword ptr [edx+8]
这段代码与前面test3()中psub2->base2()一模一样,所以虚函数完全不用考虑当前指针实际指向哪里。
某一个特定类的指针(不管它实际指向这个类的第N代派生类)调用一个特定名称的函数    其thunk代码   在调用虚函数表中的真实函数之前   是完全一样的。
比如这里如果sub3没有实现base2,这段thunk代码也还是这样。
或者sub3第一顺位基类不是sub2,是另一个基类也有个函数base2,sub3实现了base2。这样sub2范围的虚函数表内的base2指向改this指针的thunk代码。这段thunk代码也还是这样。
原因在于 某一个特定类的指针调用一个特定名称的函数, 相当于确定了类和函数,这段thunk代码中 mov         ecx,dword ptr [temsub2] 可以看出基于类可以确定地址dword ptr [edx+8]基于函数。所以不管实际指向什么派生类,都可以正确调用。
   222: }
004013A5 8B E5            mov         esp,ebp
004013A7 5D               pop         ebp 
004013A8 C3               ret             
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值