本篇文章大概從三個角度解析虛函數表 :
A : 虛函數調用方式
B : 深入解析虛函數
C : 打印虛函數表
有問題一起交流 !
A : 虛函數調用方式
關於函數調用方式,在此指的是直接調用與間接調用 , 即Call rel16/32 ( 其opcode E8 ... )或者 call [ rel16/32 ] ( 其opcode FF ...) .
具體call指令請參考: http://blog.ftofficer.com/2010/04/n-forms-of-call-instructions/
注 : 此處只涉及到近調用
測試代碼 :
#include "stdafx.h"
class CBase
{
public:
int x ;
int y ;
virtual void Fun1()
{
printf("Fun1\n");
}
};
int _tmain(int argc, _TCHAR* argv[])
{
CBase base1;
CBase* pb = &base1 ;
//利用對象直接調用成員函數
base1.Fun1();
//利用類的指針調用成員函數
pb->Fun1();
return 0;
}
代碼中中含有一個類CBase , 在main函數中定義了該類的對象和指針 ; 然後分別用兩種方式調用 :
重要的在反彙編代碼 :
19: int _tmain(int argc, _TCHAR* argv[])
20: {
000314E0 55 push ebp
000314E1 8B EC mov ebp,esp
000314E3 81 EC E4 00 00 00 sub esp,0E4h
000314E9 53 push ebx
000314EA 56 push esi
000314EB 57 push edi
000314EC 8D BD 1C FF FF FF lea edi,[ebp+FFFFFF1Ch]
000314F2 B9 39 00 00 00 mov ecx,39h
000314F7 B8 CC CC CC CC mov eax,0CCCCCCCCh
000314FC F3 AB rep stos dword ptr es:[edi]
000314FE A1 34 80 03 00 mov eax,dword ptr ds:[00038034h]
00031503 33 C5 xor eax,ebp
00031505 89 45 FC mov dword ptr [ebp-4],eax
21: CBase base1;
00031508 8D 4D EC lea ecx,[ebp-14h] /* 參數爲this指針 */
0003150B E8 58 FC FF FF call 00031168 /* 調用構造函數 */
22: CBase* pb = &base1 ;
00031510 8D 45 EC lea eax,[ebp-14h] /* 爲新創建的類指針分配空間並賦值爲該對象this指針(即該對象首個成員的地址) */
</span>00031513 89 45 E0 mov dword ptr [ebp-20h],eax
23: //利用對象直接調用成員函數</span>
24: base1.Fun1();
00031516 8D 4D EC lea ecx,[ebp-14h] /* 參數爲this指針 */
</span>00031519 E8 90 FC FF FF call 000311AE
25: //利用類的指針調用成員函數</span>
26: pb->Fun1();
0003151E 8B 45 E0 mov eax,dword ptr [ebp-20h] /* [ebp-20h]存儲的是該對象的this指針 */
00031521 8B 10 mov edx,dword ptr [eax] /* 將該對象的首個成員存儲的EDX */
00031523 8B F4 mov esi,esp /* 檢查堆棧平衡時用的 ,不必深究 */
00031525 8B 4D E0 mov ecx,dword ptr [ebp-20h] /* 參數爲this指針 */
00031528 8B 02 mov eax,dword ptr [edx] /************** 將edx的值作爲地址,取四個字節放到eax **************/
0003152A FF D0 call eax
0003152C 3B F4 cmp esi,esp /* 檢查堆棧平衡時用的 ,不必深究 */
0003152E E8 21 FC FF FF call 00031154
27: return 0;
00031533 33 C0 xor eax,eax
28: }
通過以上代碼分析可得出結論 :
通過 對象 . 成員函數 的方式調用虛函數時使用的是直接調用方式(call rel32)
通過 指針->函數名 的方式調用虛函數時使用的是間接調用方式(call [rel32])
mov eax,dword ptr [edx] 這條指令中eax到底存放的是什麼呢 ? 現在給出答案 :虛函數表的第一個虛函數 . 詳細分析看第二模塊
爲了方便理解反彙編代碼 , 在此附上顯示符號的反彙編代碼 :
19: int _tmain(int argc, _TCHAR* argv[])
20: {
000314E0 55 push ebp
000314E1 8B EC mov ebp,esp
000314E3 81 EC E4 00 00 00 sub esp,0E4h
000314E9 53 push ebx
000314EA 56 push esi
000314EB 57 push edi
000314EC 8D BD 1C FF FF FF lea edi,[ebp-0E4h]
000314F2 B9 39 00 00 00 mov ecx,39h
000314F7 B8 CC CC CC CC mov eax,0CCCCCCCCh
000314FC F3 AB rep stos dword ptr es:[edi]
000314FE A1 34 80 03 00 mov eax,dword ptr ds:[00038034h]
00031503 33 C5 xor eax,ebp
00031505 89 45 FC mov dword ptr [ebp-4],eax
21: CBase base1;
00031508 8D 4D EC lea ecx,[base1]
0003150B E8 58 FC FF FF call CBase::CBase (031168h)
22: CBase* pb = &base1 ;
00031510 8D 45 EC lea eax,[base1]
00031513 89 45 E0 mov dword ptr [pb],eax
23: //利用對象直接調用成員函數
24: base1.Fun1();
00031516 8D 4D EC lea ecx,[base1]
00031519 E8 90 FC FF FF call CBase::Fun1 (0311AEh)
25: //利用類的指針調用成員函數
26: pb->Fun1();
0003151E 8B 45 E0 mov eax,dword ptr [pb]
00031521 8B 10 mov edx,dword ptr [eax]
00031523 8B F4 mov esi,esp
00031525 8B 4D E0 mov ecx,dword ptr [pb]
00031528 8B 02 mov eax,dword ptr [edx]
0003152A FF D0 call eax
0003152C 3B F4 cmp esi,esp
0003152E E8 21 FC FF FF call __RTC_CheckEsp (031154h)
27: return 0;
00031533 33 C0 xor eax,eax
28: }
B : 深入解析虛函數表
此模塊我們主要探究什麼是虛函數表 , 虛函數表的位置 .
測試代碼1 :
#include "stdafx.h"
class CBase
{
public:
//構造函數
CBase()
{
x = 1;
y = 2;
}
int x ;
int y ;
};
int _tmain(int argc, _TCHAR* argv[])
{
CBase base1 ;
printf("%d\n",sizeof(CBase));
return 0;
}
在類中沒有定義虛函數 , 只有兩個成員變量
輸出結果 : 8
#include "stdafx.h"
class CBase
{
public:
CBase()
{
x = 1;
y = 2;
}
int x ;
int y ;
virtual void Fun1()
{
printf("Fun1");
}
};
int _tmain(int argc, _TCHAR* argv[])
{
CBase base1 ;
printf("%d\n",sizeof(CBase));
return 0;
}
在類中定義了一個虛函數 , 兩個成員變量 輸出結果 : 12
多出了四個字節 , 如果我們定義兩個虛函數呢 ? 該類的大小是多少呢 ?
#include "stdafx.h"
class CBase
{
public:
CBase()
{
x = 1;
y = 2;
}
int x ;
int y ;
virtual void Fun1()
{
printf("Fun1");
}
virtual void Fun2()
{
printf("Fun2");
}
};
int _tmain(int argc, _TCHAR* argv[])
{
CBase base1 ;
printf("%d\n",sizeof(CBase));
return 0;
}
在類中定義了一個虛函數 , 兩個成員變量 輸出結果 : 12 仍然是12個字節
可以繼續增加虛函數的個數 , 可以發現該類的大小仍然是12個字節 , 由此可以引發兩個問題 :
1. 多出來的四個字節是存儲什麼的 ?
2. 爲什麼虛函數的個數增加時 , 類的大小不再發生變化 ?
如何解決這兩個問題呢 ? 對於第一個問題簡單 , 看下內存不就知道了
看到對象base1的空間內,第一個成員不知道什麼東東(0x0133585c) , 第二個第三個成員分別是 x , y ;後面cccccccc就不是了, 只有12個字節大小不是麼 ?
我們接着看第二幅圖 :
很明顯 : 0x0133585c這個地址中存放的是兩個很規律的 "數" .
我們不難推測 : 這兩個" 數"應該是兩個虛函數的地址 .
上面這句話 mov eax,dword ptr [edx] 這條指令中eax到底存放的是什麼呢 ? 現在給出答案 :虛函數表的第一個虛函數 . 詳細分析看第二模塊
現在應該有答案了吧 .
神馬?? 還不清楚? 好吧 , 那我們就手動調用這些虛函數來印證一下. 搞起
測試代碼 :
#include "stdafx.h"
class CBase
{
public:
//構造函數
CBase()
{
x = 1;
y = 2;
}
int x ;
int y ;
virtual void Fun1()
{
printf("Fun1\n");
}
virtual void Fun2()
{
printf("Fun2\n");
}
virtual void Fun3()
{
printf("Fun3\n");
}
};
int _tmain(int argc, _TCHAR* argv[])
{
CBase base1 ;
//查看base1的虛函數表
//&base1即this指針,第一個成員即虛函數表的位置
printf("0x%x\n",&base1);
//定義函數指針
typedef void(*pFunction)(void);
//循環調用三個虛函數
for (int i = 0;i<3;i++)
{
int ptemp = *((int*)*(int*)&base1+i);
pFunction pFun = (pFunction)ptemp;
pFun();
}
return 0;
}
結果如圖 :
好的,至此我們可以總結出 :
當有虛函數存在時 , 類的大小增加4個字節 , 這四個字節在該類對象的首四個字節 . 這四個字節存儲的即是虛函數表的地址 ;
當虛函數個數增加時,類的大小不再增加,增加的是虛函數表中的函數地址 .
提出一個問題 :爲什麼虛函數採用的是間接調用方式?
虛函數本就是爲繼承而生的,失去了繼承,利用虛函數實現多態將毫無意義.提出的問題會在後續連載中解答.
本文有些囉嗦 , 只是希望路過的朋友能夠有一點點的收穫 ...