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指針而已。

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