在網上看到乙個非常熱的帖子,裡面是這樣的乙個問題:
在列印的時候發現pfun的位址和 &(base::f)的位址竟然不一樣太奇怪了?經過一番深入研究,終於把這個問題弄明白了。下面就來一步步進行剖析。
根據vc的虛函式的布局機制,上述的布局如下:
然後我們再細細的分析第一種方式:
fun pfun = (fun)*((int*)*(int*)(d)+0);
d是乙個類物件的位址。而在32位機上指標的大小是4位元組,因此*(int*)(&d)取得的是vfptr,即虛表的位址。從而*((int*)*(int*)(&d)+0)是虛表的第1項,也就是base::f()的位址。事實上我們得到了驗證,程式執行結果如下:
這說明虛表的第一項確實是虛函式的位址,上面的vc虛函式的布局也確實木有問題。
但是,接下來就引發了乙個問題,為什麼&(base::f)和pfun的值會不一樣呢?既然pfun的值是虛函式f的位址,那&(base::f)又是什麼呢?帶著這個問題,我們進行了反彙編。
printf("&(base::f): 0x%x \n", &(base::f));
00401068 mov edi,dword ptr [__imp__printf (4020d4h)]
0040106e push offset base::`vcall'' (4013a0h)
00401073 push offset string "&(base::f): 0x%x \n" (40214ch)
00401078 call edi
printf("&(base::g): 0x%x \n", &(base::g));
0040107a push offset base::`vcall'' (4013b0h)
0040107f push offset string "&(base::g): 0x%x \n" (402160h)
00401084 call edi
那麼從上面我們可以清楚的看到:
base::f 對應於base::`vcall'' (4013a0h)
base::g對應於base::`vcall'' (4013b0h)
那麼base::`vcall''和base::`vcall''到底是什麼呢,繼續進行反彙編分析
base::`vcall'':
004013a0 mov eax,dword ptr [ecx]
004013a2 jmp dword ptr [eax]
......
base::`vcall'':
004013b0 mov eax,dword ptr [ecx]
004013b2 jmp dword ptr [eax+4]
第一句中, 由於ecx是this指標, 而在vc中一般虛表指標是類的第乙個成員, 所以它是把vfptr, 也就是虛表的位址存到了eax中. 第二句
相當於取了虛表的某一項。對於base::f跳轉到base::`vcall'',取了虛表的第1項;對於base::g跳轉到base::`vcall'',取了虛表第2項。由此都能夠正確的獲得虛函式的位址。
由此我們可以看出,vc對此的解決方法是由編譯器加入了一系列的內部函式"vcall". 乙個類中的每個虛函式都有乙個唯一與之對應的vcall函式,通過特定的vcall函式跳轉到虛函式表中特定的表項。
更深一步的進行討論,考慮多型的情況,將**改寫如下:
列印的時候表現出來了多型的性質:
分析可知原因如下:
這是因為類derive的虛函式表的各項對應的值進行了改寫(rewritting),原來指向based::f()的位址變成了指向derive::f(),原來指向based::g()的位址現在編變成了指向derive::g()。
反彙編**如下:
printf("&(derive::f): 0x%x \n", &(derive::f));
00401086 push offset base::`vcall'' (4013b0h)
0040108b push offset string "&(derive::f): 0x%x \n" (40217ch)
00401090 call esi
printf("&(derive::g): 0x%x \n", &(derive::g));
00401092 push offset base::`vcall'' (4013c0h)
00401097 push offset string "&(derive::g): 0x%x \n" (402194h)
0040109c call esi
因此雖然此時derive::f依然對應base::`vcall'',而derive::g依然對應base::`vcall'',但是由於每個類有乙個虛函式表,因此跳轉到的虛表的位置也發生了改變,同時因為進行了改寫,虛表中的每個slot項的值也不一樣。
稍微總結一下:
在vc中有兩種方法呼叫虛函式,一種是通過虛表,另外一種是通過vcall thunk的方式
通過虛表的方式:
base *d = new derive;
d->f();
004115fa mov eax,dword ptr [d]
004115fd mov edx,dword ptr [eax]
004115ff mov esi,esp
00411601 mov ecx,dword ptr [d]
00411604 mov eax,dword ptr [edx]
00411606 call eax
00411608 cmp esi,esp
0041160a call @ilt+470(__rtc_checkesp) (4111dbh)
這種方式的應用環境是通過類物件的指標或引用來呼叫虛函式
通過vcall thunk的方式:
typedef void (base::* func1)( void );
base *d = new derive;
func1 pfun1 = &base::f;
(d->*pfun1)();
004115a9 mov dword ptr [pfun1],offset base::`vcall'' (4110c3h)
004115b0 mov esi,esp
004115b2 lea ecx,[d]
004115b5 call dword ptr [pfun1]
004115b8 cmp esi,esp
004115ba call @ilt+460(__rtc_checkesp) (4111d1h)
is uploaded file函式引發的問題
起因 在利用moophp的乙個專案中,接到使用者反饋說其所有客戶不能上傳檔案,都返回失敗。經過排查發現是php中的is uploaded file函式在 搗鬼。細節分析 在正常情況下,通過php 上傳檔案 需要通過is uploaded file函式來判斷檔案是否是通過 http post 上傳的,...
C 虛函式表的布局
針對虛函式表的結構與布局,寫了乙個程式驗證一下 首先看單一繼承的情況 class base virtual void y virtual void z class derive public base,public base2 重寫 virtual void y1 virtual void y 重寫...
理解虛基類 虛函式與純虛函式的概念
虛基類在說明其作用前先看一段 從 中可以看出類b c都繼承了類a的ivalue成員,因此類b c都有乙個成員變數ivalue 而類d又繼承了b c,這樣類d就有乙個重名的成員 ivalue 乙個是從類b中繼承過來的,乙個是從類c中繼承過來的 在主函式中呼叫d.ivalue 因為類d有乙個重名的成員i...