谈到虚函数,我想很多朋友都应该知道虚函数表指针VPTR和虚函数表VTABLE,如果不清楚的朋友,建议先看看侯捷先生翻译的《深度探索C++对象模型》:)
刚开始的时候,我仅仅知道虚函数的多态机制是通过VPTR和VTABLE操控的,完全地相信书上所描述的,并没有亲自去证明过,或许是因为那时候我还没有接触到逆向分析吧:)
提几个问题:
1. VPTR的大小如何确定?
2. VPTR在类实例中的偏移值是多少?
3. VPTR是如何索引到需要调用的函数的?
4. VTABLE中函数的顺序是如何确定的?
了解上述问题的朋友,可以关闭这个窗口了:)
考虑如下代码:
#include
class Base
{
public:
int i;
char c;
double d;
Base()
{
i = 4;
c = 'A';
d = 2.0;
}
virtual void Virtual_Func_A()
{
printf("Virtual_Func_A()\n");
}
virtual void Virtual_Func_B()
{
printf("Virtual_Func_B()\n");
}
};
int main(void)
{
Base* b = new Base;
b->Virtual_Func_A();
b->Virtual_Func_A();
return 0;
}
分析工具:VC 6.0
分析过程:
在解答这个问题前,读者有必要了解一下“数据对齐”的概念,如果不了解的读者,可以参考下:)
http://blog.csdn.net/yeweiouyang/article/details/8636458
注意,Inter cpu 采用的是小端法
VPTR对程序员来说是隐形的,在Win32平台下,不考虑数据对齐的情况,VPTR占用4个字节,用于保存VTABLE的地址
考虑如下程序:
#include
class Base
{
public:
virtual void Common_Func()
{
printf("Base::Common_Func()\n");
}
virtual void Base_Func()
{
printf("Base::Func()\n");
}
};
class Derived : public Base
{
public:
virtual void Common_Func()
{
printf("Derived::Common_Func()\n");
}
virtual void Derived_Func()
{
printf("Derived::Func()\n");
}
};
int main(void)
{
Base* b = new Base;
b->Common_Func();
b->Base_Func();
Derived* d = new Derived;
d->Common_Func();
d->Base_Func();
d->Derived_Func();
return 0;
}
分析工具:IDA Pro
静态反汇编:
.text:00401000 push esi
.text:00401001 push 4 ; 为 Base::VPTR 申请 4 个字节的栈空间
.text:00401003 call ??2@YAPAXI@Z ; operator new(uint)
.text:00401008 add esp, 4
.text:0040100B test eax, eax
.text:0040100D jz short loc_401019
.text:0040100F mov dword ptr [eax], offset Base_VPTR ; *b 的堆空间放入 Base::VPTR
.text:00401015 mov esi, eax
.text:00401017 jmp short loc_40101B
对应的offset Base_VPTR:
.rdata:004060BC Base_VPTR dd offset Base_Common_Func ; DATA XREF: _main+Fo
.rdata:004060C0 dd offset Base_Func
.rdata:004060C4 align 8
不难发现Base_VPTR正是VTABLE的首地址,即Base::VPTR所指向之处
由这一句:
.text:0040100F mov dword ptr [eax], offset Base_VPTR ;
不难发现,VPTR存放的位置是Base类实例b的开始处,即偏移值为0
.text:00401019
.text:00401019 loc_401019: ; CODE XREF: _main+Dj
.text:00401019 xor esi, esi
.text:0040101B
.text:0040101B loc_40101B: ; CODE XREF: _main+17j
.text:0040101B mov eax, [esi] ; eax = Base::VTABLE
.text:0040101D mov ecx, esi
.text:0040101F call dword ptr [eax] ; call Base::Common_Func()
.text:00401021 mov edx, [esi] ; edx = Base::VTABLE
.text:00401023 mov ecx, esi
.text:00401025 call dword ptr [edx+4] ; call Base::Base_Func()
.text:00401028 push 4 ; 为 Derived::VPTR 申请 4 个字节的栈空间
.text:0040102A call ??2@YAPAXI@Z ; operator new(uint)
.text:0040102F add esp, 4
.text:00401032 test eax, eax
.text:00401034 jz short loc_401040
.text:00401036 mov dword ptr [eax], offset Derived_VPTR ; *d 的堆空间放入 Derived_VPTR
.text:0040103C mov esi, eax
.text:0040103E jmp short loc_401042
.text:00401040 ; ---------------------------------------------------------------------------
.text:00401040
.text:00401040 loc_401040: ; CODE XREF: _main+34j
.text:00401040 xor esi, esi
.text:00401042
.text:00401042 loc_401042: ; CODE XREF: _main+3Ej
.text:00401042 mov eax, [esi] ; eax = Derived::VTABLE
.text:00401044 mov ecx, esi
.text:00401046 call dword ptr [eax] ; call Derived::Common_Func()
.text:00401048 mov edx, [esi] ; edx = Derived::VTABLE
.text:0040104A mov ecx, esi
.text:0040104C call dword ptr [edx+4] ; call Base_Func(),Base_Func()是从Base类中继承而来的
.text:0040104F mov eax, [esi] ; eax = Derived::VTABLE
.text:00401051 mov ecx, esi
.text:00401053 call dword ptr [eax+8] ; call Derived::Derived_Func()
.text:00401056 xor eax, eax
.text:00401058 pop esi
.text:00401059 retn
.text:00401059 _main endp
对应的Derived_VPTR:
.rdata:004060B0 Derived_VPTR dd offset Derived_Common_Func ; DATA XREF: _main+36o
……
(IDA 并没有将所有函数指针显示出来)
经过分析,Base和Derived的VTABLE结构大致如下:
在VTABLE中保存有函数的地址(即函数指针),而非函数本身或函数名,在Win32平台下,函数指针占用4个字节,当通过类实例调用虚函数时,VPTR就会在VTABLE中进行索引,若找到对应的虚函数,就会取出函数指针并进行调用