C++虚函数表深入解析 (一)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/jxz_dz/article/details/47998869

本篇文章大概从三个角度解析虚函数表 :

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个字节 , 这四个字节在该类对象的首四个字节 . 这四个字节存储的即是虚函数表的地址 ;

当虚函数个数增加时,类的大小不再增加,增加的是虚函数表中的函数地址 .


提出一个问题 :为什么虚函数采用的是间接调用方式?

虚函数本就是为继承而生的,失去了继承,利用虚函数实现多态将毫无意义.提出的问题会在后续连载中解答.

本文有些啰嗦 , 只是希望路过的朋友能够有一点点的收获 ...


猜你喜欢

转载自blog.csdn.net/jxz_dz/article/details/47998869