- class A
- {
- private:
- int ma;
- int mb;
- public:
- A()
- {
- ma = 1;
- mb = 2;
- }
- virtual void Name()
- {
- cout<<"this is A"<<endl;
- }
- virtual void special()
- {
- cout<<"Hello kitty"<<endl;
- }
- virtual ~A()
- {
- cout<<"this is the virtual function of A"<<endl;
- }
- };
- class B
- {
- private:
- int mc;
- public:
- B()
- {
- mc = 3;
- }
- virtual void Name()
- {
- cout<<"this is B"<<endl;
- }
- virtual void special()
- {
- cout<<"Hello Motor"<<endl;
- }
- virtual ~B()
- {
- cout<<"this is the virtual function of B"<<endl;
- }
- };
- class C:public A,public B
- {
- public:
- C()
- {
- }
- virtual void special()
- {
- cout<<"hello world"<<endl;
- }
- virtual ~C()
- {
- cout<<"this is the virtual function of C"<<endl;
- }
- };
然后是测试函数
- int experimentClassUpDownPointer()
- {
- C c;
- A *a;
- B *b;
- a = &c;
- b = &c;
- a->Name();
- b->Name();
- return 1;
- }
首先进入c的构造函数体内,看看它的反汇编代码是什么
去除了初始化和一些校验代码后
- 002E2075 push eax
- 002E2076 lea eax,[ebp-0Ch]
- 002E2079 mov dword ptr fs:[00000000h],eax
- 002E207F mov dword ptr [ebp-14h],ecx
- 002E2082 mov ecx,dword ptr [ebp-14h]
- 002E2085 call A::A (2E1208h)
- 002E208A mov dword ptr [ebp-4],0
- 002E2091 mov ecx,dword ptr [ebp-14h]
- 002E2094 add ecx,0Ch
- 002E2097 call B::B (2E11F9h)
- 002E209C mov eax,dword ptr [ebp-14h]
- 002E209F mov dword ptr [eax],offset C::`vftable' (2EB944h)
- 002E20A5 mov eax,dword ptr [ebp-14h]
- 002E20A8 mov dword ptr [eax+0Ch],offset C::`vftable' (2EB930h)
然后画出我理解的关系图
读过反汇编的应该很清楚要特别列出ebp的原因,因为对函数初始申请的ebp—esp之间的内存进行操作时候,经常以ebp作为基准。
在这段代码中,对象的起始地址储存在epb-14h处,可以注意到,当需要寄存器指向对象时,常用此处的值来进行赋值。
A的反汇编代码就不再看了,和单继承时候一样,要注意到的是进入A的构造函数后其中的this指针依然指向对象c的起始地址,这个是与后面B的构造函数内的this不同的;再一个便是用A的虚函数列表的地址给c的虚函数指针赋值,这个已经讨论过,不再重复。
然后看在B的构造函数内发生了什么。首先注意到在进入B的构造函数前
- 002E2091 mov ecx,dword ptr [ebp-14h]
- 002E2094 add ecx,0Ch
第一句是恢复ecx指向对象c,然后又向后移动12个字节,因为一个虚指针和两个整型变量共占有12个字节
完整代码如下
- class B
- {
- private:
- int mc;
- public:
- B()
- 002E2100 push ebp
- 002E2101 mov ebp,esp
- 002E2103 sub esp,0CCh
- 002E2109 push ebx
- 002E210A push esi
- 002E210B push edi
- 002E210C push ecx
- 002E210D lea edi,[ebp-0CCh]
- 002E2113 mov ecx,33h
- 002E2118 mov eax,0CCCCCCCCh
- 002E211D rep stos dword ptr es:[edi]
- 002E211F pop ecx
- 002E2120 mov dword ptr [ebp-8],ecx
- 002E2123 mov eax,dword ptr [this]
- 002E2126 mov dword ptr [eax],offset B::`vftable' (2EB958h)
- {
- mc = 3;
- 002E212C mov eax,dword ptr [this]
- 002E212F mov dword ptr [eax+4],3
- }
- 002E2136 mov eax,dword ptr [this]
- 002E2139 pop edi
- 002E213A pop esi
- 002E213B pop ebx
- 002E213C mov esp,ebp
- 002E213E pop ebp
- 002E213F ret
002E211D之前是初始化,忽略。直接从下一行开始看,从内容上看与单继承基本一直,但是不要忘了此时this和ecx都指向的是对象c往后便宜十二个字节处。
那么也就是说在多继承的时候,子类会分别给父类开辟空间进行存储,如果父类都有虚指针,那么有多少个父类,基类中就会有多少个虚指针。同时从整体上看,他们又是连续存储的。存储完父类1的变量后,紧接着存储父类2。
理解以上后,其他的也就没什么了,要提一下的是,与父类A的构造函数类似,B同样将自己的虚函数列表的地址写入虚指针2。
回到c的构造函数,看看又会发生什么。
- 002E209C mov eax,dword ptr [ebp-14h]
- 002E209F mov dword ptr [eax],offset C::`vftable' (2EB944h)
- 002E20A5 mov eax,dword ptr [ebp-14h]
- 002E20A8 mov dword ptr [eax+0Ch],offset C::`vftable' (2EB930h)
在单继承的时候说过,c会用它的虚函数列表重写虚函数指针。同样都是父类,那么父类B也不会有特殊。
ebp-14h处的内存存储了对象的地址还记得吧,那么这就很清楚了,ecx和ecx+0ch分别指向两个虚函数指针,然后分别用不同的地址重新赋值。
谈完虚继承条件下关于虚函数和成员变量如何存储的问题,接下来看看所谓虚函数列表究竟是怎么回事。
- A *a;
- B *b;
- a = &c;
- 002E1F3C lea eax,[ebp-24h]
- 002E1F3F mov dword ptr [ebp-30h],eax
上面说明对象c的起始地址是ebp-24h,刚才说了c的起始地址别存储在ebp-14h处,都用到了ebp,不要晕。此时的ebp是测试函数内的ebp,ebp-14h处的ebp是C的构造函数体内的ebp,他们的值虽然可能是一样的。理解程序的时候最好根据函数看做不同的ebp.
然后完成a和b的赋值
- a = &c;
- 002E1F3C lea eax,[ebp-24h]
- 002E1F3F mov dword ptr [ebp-30h],eax
- b = &c;
- 002E1F42 lea eax,[ebp-24h]
- 002E1F45 test eax,eax
- 002E1F47 je experimentClassUpDownPointer+67h (2E1F57h)
- 002E1F49 lea ecx,[ebp-24h]
- 002E1F4C add ecx,0Ch
- 002E1F4F mov dword ptr [ebp-110h],ecx
- 002E1F55 jmp experimentClassUpDownPointer+71h (2E1F61h)
- 002E1F57 mov dword ptr [ebp-110h],0
- 002E1F61 mov edx,dword ptr [ebp-110h]
- 002E1F67 mov dword ptr [ebp-3Ch],edx
接着看看如何调用虚函数。
- a->Name();
- 002E1F6A mov eax,dword ptr [ebp-30h]
- 002E1F6D mov edx,dword ptr [eax]
- 002E1F6F mov esi,esp
- 002E1F71 mov ecx,dword ptr [ebp-30h]
- 002E1F74 mov eax,dword ptr [edx]
- 002E1F76 call eax
- 002E1F78 cmp esi,esp
- 002E1F7A call @ILT+805(__RTC_CheckEsp) (2E132Ah)
- 重点在
- 002E1F6A mov eax,dword ptr [ebp-30h]
- 002E1F6D mov edx,dword ptr [eax]
- 和
- 002E1F74 mov eax,dword ptr [edx]
- 002E1F76 call eax
刚才讲起始地址放入ebp-30h处,这个时候先将对象起始地址写入eax,然后取出四个字节的内容放入edx,那么edx里面是什么?当然就是第一个虚函数指针的值,也就是第一个虚函数列表的地址。此时edx是
EDX = 002EB944
接着将edx指向的地址的四个字节写入eax,此时eax是什么?是第一个虚指针指向的虚函数列表的第一项,来看看究竟是什么。
- 0x002EB944 0d 12 2e 00 a9 11 2e 00 9f 11 2e 00 00 00 00 00 34 ce 2e 00 9d 13 2e 00 3b 11 2e 00 5a 10 2e
第一项是
002e120d这也是即将调用的函数,接着进入ILT表。
看到
- A::Name:
- 002E120D jmp A::Name (2E1CF0h)
不急着进入函数体,我们看看虚函数表后面都指向什么,第二项是002e11a9
- C::special:
- 002E11A9 jmp C::special (2E2350h)
第三项是002e119f
- C::`vector deleting destructor':
- 002E119F jmp C::`scalar deleting destructor' (2E24D0h)
是c的析构函数。
继续第二个调用
- b->Name();
- 002E1F7F mov eax,dword ptr [ebp-3Ch]
- 002E1F82 mov edx,dword ptr [eax]
- 002E1F84 mov esi,esp
- 002E1F86 mov ecx,dword ptr [ebp-3Ch]
- 002E1F89 mov eax,dword ptr [edx]
- 002E1F8B call eax
- 002E1F8D cmp esi,esp
- 002E1F8F call @ILT+805(__RTC_CheckEsp) (2E132Ah)
- 002E1F7F mov eax,dword ptr [ebp-3Ch]
这个是什么呢?回看b的初始代码有这么几行
- 002E1F49 lea ecx,[ebp-24h]
- 002E1F4C add ecx,0Ch
- 002E1F4F mov dword ptr [ebp-110h],ecx
- 002E1F55 jmp experimentClassUpDownPointer+71h (2E1F61h)
完成校验后跳入
- 002E1F61 mov edx,dword ptr [ebp-110h]
- 002E1F67 mov dword ptr [ebp-3Ch],edx
这时候就很清楚了,ebp-3ch处存储的就是对象起始地址往后偏移12个字节后的地址。那么是什么呢?就是第二个虚指针的地址。
重新回到b->Name()的调用
首先取出ebp-3ch处的值,也就是虚指针的地址,放入eax,然后将虚指针的值,也就是虚函数列表的首地址放入edx,此时eax和edx的值如下。
- EAX = 001BF868 EBX = 7FFA01A2 ECX = 1043ED48 EDX = 002EB930
- 0x002EB930 9d 13 2e 00 dc 10 2e 00 be 10 2e 00 00 00 00 00 58 cd 2e 00 0d 12 2e 00 a9 11 2e 00 9f 11 2e
第一项是002e139d
进入ILT表
- B::Name:
- 002E139D jmp B::Name (2E2150h)
第二项是002e10dc
第三项是002e10be
这两项都不在ILT表中,那么为什么呢?
回过头看一下,C的对象应该包含多少个虚函数。
显然有A的name和b的name,还有C的special,z这个完成了对父类的覆盖,然后便是覆盖了父类的虚析构函数。这四个之前已经完全写入了两个虚函数列表,后面自然就没有了,但是指向什么呢,很简单做个实验即可。
已经知道了第二个虚函数列表的地址,这个实验就很简单了。代码
- typedef void (*funcF)();
- char *pt = (char *)&c;
- pt = pt+12;
- int **pointer;
- pointer = (int **)(pt);
- funcF funcT;
- for(int i=0;i<3;i++)
- {
- funcT = (funcF)pointer[0][i];
- funcT();
- }
虚表不存在对齐的问题。
单线多继承的时候,如C继承B,B继承A。那么C类的对象存在一个虚指针。
不负责任猜想“:
在运行过程中,每次进入一个基类的时候,ecx都会保存将要构建的基类对象的地址,其实是不是通过ecx来完成this指针的值得变化?
不过虽说是构造基类对象,只不过是为了说起来简单而已,可以看到编译过程中内存的地址只有一块,不存在分别申请空间,无论有多少个父类,都只有一块连续的存储空间。每次进入不同的父类的构造函数,变化的只有this指针而已。