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