本篇文章大概从三个角度解析虚函数表 :
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个字节 , 这四个字节在该类对象的首四个字节 . 这四个字节存储的即是虚函数表的地址 ;
当虚函数个数增加时,类的大小不再增加,增加的是虚函数表中的函数地址 .
提出一个问题 :为什么虚函数采用的是间接调用方式?
虚函数本就是为继承而生的,失去了继承,利用虚函数实现多态将毫无意义.提出的问题会在后续连载中解答.
本文有些啰嗦 , 只是希望路过的朋友能够有一点点的收获 ...