每個(gè)含有虛函數(shù)的類有一張?zhí)摵瘮?shù)表(vtbl),表中每一項(xiàng)指向一個(gè)虛函數(shù)的地址,實(shí)現(xiàn)上是一個(gè)函數(shù)指針的數(shù)組。 虛函數(shù)表既有繼承性又有多態(tài)性。每個(gè)派生類的vtbl繼承了它各個(gè)基類的vtbl,如果基類vtbl中包含某一項(xiàng),則其派生類的vtbl中也將包含同樣的一項(xiàng),但是兩項(xiàng)的值可能不同。如果派生類重載(override)了該項(xiàng)對(duì)應(yīng)的虛函數(shù),則派生類vtbl的該項(xiàng)指向重載后的虛函數(shù),沒有重載的話,則沿用基類的值。 在類對(duì)象的內(nèi)存布局中,首先是該類的vtbl指針,然后才是對(duì)象數(shù)據(jù)。在通過對(duì)象指針調(diào)用一個(gè)虛函數(shù)時(shí),編譯器生成的代碼將先獲取對(duì)象類的vtbl指針,然后調(diào)用vtbl中對(duì)應(yīng)的項(xiàng)。對(duì)于通過對(duì)象指針調(diào)用的情況,在編譯期間無法確定指針指向的是基類對(duì)象還是派生類對(duì)象,或者是哪個(gè)派生類的對(duì)象。但是在運(yùn)行期間執(zhí)行到調(diào)用語句時(shí),這一點(diǎn)已經(jīng)確定,編譯后的調(diào)用代碼能夠根據(jù)具體對(duì)象獲取正確的vtbl,調(diào)用正確的虛函數(shù),從而實(shí)現(xiàn)多態(tài)性。分析一下這里的思想所在,問題的實(shí)質(zhì)是這樣,對(duì)于發(fā)出虛函數(shù)調(diào)用的這個(gè)對(duì)象指針,在編譯期間缺乏更多的信息,而在運(yùn)行期間具備足夠的信息,但那時(shí)已不再進(jìn)行綁定了,怎么在二者之間作一個(gè)過渡呢?把綁定所需的信息用一種通用的數(shù)據(jù)結(jié)構(gòu)記錄下來,該數(shù)據(jù)結(jié)構(gòu)可以同對(duì)象指針相聯(lián)系,在編譯時(shí)只需要使用這個(gè)數(shù)據(jù)結(jié)構(gòu)進(jìn)行抽象的綁定,而在運(yùn)行期間將會(huì)得到真正的綁定。這個(gè)數(shù)據(jù)結(jié)構(gòu)就是vtbl??梢钥吹?,實(shí)現(xiàn)用戶所需的抽象和多態(tài)需要進(jìn)行后綁定,而編譯器又是通過抽象和多態(tài)而實(shí)現(xiàn)后綁定的。 下面說一下多重繼承。多重繼承的兩個(gè)基類如果繼承了同一個(gè)類,則其派生類相當(dāng)于繼承了該類兩次,vtbl也繼承了兩次。對(duì)象布局中,該類的數(shù)據(jù)有兩份,vtbl指針有兩個(gè),分別指向兩次被繼承的vtbl。但派生類重載該類的虛函數(shù)時(shí)只能重載一次,那么重載后的函數(shù)地址將占據(jù)vtbl的哪個(gè)位置?通過寫程序測(cè)試,我覺得應(yīng)該是同時(shí)出現(xiàn)在所繼承的兩個(gè)vtbl的相應(yīng)位置,有待進(jìn)一步驗(yàn)證。 說到虛函數(shù)機(jī)制,對(duì)象指針的類型轉(zhuǎn)換也是要弄清的,這里就不說了。還有一個(gè)this指針的問題,提一下。虛函數(shù)調(diào)用的時(shí)候也是需要傳遞this指針的,這沒什么奇怪,但是這時(shí)的this指針就隱含著一個(gè)問題,它要和實(shí)際調(diào)用的虛函數(shù)相一致,即this指針也要實(shí)現(xiàn)多態(tài)性。在多重繼承的情況下,這個(gè)問題不是那么簡(jiǎn)單的,請(qǐng)參考[《C++語言的設(shè)計(jì)和演化》p203]。 C++虛函數(shù)表深度分析 昨天聽完彭老師的C++的講座,感覺很不錯(cuò),但之后留了一個(gè)疑問,就是關(guān)于虛函數(shù)表的機(jī)制,課下和彭老師的討論似乎也沒能完全解惑,我的疑問主要就是: 1:虛函數(shù)表到底是怎么工作的,for類,還是for對(duì)象 2:如果for類,那么基類和派生類是共用一表,還是各有各的表(物理上) 3:如果共用一表的話,總是后面的覆蓋前面的函數(shù)地址,那不是很容易出現(xiàn)混亂嗎? 帶著這三個(gè)疑問,趁著熱呼勁,我搜了搜關(guān)于虛函數(shù)表的DASM的文章,當(dāng)然了,能搜到的幾篇都是for VC編譯器的 初步得出了以前結(jié)論: 1:虛表(虛函數(shù)表)是for類的 2:基類和派生類是各有各的表,也就是說他們的物理地址是分開的,基類和派生類的虛表的唯一關(guān)聯(lián)是:當(dāng)派生類沒有實(shí)現(xiàn) 基類虛函數(shù)的重載時(shí),派生類會(huì)直接把自己表的該函數(shù)地址值寫為基類的該函數(shù)地址值. 3:任何一個(gè)有虛表的類,在實(shí)例化時(shí)不允許其虛表內(nèi)有項(xiàng)為空->純虛類不能初始化對(duì)象 4:帶虛表的類在對(duì)象構(gòu)造函數(shù)中,會(huì)把一個(gè)指針指向該類虛表地址,我在這給它起個(gè)名字叫vp; 5:僅對(duì)于VC和BC兩種編譯器論,如果該類帶有虛表,那么該類的對(duì)象的首地址就是虛表地址,也是this指針指向虛表 下面我就用IDE Borland C++ Builder 6.0 sp4,編譯器版本Borland C++ 5.5,來驗(yàn)證一下: 首先打開BCB6建立一個(gè)控制臺(tái)程序,寫上下面幾個(gè)備用類 #include <conio.h> #include <stdio.h> #pragma hdrstop #pragma argsused class A { public: __stdcall A() { } virtual void __stdcall output() { printf("Class An"); } virtual void __stdcall output2() { } }; class B :public A { public: void __stdcall output() { printf("Class Bn"); } }; class C:public A { public : void __stdcall output() { printf("Class Cn"); } }; 幾個(gè)類很簡(jiǎn)單,B和C是A的派生 下面先寫一個(gè)引子主程序,用來驗(yàn)證虛表的存在: int main(int argc, char* argv[]) { B b; printf("%d",sizeof(b)); } 結(jié)果是8 我把A類的兩個(gè)virtual都去掉后再運(yùn)行一次 結(jié)果是4 這說明了有virtual比沒virtual的對(duì)象多了32位,在win32中,32位正好是一個(gè)地址,那么這個(gè)地址就應(yīng)該指向的是虛表 看來虛表果然存在,那么虛表指針是在對(duì)象什么時(shí)候生成的呢?我改一下main函數(shù) int main(int argc, char* argv[]) { A *pa; B b; C c; A a; pa=&b; pa->output(); getch(); return 0; } 這應(yīng)該是一個(gè)很經(jīng)典的教科書上講多態(tài)的例子,如果有virtual輸出Class B,如果沒virtual輸出Class A 現(xiàn)在看一下這段代碼的反編譯代碼,我把BCB6的full debug模式打開,在 B b; 處設(shè)斷點(diǎn) 圖片 我們可以看到在b執(zhí)行完基類的構(gòu)造函數(shù)后,執(zhí)行了 mov edx,0x0040c114 mov [ebp-0x0x],edx 而這兩句話經(jīng)驗(yàn)證,在沒有virtual關(guān)鍵字時(shí)是沒有的,讓我們記住0x0040c114這個(gè)地址先 [ebp-0x0x]是this指針,我們目前猜測(cè)這段話就是把虛表的地址寫入this指針 我們?cè)倏碈 c;后的反編譯代碼 mov eax,0x0040c0f8 mov [ebp-0x14],eax 看來不同的類具有不同的虛表地址,也就是不同的類的表從物理上是不同的 我們現(xiàn)在來探討虛表工作的原理 我們對(duì)比一下pa->output()在有沒有virtual修飾時(shí)候的區(qū)別 mov eax,[ebp-0x04] push eax mov edx,[eax] call dword ptr [edx] 這是有virtual的 push dword ptr [ebp-0x04] call A::output(); 這是沒有virtual的 我們分析一下asm代碼,可以得出虛表的過程,先把根據(jù)this地址得到虛表地址,然后由虛表項(xiàng)里存放的函數(shù)指針地址,訪問 相應(yīng)的函數(shù),如果有多個(gè)虛函數(shù),且調(diào)用的是第N個(gè)虛函數(shù),那么上句call指令就會(huì)被更改為這樣的形式:call dword ptr [edx-4*(N-1)]) 一上是我們對(duì)dasm代碼做的一些推測(cè),一會(huì)兒我們還要進(jìn)一步驗(yàn)證這些 我們仔細(xì)看反編譯的結(jié)果,發(fā)現(xiàn)在A a;的dasm結(jié)果中,好象沒有vp初始化的一步,我查了其他文獻(xiàn)針對(duì)VC編譯器的dasm結(jié) 果,發(fā)現(xiàn)VC編譯器的dasm結(jié)果里是有初始化vp的一步的,類似 004010E8 mov dword ptr [eax],offset Derive::`vftable' (0042201c) 我現(xiàn)在就得出這樣一個(gè)結(jié)論,在BC編譯器中很可能對(duì)于基類的對(duì)象構(gòu)造函數(shù)作出了這樣的優(yōu)化,就是默認(rèn)把this指針指向 虛表地址,所以我們看不到這樣的dasm結(jié)果 我還發(fā)現(xiàn),對(duì)于類的構(gòu)造函數(shù)處理,VC和BC的編譯器也是不一樣的 如果我們?cè)陬惱锩鏇]有寫構(gòu)造函數(shù),VC會(huì)自動(dòng)為我們加一個(gè)構(gòu)造函數(shù),比如 class Base { public: void __stdcall Output() { printf("Class Basen"); } }; 我們得到這樣的dasm: 004010D9 pop ecx 004010DA mov dword ptr [ebp-4],ecx 004010DD mov ecx,dword ptr [ebp-4] 004010E0 call @ILT+30(Base::Base) (00401023) 可以看到自動(dòng)生成構(gòu)造函數(shù)地址 但在BC中,我們沒有看到這樣的代碼 當(dāng)我們把上面的A類里面的構(gòu)造函數(shù)刪去后,這是得到的A a;的dasm mov edx, 0x0040c0f0 mov [ebp-0x04],ecx 完全找不到構(gòu)造函數(shù)的影子,我猜測(cè)這也是編譯器對(duì)構(gòu)造函數(shù)所作出的優(yōu)化 我這里不評(píng)價(jià)兩種編譯器在這問題上的優(yōu)次,我繼續(xù)回到正題,驗(yàn)證我們的結(jié)論的正確性 因?yàn)榘凑瘴覀兊耐茰y(cè),0x0040c114就是虛表地址 那么按照此理,我們通過訪問虛表地址的內(nèi)容里的第一個(gè)函數(shù)地址,就能訪問output函數(shù),而虛表的地址就是this地址,是這樣 嗎,我再編了個(gè)main函數(shù) int main(int argc, char* argv[]) { A *pa; B b; C c; A a; //pa=&b; //pa->output(); //printf("%d",sizeof(b)); typedef void (__stdcall *PF)(void); void *pthis=&b; PF pf=(PF)(*(unsigned int*)pthis); printf("%x",pf); printf("n"); pf=(PF)(*(unsigned int*)pf); pf(); getch(); return 0; } 先來解釋一下這段代碼 typedef void (__stdcall *PF)(void); 聲明了配搭output的函數(shù)指針 void *pthis=&b; 用來得到b的this地址,它是指向虛表地址的 PF pf=(PF)(*(unsigned int*)pthis); 用來得到this地址的內(nèi)容,也就是虛表地址 然后我們把虛表地址輸出 pf=(PF)(*(unsigned int*)pf); 用來得到虛表里第一項(xiàng)的內(nèi)容,也就是output的地址(表第一項(xiàng)目地址=表地址) pf(); 調(diào)用函數(shù) 我們來看結(jié)果 成功了!!! 雖然我們沒有在代碼里寫output();但執(zhí)行結(jié)果就是輸出了output的結(jié)果 另外輸出的虛表地址就是0x0040c114,也就是我們最早推測(cè)的虛表地址!!! 我把代碼改下一下,按照我們的推測(cè),如果把表第一項(xiàng)地址偏移32位,應(yīng)該就是表第二項(xiàng)地址,而第二項(xiàng)的內(nèi)容就應(yīng)該是 output2的地址,驗(yàn)證一下: typedef void (__stdcall *PF)(void); void *pthis=&b; PF pf=(PF)(*(unsigned int*)pthis); printf("%x",pf); printf("n"); pf=(PF)(*( (unsigned int*)pf-0x04 ) ); pf(); 完全不出我們所料,輸出就是Class A output2 到這里,應(yīng)該對(duì)虛表的機(jī)制很清楚了,每個(gè)類都有各的虛表,每個(gè)類生成的各對(duì)象分別把this指向類的虛表地址,如果本類沒 有重載基類的虛函數(shù),那么虛表的該項(xiàng)會(huì)寫為基類的該項(xiàng)的內(nèi)容,在調(diào)用虛表的時(shí)候,會(huì)根據(jù)虛表地址做適當(dāng)?shù)钠埔缘玫? 相應(yīng)的虛函數(shù)地址,再進(jìn)行調(diào)用. 先分析到這,以后我會(huì)就修改虛表地址,以及如何應(yīng)用虛表做hook,繼續(xù)分析 |
|