C++ 对象内存布局

C++对象内存布局(1)...1

1.1    最简单的类...1

1.1.1   赋值语句的行为...2

1.1.2   函数调用的行为...2

1.1.3   当p指向自定义的缓冲区...3

C++对象内存布局(2)...4

1.1.1   内存布局...4

1.1.2   函数调用...5

1.1.3   变量覆盖...5

1.1.4   函数覆盖...6

C++对象内存布局(3)...6

1.1.1   内存布局...7

1.1.2   父类虚函数调用...7

1.1.3   子类虚函数的调用...8

1.1.4   父类指针调用子类的虚函数...9

C++对象内存布局(4)...9

1.1.1   内存布局...10

1.1.2   虚函数调用...11

1.1.3   当两个父类有同名的虚函数...12

C++对象内存布局(5)...12

1.1.1   Vtbl的属性...13

1.1.2   单继承时vtbl的内容...14

1.1.3   多重继承时VTBL的内容...14

 

C++对象内存布局(1)

1.1    最简单的类

写一小段简单代码:

class CTest

{

public:

     int var_a;

     int var_b;

     int var_c;

 

public:

     void fun1()

     {

         var_a = 10;

     }

     void fun2()

     {

         var_b = 20;

     }

};

 

CTest a, *p;

 

int _tmain(int argc, _TCHAR* argv[])

{

     a.var_a = 1;

     a.var_b = 2;

     a.var_c = 3;

 

     p = &a;

     p->fun1();

     p->fun2();

     return 0;

}

为了更直观地观察编译器的行为,把a和p做为全局变量。

1.1.1  赋值语句的行为

先观察赋值语句的行为:

     a.var_a = 1;

004113CE C7 05 C0 74 41 00 01 00 00 00 mov         dword ptr [a (4174C0h)],1

     a.var_b = 2;

004113D8 C7 05 C474 41 00 02 00 00 00 mov         dwordptr [a+4 (4174C4h)],2

     a.var_c = 3;

004113E2 C7 05 C874 41 00 03 00 00 00 mov         dwordptr [a+8 (4174C8h)],3

再观察赋值结束后p指向的内存区域:

0x004174C0  01 0000 00 02 00 00 00 03 00 00 00 00 00 00 ...............

显然,这个时候p指向CTest::var_a,也就是说vs2008并没有在类中插入多余的内容。CTest的大小为var_a, var_b, var_c的大小之和,即12个字节。

1.1.2  函数调用的行为

再观察一下函数调用的行为:

     p->fun1();

004113F6 8B 0D BC 74 41 00mov         ecx,dword ptr [p (4174BCh)]

004113FC E8 53 FD FF FF   call       CTest::fun1 (411154h)

这段代码可以说明CTest::fun1的调用和其它的C代码并没有什么不同,一个函数而已。看看call把我们带到哪里:

00411118 E9 83 03 00 00   jmp        CTest::fun2 (4114A0h)

0041111D E9 6E 15 00 00   jmp        __CxxUnhandledExceptionFilter (412690h)

00411122 E9 E9 15 00 00   jmp        __CxxSetUnhandledExceptionFilter (412710h)

00411127 E9 64 24 00 00   jmp        QueryPerformanceCounter (413590h)

0041112C E9 9F 16 00 00   jmp        _wsetargv (4127D0h)

00411131 E9 0A 17 00 00   jmp        __p__commode (412840h)

00411136 E9 89 22 00 00   jmp        _unlock (4133C4h)

0041113B E9 62 24 00 00   jmp        GetCurrentProcessId (4135A2h)

00411140 E9 9B 04 00 00   jmp        _RTC_CheckStackVars2 (4115E0h)

00411145 E9 80 18 00 00   jmp        __set_app_type (4129CAh)

0041114A E9 A1 03 00 00   jmp        _RTC_CheckEsp (4114F0h)

0041114F E9 8C 16 00 00   jmp        _RTC_Initialize (4127E0h)

00411154 E9 07 03 00 00   jmp        CTest::fun1 (411460h)

一个神秘的地方,只是func1和func2为什么要离得这么远呢?莫非有什么特殊的考虑?暂且不管它,在jmp的带领下,看到了func1的代码:

     void fun1()

     {

00411460 55               push        ebp 

00411461 8B EC            mov         ebp,esp

00411463 81 EC CC 00 00 00sub         esp,0CCh

00411469 53               push       ebx 

0041146A 56               push        esi 

0041146B 57               push        edi 

0041146C 51               push        ecx 

0041146D 8D BD 34 FF FF FFlea         edi,[ebp-0CCh]

00411473 B9 33 00 00 00   mov        ecx,33h

00411478 B8 CC CC CC CC   mov        eax,0CCCCCCCCh

0041147D F3 AB            rep stos    dword ptr es:[edi]

0041147F 59               pop         ecx 

00411480 89 4D F8         mov         dword ptr [ebp-8],ecx

         var_a = 10;

00411483 8B 45 F8         mov         eax,dword ptr [this]

00411486 C7 00 0A 00 00 00 mov         dword ptr [eax],0Ah

     }

0041148C 5F               pop         edi 

0041148D 5E               pop         esi 

0041148E 5B               pop        ebx 

0041148F 8B E5            mov        esp,ebp

00411491 5D               pop         ebp 

00411492 C3               ret             

传说中的对象应该有一个this指针的,怎么传递过来的?

在调用fun1之前,将p的值赋给了ecx,在fun1里面,其它寄存器都是函数开始就入栈,函数退出时再出栈,唯独ecx是个例外,想来vs就是通过ECX来传递this指针的值。

1.1.3  当p指向自定义的缓冲区

由于在此种简单情况下,vs2008并没有添加额外的空间,想来把p指向自己定义的数组再调用其成员函数应该也可以工作得很好,试试下面的代码:

     int buf[3] = {4, 5, 6};

     p = (CTest*)buf;

    p->fun1();

经过此调用,buf[0]的值果然变成了10。

 

 

 

C++对象内存布局(2)

 

 

这次我们添加一个子类,父类和子类均不带虚函数:

class CParent

{

public:

     int parent_a;

     int parent_b;

 

public:

     void parent_f1()

     {

         parent_a = 0x10;

     }

     void parent_f2()

     {

         parent_b = 0x20;

     }

};

 

class CChild :public CParent

{

public:

     int child_a;

     int child_b;

 

public:

     void child_f1()

     {

         child_a = 0x30;

     }

     void child_f2()

     {

         child_b = 0x40;

     }

};

1.1.1  内存布局

先给这些成员变量赋几个值

     child.parent_a = 1;

0041138E C7 05 50 71 41 00 0100 00 00 mov         dword ptr [child(417150h)],1

     child.parent_b = 2;

00411398 C7 05 54 71 41 00 0200 00 00 mov         dword ptr [child+4(417154h)],2

     child.child_a = 3;

004113A2 C7 05 58 71 41 00 03 00 00 00 mov        dword ptr [child+8 (417158h)],3

     child.child_b = 4;

004113AC C7 05 5C 71 41 00 04 00 00 00 mov         dword ptr [child+0Ch (41715Ch)],4

观察&child所指的内存区内容:

0x00417150  01 00 00 00 02 00 00 00 03 00 00 00 04 00 0000  ................

很明显,VS将父类和子类的数据成员合并到同一块内存区域,但是同样没有往里面加额外的东西,&child指针将指向第一个成员parent_a。

1.1.2  函数调用

观察函数调用:

     child.parent_f1();

00411840 B9 50 71 41 00   mov        ecx,offset child (417150h)

00411845 E8 78 F9 FF FF   call       CParent::parent_f1 (4111C2h)

     child.child_f1();

0041184A B9 50 71 4100   mov         ecx,offset child (417150h)

0041184F E8 64 F9 FF FF   call       CChild::child_f1 (4111B8h)

和简单父类的函数调用没什么两样。用ECX传递this指针。

1.1.3  变量覆盖

在子类里添加两个和父类同名的成员变量:

class CChild :public CParent

{

public:

     int child_a;

     int child_b;

     int parent_a;

     int parent_b;

………

再执行前面的赋值语句:

     child.parent_a = 1;

0041180E C7 05 04 75 41 00 0100 00 00 mov         dword ptr [child+10h(417504h)],1

     child.parent_b = 2;

00411818 C7 05 08 75 41 00 0200 00 00 mov         dword ptr [child+14h(417508h)],2

     child.child_a = 3;

00411822 C7 05 FC 74 41 00 0300 00 00 mov         dword ptr [child+8(4174FCh)],3

     child.child_b = 4;

0041182C C7 05 00 75 41 0004 00 00 00 mov         dword ptr [child+0Ch (417500h)],4

可以发现赋值时使用的偏移量发生了变化,观察&child所在的内存区:

0x004174F4  00 0000 00 00 00 00 00 03 00 00 00 04 00 00 00 ................

0x00417504  01 00 00 00 02 00 00 00 00 00 00 00 00 00 0000  ................

可以看到合并后的内存有6个整数,且对child.parent_a的赋值其实是对子类的成员进行赋值。

而当我们用下面的赋值语句时:

     pparent = &child;

00412E44 C7 05 E4 74 41 00 F474 41 00 mov         dword ptr [pparent(4174E4h)],offset child (4174F4h)

     pparent->parent_a = 1;

00412E4E A1 E4 74 41 00   mov        eax,dword ptr [pparent (4174E4h)]

00412E53 C7 00 01 00 00 00 mov         dword ptr [eax],1

     pparent->parent_b = 2;

00412E59 A1 E4 74 41 00  mov         eax,dword ptr [pparent(4174E4h)]

00412E5E C7 40 04 02 00 00 00mov         dword ptr [eax+4],2

从偏移量就可以看出它是对父类的成员进行赋值。

1.1.4  函数覆盖

在子类里面添加一个和父类同名的函数,再进行函数调用:

     pchild = &child;

00412E26 C7 05 F074 41 00 F4 74 41 00 mov         dword ptr [pchild (4174F0h)],offset child (4174F4h)

     pchild->parent_f1();

00412E30 8B 0D F0 74 41 00mov         ecx,dword ptr [pchild (4174F0h)]

00412E36 E8 9B E3 FF FF   call       CChild::parent_f1 (4111D6h)

 

     pparent = &child;

00412E3B C7 05 E4 74 41 00 F4 74 41 00 mov         dword ptr [pparent (4174E4h)],offsetchild (4174F4h)

     pparent->parent_f1();

00412E45 8B 0D E4 74 41 00mov         ecx,dword ptr [pparent(4174E4h)]

00412E4B E8 72 E3 FF FF   call       CParent::parent_f1 (4111C2h)

从反汇编的结果可以明显看出使用父类指针和使用子类指针调用同名函数时的差异。

 

 

 

 

 

C++对象内存布局(3)

这次我们为父类加上虚函数:

class CParent

{

public:

     int parent_a;

     int parent_b;

 

public:

     virtual void parent_f1()

     {

         parent_a = 0x10;

     }

     virtual void parent_f2()

     {

         parent_b = 0x20;

     }

};

 

class CChild :public CParent

{

public:

     int child_a;

     int child_b;

 

public:

     virtual void parent_f1()

     {

         child_a = 0x30;

     }

     virtual void parent_f2()

     {

         child_b = 0x40;

     }

};

CChild child, *pchild;

CParent parent, *pparent;

 

1.1.1  内存布局

同样,我们先对其进行简单赋值再观察内存变化。

先对父类进行赋值:

     parent.parent_a = 1;

004139FE C7 05 B8 81 41 00 0100 00 00 mov         dword ptr [parent+4(4181B8h)],1

     parent.parent_b = 2;

00413A08 C7 05 BC 81 41 00 02 00 00 00 mov        dword ptr [parent+8 (4181BCh)],2

看内存:

0x004181B4  5c68 41 00 01 00 00 00 02 00 00 00 00 00 00 00 \hA.............

很显然,在数据之前插入了四个字节,猜想这个应该就是所谓的vtbl的指针,如果这样,那么所有同类对象都应该有相同的指针,定义两个变量:

CParent v1, v2;

先看v1的内存:

0x0012FF58  5c68 41 00 cc cc cc cc cc cc cc cc cc cc cc cc PhA.............

再看v2的内存:

0x0012FF44  5c68 41 00 cc cc cc cc cc cc cc cc cc cc cc cc PhA.............

果然是一样的,嘿。

再看子类的赋值:

          child.child_a = 3;

00413A12 C7 05 AC 81 41 00 03 00 0000 mov         dword ptr [child+0Ch(4181ACh)],3

     child.child_b = 4;

00413A1C C7 05 B0 81 41 00 04 00 00 00 mov        dword ptr [child+10h (4181B0h)],4

看&child的内存:

0x004181A0  50 6841 00 00 00 00 00 00 00 00 00 03 00 00 00 PhA.............

0x004181B0  04 00 00 00 5c 68 41 00 01 00 00 00 02 00 00 00  ....\hA.........

很明显,父类和子类指向的vtbl是不一样的。

 

1.1.2  父类虚函数调用

先看父类虚函数的调用:

     pparent = &parent;

00413A26 C7 05 98 81 41 00 B4 81 41 00 mov        dword ptr [pparent (418198h)],offset parent (4181B4h)

     pparent->parent_f1();

00413A30 A1 98 81 41 00   mov         eax,dword ptr [pparent (418198h)]

00413A35 8B 10            mov         edx,dword ptr [eax]

00413A37 8B F4            mov         esi,esp

00413A39 8B 0D 98 81 4100 mov         ecx,dword ptr [pparent(418198h)]

00413A3F 8B 02            mov         eax,dword ptr [edx]

00413A41 FF D0            call        eax 

00413A43 3B F4            cmp         esi,esp

00413A45 E8 1E D7 FFFF   call        @ILT+355(__RTC_CheckEsp) (411168h)

     pparent->parent_f2();

00413A4A A1 98 81 41 00   mov         eax,dword ptr [pparent (418198h)]

00413A4F 8B 10            mov         edx,dword ptr [eax]

00413A51 8B F4            mov         esi,esp

00413A53 8B 0D 98 81 4100 mov         ecx,dword ptr [pparent(418198h)]

00413A59 8B 42 04         mov         eax,dword ptr [edx+4]

00413A5C FF D0            call        eax 

00413A5E 3B F4            cmp         esi,esp

00413A60 E8 03 D7 FFFF   call        @ILT+355(__RTC_CheckEsp) (411168h)

这回的函数调用就不是简单地使用名称进行调用了,而是先取出vtbl的首地址(pparent指向的头四个字节),然后加上偏移量,再取出这个新地址中存储的指针,最后用call进行函数调用。这个偏移量是由编译器自动排列得到的。

我们看看vtbl的内容:

0x0041685C b4 10 41 00 08 12 41 00 44 76 41 00 dc 10 41 00  ..A...A.DvA...A.

这里有两个指针:0x004110b4和0x00411208,看看它们指向的位置:

004110B4 E9 57 0C 00 00  jmp         CParent::parent_f1(411D10h)

……..

00411208 E9 43 0B 00 00   jmp        CParent::parent_f2 (411D50h)

果然指向的是父类的成员函数。

1.1.3  子类虚函数的调用

现在我们用子类的指针调用子类的虚函数:

     pchild = &child;

00413A65 C7 05 9C 81 4100 A0 81 41 00 mov         dword ptr [pchild (41819Ch)],offsetchild (4181A0h)

     pchild->parent_f1();

00413A6F A1 9C 81 41 00   mov        eax,dword ptr [pchild (41819Ch)]

00413A74 8B 10            mov         edx,dword ptr [eax]

00413A76 8B F4            mov         esi,esp

00413A78 8B 0D 9C 81 41 00 mov         ecx,dword ptr [pchild (41819Ch)]

00413A7E 8B 02            mov         eax,dword ptr [edx]

00413A80 FF D0            call        eax 

00413A82 3B F4            cmp         esi,esp

00413A84 E8 DF D6 FFFF   call        @ILT+355(__RTC_CheckEsp)(411168h)

     pchild->parent_f2();

00413A89 A1 9C 81 41 00   mov        eax,dword ptr [pchild (41819Ch)]

00413A8E 8B 10            mov         edx,dword ptr [eax]

00413A90 8B F4            mov         esi,esp

00413A92 8B 0D 9C 81 41 00 mov         ecx,dword ptr [pchild (41819Ch)]

00413A98 8B 42 04         mov         eax,dword ptr [edx+4]

00413A9B FF D0            call        eax 

00413A9D 3B F4            cmp         esi,esp

00413A9F E8 C4 D6 FF FF   call       @ILT+355(__RTC_CheckEsp)(411168h)

它的调用方式和父类完全一样,都是先取vtbl的首地址,再加上偏移量后进行call。看一下子类的vtbl:

0x00416850  a911 41 00 0d 12 41 00 20 76 41 00 b4 10 41 00 ..A...A. vA...A.

这里有两个指针:0x004111a9和0x0041120d,看看它们指向的位置:

004111A9 E9 82 03 0000   jmp         CChild::parent_f1 (411530h)

……..

0041120D E9 6E 03 00 00   jmp        CChild::parent_f2 (411580h)

果然指向的是子类的成员函数。

1.1.4  父类指针调用子类的虚函数

这回我们用一个父类指针来调用虚函数:

     pparent = &child;

00413AA4 C7 05 98 81 41 00 A081 41 00 mov         dword ptr [pparent(418198h)],offset child (4181A0h)

     pparent->parent_f1();

00413AAE A1 98 81 41 00   mov        eax,dword ptr [pparent (418198h)]

00413AB3 8B 10            mov         edx,dword ptr [eax]

00413AB5 8B F4            mov         esi,esp

00413AB7 8B 0D 98 81 41 00mov         ecx,dword ptr [pparent(418198h)]

00413ABD 8B 02            mov         eax,dword ptr [edx]

00413ABF FF D0            call        eax 

00413AC1 3B F4            cmp         esi,esp

00413AC3 E8 A0 D6 FF FF   call       @ILT+355(__RTC_CheckEsp)(411168h)

可以看到,这个时候使用的是子函数的VTBL,因此调用的自然是子类的虚函数。

 

 

 

 

 

 

C++对象内存布局(4)

这回我们考虑多重继承的情况:

class CParentA

{

public:

     int parenta_a;

     int parenta_b;

 

public:

     virtual void parenta_f1()

     {

         parenta_a = 0x10;

     }

     virtual void parenta_f2()

     {

         parenta_b = 0x20;

     }

};

 

class CParentB

{

public:

     int parentb_a;

     int parentb_b;

 

public:

     virtual void parentb_f1()

     {

         parentb_a = 0x30;

     }

     virtual void parentb_f2()

     {

         parentb_b = 0x40;

     }

};

 

class CChild :public CParentA,public CParentB

{

public:

     int child_a;

     int child_b;

 

public:

     virtual void parenta_f1()

     {

         child_a = 0x50;

     }

     virtual void parenta_f2()

     {

         child_b = 0x60;

     }

    virtual void parentb_f1()

     {

         child_a = 0x70;

     }

     virtual void parentb_f2()

     {

         child_b = 0x80;

     }

};

 

CChild child, *pchild;

这个子类拥有两个父类。

1.1.1  内存布局

我们先对子类成员进行赋值,然后观察其内存变化:

     child.parenta_a = 1;

00413B9E C7 05 50 85 41 00 0100 00 00 mov         dword ptr [child+4(418550h)],1

     child.parenta_b = 2;

00413BA8 C7 05 54 85 41 00 02 00 00 00 mov         dword ptr [child+8 (418554h)],2

     child.parentb_a = 3;

00413BB2 C7 05 5C85 41 00 03 00 00 00 mov         dword ptr [child+10h (41855Ch)],3

     child.parentb_b = 4;

00413BBC C7 05 60 85 41 00 0400 00 00 mov         dword ptr [child+14h(418560h)],4

     child.child_a = 5;

00413BC6 C7 05 64 85 41 00 05 00 00 00 mov         dword ptr [child+18h (418564h)],5

     child.child_b = 6;

00413BD0 C7 05 68 85 41 00 06 00 00 00 mov         dword ptr [child+1Ch (418568h)],6

再看&child的内容:

0x0041854C 60 68 41 00 01 00 00 00 02 00 00 00 50 68 41 00  `hA.........PhA.

0x0041855C 03 00 00 00 04 00 00 00 05 00 00 00 06 00 00 00  ................

很容易可以发现,这个子类空间中包含有两个vtbl的指针。再有就是父类和子类的数据成员。当我们把继承的顺序改为:

class CChild :public CParentB,public CParentA

再看内存:

0x0041854C 60 68 41 00 03 00 00 00 04 00 00 00 50 68 41 00  `hA.........PhA.

0x0041855C 01 00 00 00 02 00 00 00 05 00 00 00 06 00 00 00  ................

可以发现,vs对类成员的排列是按照继承的顺序而来的。

1.1.2  虚函数调用

观察子类对虚函数的调用:

     pchild = &child;

00413BDA C7 05 48 85 41 00 4C 85 41 00 mov         dword ptr [pchild (418548h)],offsetchild (41854Ch)

     pchild->parenta_f1();

00413BE4 A1 48 85 41 00  mov         eax,dword ptr [pchild(418548h)]

00413BE9 8B 10            mov         edx,dword ptr [eax]

00413BEB 8B F4            mov         esi,esp

00413BED 8B 0D 48 85 41 00mov         ecx,dword ptr [pchild(418548h)]

00413BF3 8B 02            mov         eax,dword ptr [edx]

00413BF5 FF D0            call        eax 

00413BF7 3B F4            cmp         esi,esp

00413BF9 E8 74 D5 FF FF   call       @ILT+365(__RTC_CheckEsp)(411172h)

     pchild->parenta_f2();

00413BFE A1 48 85 41 00   mov        eax,dword ptr [pchild (418548h)]

00413C03 8B 10            mov         edx,dword ptr [eax]

00413C05 8B F4            mov         esi,esp

00413C07 8B 0D 48 85 4100 mov         ecx,dword ptr [pchild(418548h)]

00413C0D 8B 42 04         mov         eax,dword ptr [edx+4]

00413C10 FF D0            call        eax 

00413C12 3B F4            cmp         esi,esp

00413C14 E8 59 D5 FFFF   call        @ILT+365(__RTC_CheckEsp) (411172h)

     pchild->parentb_f1();

00413C19 8B 0D 48 85 4100 mov         ecx,dword ptr [pchild (418548h)]

00413C1F 83 C1 0C        add         ecx,0Ch

00413C22 A1 48 85 41 00   mov         eax,dword ptr [pchild (418548h)]

00413C27 8B 50 0C        mov         edx,dword ptr[eax+0Ch]

00413C2A 8B F4            mov         esi,esp

00413C2C 8B 02            mov         eax,dword ptr [edx]

00413C2E FF D0            call        eax 

00413C30 3B F4            cmp         esi,esp

00413C32 E8 3B D5 FFFF   call        @ILT+365(__RTC_CheckEsp) (411172h)

     pchild->parentb_f2();

00413C37 8B 0D 48 85 4100 mov         ecx,dword ptr [pchild(418548h)]

00413C3D 83 C1 0C        add         ecx,0Ch

00413C40 A1 48 85 41 00   mov         eax,dword ptr [pchild (418548h)]

00413C45 8B 50 0C        mov         edx,dword ptr [eax+0Ch]

00413C48 8B F4            mov         esi,esp

00413C4A 8B 42 04         mov         eax,dword ptr [edx+4]

00413C4D FF D0            call        eax 

00413C4F 3B F4            cmp         esi,esp

00413C51 E8 1C D5 FF FF   call       @ILT+365(__RTC_CheckEsp)(411172h)

从上面的代码可以发现,当调用父类A的虚函数时,使用的是第一个vtbl的指针,而在调用父类B的虚函数时,使用的则是第二个vtbl的指针。

还有很重要的一点,vs是通过ecx这个寄存器来传递this指针的,上面的汇编代码表明,当调用父类A的虚函数时,this指针将指向子类的首地址,当调用父类B的虚函数时,this指针将指向存放第二个vtbl的地址,也就是父类A成员结束的位置。

跟踪可以看到,调用parenta_a时的this指针值为0x0041854c,而调用parentb_a时的this指针值为0x00418558。从前面&child的内容可以得到印证。

1.1.3  当两个父类有同名的虚函数

一个很有意思的问题,既然调用父类A的虚函数时传递的this指针和调用父类B的虚函数时传递的this指针不一样,那么假如父类A和父类B拥有同名的虚函数,vs又该如何处理呢?

试试将CParentB::parentb_f1的名称改为CParentB::parenta_f1,编译:

     pchild = &child;

00413BDA C7 05 48 85 41 00 4C 85 41 00 mov         dword ptr [pchild (418548h)],offsetchild (41854Ch)

     pchild->parenta_f1();

00413BE4 A1 48 85 41 00  mov         eax,dword ptr [pchild(418548h)]

00413BE9 8B 10            mov         edx,dword ptr [eax]

00413BEB 8B F4            mov         esi,esp

00413BED 8B 0D 48 85 41 00mov         ecx,dword ptr [pchild(418548h)]

00413BF3 8B 02            mov         eax,dword ptr [edx]

00413BF5 FF D0            call        eax 

00413BF7 3B F4            cmp         esi,esp

00413BF9 E8 74 D5 FF FF   call       @ILT+365(__RTC_CheckEsp)(411172h)

显然,这个时候传递的是child的首地址。

 

 

 

 

 

C++对象内存布局(5)

再看看vtbl的其它内容,先写段代码:

class CParent

{

public:

     int parent_a;

     int parent_b;

 

public

     virtual void parent_f1() {}

     virtual void parent_f2() {}

 

public

     void parent_f3() {}

     void parent_f4() {}

};

 

class CChild :public CParent

{

public:

     int child_a;

     int child_b;

 

public:      //子类的函数

     virtual void child_f1() {}

     virtual void child_f2() {}

 

public // 不可重载的函数

     void child_f3() {}

     void child_f4() {}

 

public:      //重载父类的函数

     virtual void parent_f2() {}

     virtual void parent_f1() {}

};

CChild child, *pchild;

CParent parent, *pparent;

这是一个单继承的关系。

1.1.1  Vtbl的属性

Vtbl应该是在链接时确定,之后就不再改变,那么它应该具有只读的属性,先对此做个验证:

     MEMORY_BASIC_INFORMATION mi;

     ::VirtualQueryEx(::GetCurrentProcess(), (void*)(*(unsignedint*)&parent),&mi, sizeof(mi));

这段代码将查询vtbl所在内存块的属性,返回的mi值如下:

              BaseAddress   0x00416000 ___xc_a     void *

              AllocationBase 0x00400000    void *

              AllocationProtect    0x00000080    unsigned long

              RegionSize      0x00002000    unsigned long

              State       0x00001000    unsigned long

              Protect    0x00000002    unsigned long

              Type       0x01000000    unsigned long

Protect这个成员的值表明vtbl所在的内存块具有PAGE_READONLY的属性,MSDN这样解释这个属性的意义:

Enables read access to thecommitted region of pages. An attempt to write to the committed region resultsin an access violation. If the system differentiates between read-only accessand execute access, an attempt to execute code in the committed region resultsin an access violation.

因而vtbl的内容将不可更改。

1.1.2  单继承时vtbl的内容

那么一个vtbl究竟应该有多大?先看&parent的vtbl指针,它的值为0x00416868,看这块内存的内容:

0x00416868  c810 41 00 1e 10 41 00 00 00 00 00 54 76 41 00 ..A...A.....TvA.

它的两个指针0x004110c8和0x0041101e分别指向下面的函数:

CParent::parent_f1:

004110C8 E9 33 08 0000   jmp         CParent::parent_f1 (411900h)

CParent::parent_f2:

0041101E E9 1D 09 00 00   jmp        CParent::parent_f2 (411940h)

指向父类的两个虚函数。

再看&child的vtbl指针值为0x00416850,看它的内容:

0x00416850  d1 11 41 00 82 10 41 00 b3 11 41 00 0a 10 41 00  ..A...A...A...A.

0x00416860  00 00 00 00 3c 76 41 00 c810 41 00 1e 10 41 00  ....<vA...A...A.

这里有四个指针0x004111d1,0x00411082,0x004111b3和0x0041100a,看它们的内容:

CChild::parent_f1:

004111D1 E9 9A 06 00 00   jmp        CChild::parent_f1 (411870h)

CChild::parent_f2:

00411082 E9 A9 07 00 00  jmp         CChild::parent_f2(411830h)

CChild::child_f1:

004111B3 E9 F8 05 00 00  jmp         CChild::child_f1 (4117B0h)

CChild::child_f2:

0041100A E9 E1 07 0000   jmp         CChild::child_f2 (4117F0h)

这说明子类的VTBL前面的部分是和父类的VTBL一致的,只是它们指向的内容不一样而已。想想也是,如果我们把子类的指针赋给父类的指针再调用虚函数,此时编译器将按照父类中虚函数的偏移量来进行函数调用,如果子类的VTBL中的函数指针意义和父类不一致,那么调用自然失败。

因而可以得出结论,一个类的VTBL的大小等于父类的VTBL大小加上这个类中定义的虚函数个数。而非虚函数则与VTBL没有任何关系。

1.1.3  多重继承时VTBL的内容

将上面的代码改为多重继承:

class CParentA

{

public:

     int parenta_a;

     int parenta_b;

 

public

     virtual void parenta_f1() {}

     virtual void parenta_f2() {}

 

public

     void parenta_f3() {}

     void parenta_f4() {}

};

 

class CParentB

{

public:

     int parentb_a;

     int parentb_b;

 

public

     virtual void parentb_f1() {}

     virtual void parentb_f2() {}

 

public

     void parentb_f3() {}

     void parentb_f4() {}

};

 

class CChild :public CParentA,public CParentB

{

public:

     int child_a;

     int child_b;

 

public:      //子类的函数

     virtual void child_f1() {}

     virtual void child_f2() {}

 

public // 不可重载的函数

     void child_f3() {}

     void child_f4() {}

 

public:      //重载父类A的函数

     virtual void parenta_f2() {}

     virtual void parenta_f1() {}

 

public:      //重载父类B的函数

     virtual void parentb_f2() {}

     virtual void parentb_f1() {}

};

CChild child, *pchild;

我们只看子类的VTBL,从前面的文章可以知道,子类将有两个VTBL,其第一个VTBL指针值为0x0041685c,看它的内容:

0x0041685C 49 12 41 00 44 12 41 00 b3 11 41 00 0a10 41 00  I.A.D.A...A...A.

0x0041686C 00 00 00 00 00 00 00 00 54 76 41 00 f010 41 00  ........TvA...A.

它有四个指针:

00411249 E9 B2 06 00 00   jmp        CChild::parenta_f1 (411900h)

00411244 E9 67 06 00 00   jmp        CChild::parenta_f2 (4118B0h)

004111B3 E9 78 06 00 00   jmp        CChild::child_f1 (411830h)

0041100A E9 61 08 0000   jmp         CChild::child_f2 (411870h)

和单继承时一样。

再看第二个VTBL的内容,其指针为0x00416850:

0x00416850  3a12 41 00 35 12 41 00 a0 7541 00 49 12 41 00  :.A.5.A..uA.I.A.

它有两个指针:

0041123A E9 41 05 0000   jmp         CChild::parentb_f1 (411780h)

00411235 E9 06 07 00 00   jmp        CChild::parentb_f2 (411940h)

紧接着这两个指针之后就是第一个VTBL的内容。在这二者之间还有一个指针0x004175a0,猜想和type info有关,因为将编译选项里的“生成运行时类型信息”关闭后这个指针将为NULL。

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值