C++虛函數表深入解析 (一)

本篇文章大概從三個角度解析虛函數表 :

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個字節 , 這四個字節在該類對象的首四個字節 . 這四個字節存儲的即是虛函數表的地址 ;

當虛函數個數增加時,類的大小不再增加,增加的是虛函數表中的函數地址 .


提出一個問題 :爲什麼虛函數採用的是間接調用方式?

虛函數本就是爲繼承而生的,失去了繼承,利用虛函數實現多態將毫無意義.提出的問題會在後續連載中解答.

本文有些囉嗦 , 只是希望路過的朋友能夠有一點點的收穫 ...


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