c++多继承条件下的虚函数列表及其内容

首先还是简单定义三个类

  1. class A  
  2. {  
  3. private:  
  4.     int ma;  
  5.     int mb;  
  6. public:  
  7.     A()  
  8.     {  
  9.         ma = 1;  
  10.         mb = 2;  
  11.     }  
  12.       
  13.     virtual void Name()  
  14.     {  
  15.         cout<<"this is A"<<endl;      
  16.     }  
  17.   
  18.     virtual void special()  
  19.     {  
  20.         cout<<"Hello kitty"<<endl;  
  21.     }  
  22.   
  23.     virtual ~A()  
  24.     {  
  25.         cout<<"this is the virtual function of A"<<endl;  
  26.     }  
  27. };  
  28.   
  29.   
  30. class B  
  31. {  
  32. private:  
  33.     int mc;  
  34. public:  
  35.     B()  
  36.     {  
  37.         mc = 3;  
  38.     }  
  39.   
  40.       
  41.     virtual void Name()  
  42.     {  
  43.         cout<<"this is B"<<endl;      
  44.     }  
  45.   
  46.       
  47.     virtual void special()  
  48.     {  
  49.             cout<<"Hello Motor"<<endl;  
  50.     }  
  51.       
  52.   
  53.     virtual ~B()  
  54.     {  
  55.         cout<<"this is the virtual function of B"<<endl;  
  56.     }  
  57. };  
  58.   
  59. class C:public A,public B  
  60. {  
  61. public:  
  62.     C()  
  63.     {  
  64.   
  65.     }  
  66.     virtual void special()  
  67.     {  
  68.         cout<<"hello world"<<endl;  
  69.     }  
  70.   
  71.      
  72.     virtual ~C()  
  73.     {  
  74.         cout<<"this is the virtual function of C"<<endl;  
  75.     }  
  76.   
  77. };  

然后是测试函数

  1. int experimentClassUpDownPointer()  
  2. {  
  3.     C c;  
  4.     A *a;  
  5.     B *b;  
  6.   
  7.     a = &c;  
  8.     b = &c;  
  9.     a->Name();  
  10.     b->Name();  
  11.       
  12.     return 1;  
  13. }  

首先进入c的构造函数体内,看看它的反汇编代码是什么

去除了初始化和一些校验代码后

  1. 002E2075  push        eax    
  2. 002E2076  lea         eax,[ebp-0Ch]    
  3. 002E2079  mov         dword ptr fs:[00000000h],eax    
  4. 002E207F  mov         dword ptr [ebp-14h],ecx    
  5. 002E2082  mov         ecx,dword ptr [ebp-14h]    
  6. 002E2085  call        A::A (2E1208h)    
  7. 002E208A  mov         dword ptr [ebp-4],0    
  8. 002E2091  mov         ecx,dword ptr [ebp-14h]    
  9. 002E2094  add         ecx,0Ch    
  10. 002E2097  call        B::B (2E11F9h)    
  11. 002E209C  mov         eax,dword ptr [ebp-14h]    
  12. 002E209F  mov         dword ptr [eax],offset C::`vftable' (2EB944h)    
  13. 002E20A5  mov         eax,dword ptr [ebp-14h]    
  14. 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的构造函数前

  1. 002E2091  mov         ecx,dword ptr [ebp-14h]    
  2. 002E2094  add         ecx,0Ch   

第一句是恢复ecx指向对象c,然后又向后移动12个字节,因为一个虚指针和两个整型变量共占有12个字节


完整代码如下

  1. class B  
  2. {  
  3. private:  
  4.     int mc;  
  5. public:  
  6.     B()  
  7. 002E2100  push        ebp    
  8. 002E2101  mov         ebp,esp    
  9. 002E2103  sub         esp,0CCh    
  10. 002E2109  push        ebx    
  11. 002E210A  push        esi    
  12. 002E210B  push        edi    
  13. 002E210C  push        ecx    
  14. 002E210D  lea         edi,[ebp-0CCh]    
  15. 002E2113  mov         ecx,33h    
  16. 002E2118  mov         eax,0CCCCCCCCh    
  17. 002E211D  rep stos    dword ptr es:[edi]    
  18.   
  19. 002E211F  pop         ecx    
  20. 002E2120  mov         dword ptr [ebp-8],ecx    
  21. 002E2123  mov         eax,dword ptr [this]    
  22. 002E2126  mov         dword ptr [eax],offset B::`vftable' (2EB958h)    
  23.     {  
  24.         mc = 3;  
  25. 002E212C  mov         eax,dword ptr [this]    
  26. 002E212F  mov         dword ptr [eax+4],3    
  27.     }  
  28. 002E2136  mov         eax,dword ptr [this]    
  29. 002E2139  pop         edi    
  30. 002E213A  pop         esi    
  31. 002E213B  pop         ebx    
  32. 002E213C  mov         esp,ebp    
  33. 002E213E  pop         ebp    
  34. 002E213F  ret    

002E211D之前是初始化,忽略。直接从下一行开始看,从内容上看与单继承基本一直,但是不要忘了此时thisecx都指向的是对象c往后便宜十二个字节处。

那么也就是说在多继承的时候,子类会分别给父类开辟空间进行存储,如果父类都有虚指针,那么有多少个父类,基类中就会有多少个虚指针。同时从整体上看,他们又是连续存储的。存储完父类1的变量后,紧接着存储父类2

理解以上后,其他的也就没什么了,要提一下的是,与父类A的构造函数类似,B同样将自己的虚函数列表的地址写入虚指针2


回到c的构造函数,看看又会发生什么。

  1. 002E209C  mov         eax,dword ptr [ebp-14h]    
  2. 002E209F  mov         dword ptr [eax],offset C::`vftable' (2EB944h)    
  3. 002E20A5  mov         eax,dword ptr [ebp-14h]    
  4. 002E20A8  mov         dword ptr [eax+0Ch],offset C::`vftable' (2EB930h)   

在单继承的时候说过,c会用它的虚函数列表重写虚函数指针。同样都是父类,那么父类B也不会有特殊。

ebp-14h处的内存存储了对象的地址还记得吧,那么这就很清楚了,ecxecx+0ch分别指向两个虚函数指针,然后分别用不同的地址重新赋值。




谈完虚继承条件下关于虚函数和成员变量如何存储的问题,接下来看看所谓虚函数列表究竟是怎么回事。

  1. A *a;  
  2.     B *b;  
  3.   
  4.     a = &c;  
  5. 002E1F3C  lea         eax,[ebp-24h]    
  6. 002E1F3F  mov         dword ptr [ebp-30h],eax    

上面说明对象c的起始地址是ebp-24h,刚才说了c的起始地址别存储在ebp-14h处,都用到了ebp,不要晕。此时的ebp是测试函数内的ebpebp-14h处的ebpC的构造函数体内的ebp,他们的值虽然可能是一样的。理解程序的时候最好根据函数看做不同的ebp.

然后完成ab的赋值

  1. a = &c;  
  2. 002E1F3C  lea         eax,[ebp-24h]    
  3. 002E1F3F  mov         dword ptr [ebp-30h],eax    
  4.     
  5.   b = &c;  
  6. 002E1F42  lea         eax,[ebp-24h]    
  7. 002E1F45  test        eax,eax    
  8. 002E1F47  je          experimentClassUpDownPointer+67h (2E1F57h)    
  9. 002E1F49  lea         ecx,[ebp-24h]    
  10. 002E1F4C  add         ecx,0Ch    
  11. 002E1F4F  mov         dword ptr [ebp-110h],ecx    
  12. 002E1F55  jmp         experimentClassUpDownPointer+71h (2E1F61h)    
  13. 002E1F57  mov         dword ptr [ebp-110h],0    
  14. 002E1F61  mov         edx,dword ptr [ebp-110h]    
  15. 002E1F67  mov         dword ptr [ebp-3Ch],edx    

接着看看如何调用虚函数。


  1. a->Name();  
  2. 002E1F6A  mov         eax,dword ptr [ebp-30h]    
  3. 002E1F6D  mov         edx,dword ptr [eax]    
  4. 002E1F6F  mov         esi,esp    
  5. 002E1F71  mov         ecx,dword ptr [ebp-30h]    
  6. 002E1F74  mov         eax,dword ptr [edx]    
  7. 002E1F76  call        eax    
  8. 002E1F78  cmp         esi,esp    
  9. 002E1F7A  call        @ILT+805(__RTC_CheckEsp) (2E132Ah)    
  10. 重点在  
  11. 002E1F6A  mov         eax,dword ptr [ebp-30h]    
  12. 002E1F6D  mov         edx,dword ptr [eax]    
  13. 和  
  14. 002E1F74  mov         eax,dword ptr [edx]    
  15. 002E1F76  call        eax   

刚才讲起始地址放入ebp-30h处,这个时候先将对象起始地址写入eax,然后取出四个字节的内容放入edx,那么edx里面是什么?当然就是第一个虚函数指针的值,也就是第一个虚函数列表的地址。此时edx

EDX = 002EB944

接着将edx指向的地址的四个字节写入eax,此时eax是什么?是第一个虚指针指向的虚函数列表的第一项,来看看究竟是什么。

  1. 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表。

看到

  1. A::Name:  
  2. 002E120D  jmp         A::Name (2E1CF0h)    

不急着进入函数体,我们看看虚函数表后面都指向什么,第二项是002e11a9

  1. C::special:  
  2. 002E11A9  jmp         C::special (2E2350h)   

第三项是002e119f

  1. C::`vector deleting destructor':  
  2. 002E119F  jmp         C::`scalar deleting destructor' (2E24D0h)   

c的析构函数。


继续第二个调用

  1. b->Name();  
  2. 002E1F7F  mov         eax,dword ptr [ebp-3Ch]    
  3. 002E1F82  mov         edx,dword ptr [eax]    
  4. 002E1F84  mov         esi,esp    
  5. 002E1F86  mov         ecx,dword ptr [ebp-3Ch]    
  6. 002E1F89  mov         eax,dword ptr [edx]    
  7. 002E1F8B  call        eax    
  8. 002E1F8D  cmp         esi,esp    
  9. 002E1F8F  call        @ILT+805(__RTC_CheckEsp) (2E132Ah)  


  1. 002E1F7F  mov         eax,dword ptr [ebp-3Ch]  

这个是什么呢?回看b的初始代码有这么几行

  1. 002E1F49  lea         ecx,[ebp-24h]    
  2. 002E1F4C  add         ecx,0Ch    
  3. 002E1F4F  mov         dword ptr [ebp-110h],ecx    
  4. 002E1F55  jmp         experimentClassUpDownPointer+71h (2E1F61h)  

完成校验后跳入

  1. 002E1F61  mov         edx,dword ptr [ebp-110h]    
  2. 002E1F67  mov         dword ptr [ebp-3Ch],edx    

这时候就很清楚了,ebp-3ch处存储的就是对象起始地址往后偏移12个字节后的地址。那么是什么呢?就是第二个虚指针的地址。


重新回到b->Name()的调用

首先取出ebp-3ch处的值,也就是虚指针的地址,放入eax,然后将虚指针的值,也就是虚函数列表的首地址放入edx,此时eaxedx的值如下。

  1. EAX = 001BF868 EBX = 7FFA01A2 ECX = 1043ED48 EDX = 002EB930  
  2.   
  3. 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

  1. B::Name:  
  2. 002E139D  jmp         B::Name (2E2150h)    

第二项是002e10dc

第三项是002e10be

这两项都不在ILT表中,那么为什么呢?

回过头看一下,C的对象应该包含多少个虚函数。

显然有Anamebname,还有Cspecial,z这个完成了对父类的覆盖,然后便是覆盖了父类的虚析构函数。这四个之前已经完全写入了两个虚函数列表,后面自然就没有了,但是指向什么呢,很简单做个实验即可。


已经知道了第二个虚函数列表的地址,这个实验就很简单了。代码

  1. typedef void (*funcF)();  
  2.     char *pt = (char *)&c;  
  3.     pt = pt+12;  
  4.     int **pointer;  
  5.     pointer = (int **)(pt);  
  6.     funcF funcT;  
  7.   
  8.     for(int i=0;i<3;i++)  
  9.     {  
  10.         funcT = (funcF)pointer[0][i];  
  11.         funcT();  
  12.     }  
结果:


虚表不存在对齐的问题。

单线多继承的时候,如C继承B,B继承A。那么C类的对象存在一个虚指针。



不负责任猜想“:

在运行过程中,每次进入一个基类的时候,ecx都会保存将要构建的基类对象的地址,其实是不是通过ecx来完成this指针的值得变化?

不过虽说是构造基类对象,只不过是为了说起来简单而已,可以看到编译过程中内存的地址只有一块,不存在分别申请空间,无论有多少个父类,都只有一块连续的存储空间。每次进入不同的父类的构造函数,变化的只有this指针而已。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章