虚表和虚表指针
编译器:VS2015
0x01 基础概念
首先还是简单重复一下基础概念。
C++的多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。
1、多态性
指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。
a、编译时多态性:通过重载函数实现
b、运行时多态性:通过虚函数实现。
2、虚函数
虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态覆盖(Override)
3.虚函数表
编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。类的每个虚成员占据虚函数表中的一行。如果类中有N个虚函数,那么其虚函数表将有N*4(x64下是N*8)的大小。
派生类的虚函数表存放重写的虚函数,当基类的指针指向派生类的对象时,调用虚函数时都会根据vptr(虚表指针)来选择虚函数,而基类的虚函数在派生类里已经被改写或者说已经不存在了,所以也就只能调用派生类的虚函数版本了.
4.虚表指针
虚表指针在类对象中,每个同类对象中都有个一个vptr,指向内存中的vtable,所有同类对象,共享一个vtable,但是每个对象都自带一个vptr指向这个vtable,否则调用虚函数的时候会找不到正确的函数入口,(后面将会讲明)虚表指针是对象的第一个数据成员。
0x02 虚表和虚表指针的反汇编分析
我是直接通过VS2015的虚函数的反汇编指令来分析的。
先看源代码:
class CVirtual { public: virtual int GetNumber() { return m_nNumber; return 0; } virtual void SetNumber(int nNumber) { m_nNumber = nNumber; } private: int m_nNumber; }; int main() { int a = sizeof(CVirtual); CVirtual TheVirtual; return 0; }
反汇编:
f11进类对象的构造函数:
CVirtual::CVirtual: 00A316E0 55 push ebp 00A316E1 8B EC mov ebp,esp 00A316E3 81 EC CC 00 00 00 sub esp,0CCh 00A316E9 53 push ebx 00A316EA 56 push esi 00A316EB 57 push edi 00A316EC 51 push ecx 00A316ED 8D BD 34 FF FF FF lea edi,[ebp-0CCh] 00A316F3 B9 33 00 00 00 mov ecx,33h 00A316F8 B8 CC CC CC CC mov eax,0CCCCCCCCh 00A316FD F3 AB rep stos dword ptr es:[edi] 00A316FF 59 pop ecx 00A31700 89 4D F8 mov dword ptr [this],ecx 00A31703 8B 45 F8 mov eax,dword ptr [this] 00A31706 C7 00 34 6B A3 00 mov dword ptr [eax],offset CVirtual::`vftable' (0A36B34h) 00A3170C 8B 45 F8 mov eax,dword ptr [this] 00A3170F 5F pop edi 00A31710 5E pop esi 00A31711 5B pop ebx 00A31712 8B E5 mov esp,ebp 00A31714 5D pop ebp 00A31715 C3 ret
先分析构造函数开栈之后的这三句汇编指令:
00E716FF 59 pop ecx 00E71700 89 4D F8 mov dword ptr [this],ecx 00E71703 8B 45 F8 mov eax,dword ptr [this]
第一句 pop ecx ,类的非静态成员函数调用时,会传入一个隐藏的参数,也就是this指针,这里的pop,也就是还原this指针的值保存在ecx中了,接下来this指针的值又被保存到了eax中。
接下来一句,就看到关键了:
00E71706 C7 00 34 6B E7 00 mov dword ptr [eax],offset CVirtual::`vftable' (0E76B34h)
取出了虚表的首地址,保存到了虚表指针中(这里也可以看出,虚表指针是对象的第一个数据成员,也就是说对象的首地址就是虚表指针的首地址)。
00E7170C 8B 45 F8 mov eax,dword ptr [this]
将对象的首地址保存到eax中,准备作为返回值。
执行过这五句汇编指令后,再来看看局部变量窗口中this指针的值,实在一目了然~:
虚表指针指向地址存储着两个虚函数的地址(注意到两个函数地址结束之后,填充了四个字节的0,我猜测这是虚表的结束标志,就像字符串的结束标志“/o”一样):
根据上面分析的五句汇编指令,可以得出这样的结论:
虚表(虚表存储在数据段)的地址存放在对象的起始位置,即对象的第一个数据成员就是它的虚表指针,同时注意到,虚表指针的初始化发生在构造函数过程中。
0x03 派生类中的虚表结构:
1.一般继承(无虚函数覆盖)
子类虚函数表结构:
2.一般继承(有虚函数覆盖)
子类虚函数表结构:
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
3.多重继承(无虚函数覆盖)
子类虚函数表结构:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
4.多重继承(有虚函数覆盖)
子类虚函数表结构:
,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了:
Derive d; Base1 *b1 = &d; Base2 *b2 = &d; Base3 *b3 = &d; b1->f(); //Derive::f() b2->f(); //Derive::f() b3->f(); //Derive::f() b1->g(); //Base1::g() b2->g(); //Base2::g() b3->g(); //Base3::g()