虚表指针
群里有人问。就写了
从最简单的类开始, 后面有单继承和多继承虚表指针的不同
代码在32环境下:
先写结论:
虚表指针在构造函数与析构中赋值(下面反汇编证明), 赋值: *this = 虚表指针(即首个成员)
虚表指针指向的是一个数组,存放虚函数,虚函数按一般声明先后排序.
这个数组一般在 .rdata 或 .data
每个父类都会在构造,析构中设置自己的虚表,下面继承中会写
* 为什么要在构造和析构中都赋值, 在下面的继承中会写.
下面代码中顺带一些内存布局
虚表指针在构造函数中 *this = 虚表指针, 指向数组[ virtual void t ]
成员变量a 在 this + 4 的地方,跟随在虚表后, 若没有虚表的类则放在this上
#include <iostream>
#include <Windows.h>
#include <stdio.h>
using std::cout;
using std::endl;
class A
{
public:
A() {
a = 1;
}
~A() {
}
virtual void t() {
cout << "t" << endl;
}
int a;
};
int main()
{
cout <<"size:" << sizeof(A) << endl;
A a;
DWORD * ptr = (DWORD*)&a;
DWORD vptr_addr = *ptr;
//虚表在首个成员的地址上 , 即 *this
cout << "虚表:" << std::hex << vptr_addr<< endl;
// 因为vptr 占了首个成员的位置, 成员a跟在虚表后面
int member = *(ptr + 1);
cout << "member:" << member << endl;
//通过虚表找到函数地址,只有一个虚函数,因此偏移0
DWORD * arr_func = (DWORD*)vptr_addr;
void (*func)() = (void (*)())(*(arr_func + 0)); // arr_func[0]
cout << "函数地址:" << func << endl;
func();
return 0;
}
看反汇编的构造 析构函数 , 删掉了不相关的, 只看虚表指针在构造和析构中赋值:
在这2个函数中赋值都是与继承相关,防止调用错误的虚函数
A(){
mov dword ptr [this],ecx // ecx 为 this
mov eax,dword ptr [this] // eax = this
// *this = A的虚表指针 ,这里赋值
mov dword ptr [eax],offset A::`vftable' (0419B34h)
}
虚表指针: 0x00419B34
内存数据: 0x00419B34 9a 11 41 00 00 00 00 00
还原数组: [0x0041119a] -> [virtual t]
// 在析构中同样出现
~A(){
mov dword ptr [this],ecx // ecx == this
mov eax,dword ptr [this] // eax = this
// *this = A的虚表指针
mov dword ptr [eax],offset A::`vftable' (0419B34h)
}
我这里的vptr为 0x00419B34 , 首个函数地址 0x0041119a -> 虚函数t 的地址;
单继承:
父类会在构造函数中把 *this = 父类自己的虚表指针指向自己的数组,[virtual ~A, virtual A::t]
子类在构造中会把 *this = 子类自己的虚表指针,数组[virtual ~B, virtual A::t]
虚函数在数组中的排序一般按声明先后顺序
数据成员按继承先后顺序排放,即 虚表 , a , b
在父类中构造和析构中 设置 *this = 父类虚表 , 为了防止调用虚函数出错.
现在假设:
1.子类如果有 virtual void t, 则子类虚表数组:[virtual ~B, virtual B::t ]
2. 如果在父类构造和析构调用t(),且没有设置 *this = 父类虚表,
则父类就会调用到子类的 virtual void t
#include <iostream>
#include <Windows.h>
#include <stdio.h>
using std::cout;
using std::endl;
class A
{
public:
A() {
a = 1;
}
virtual ~A() { // 虚析构
}
virtual void t() {
cout << "t" << endl;
}
int a;
};
class B : public A
{
public:
B() {
b = 2;
}
~B() {
}
int b;
};
int main()
{
cout <<"size:" << sizeof(B) << endl;
B b;
DWORD * ptr = (DWORD*)&b;
DWORD vptr_addr = *ptr;
cout << "虚表:" <<std::hex <<vptr_addr << endl;
//获取成员, 继承顺序的原因 : A的东西放在前, 自己的东西放在后
int member_in_class_a = *(ptr + 1);
int member_in_class_b = *(ptr + 2);
cout << "A::a :" << member_in_class_a << ", B::b:" << member_in_class_b << endl;
// 获取子类虚表指针, 调用函数t, 第一个是析构, 第二个函数t
DWORD * arr_func = (DWORD*)vptr_addr;
void(*func)() = (void(*)())(arr_func[1]);
cout << "函数地址:" << std::hex <<func << endl;
func();
return 0;
}
反汇编, 构造函数, 去掉了不相关的:
A(){
mov dword ptr [this],ecx // ecx = this指针
mov eax,dword ptr [this] // eax = this
// *this = 父类虚表指针 0x00419B34
mov dword ptr [eax],offset A::`vftable' (0419B34h)
}
A的虚表地址: 0x00419B34
A的虚表内存数据:0x00419B34 65 14 41 00 60 14 41 00
还原数组:[0x00411465,0x00411460] -> [virtual ~A , virtual A::t ]
对应VSdebug:
0x00411465 {virtualptr.exe!A::`vector deleting destructor'(unsigned int)} // 析构 ~A
0x00411460 {virtualptr.exe!A::t(void)} // A::t
B() {
mov ecx,dword ptr [this] // eax = this
call A::A (041146Fh) // 调用父类构造
mov eax,dword ptr [this] // eax = this
// *this = 子类虚表指针 0x00419B44
mov dword ptr [eax],offset B::`vftable' (0419B44h)
}
B的虚表地址:0x00419B44
虚表内存数据:0x00419B44 51 14 41 00 60 14 41 00
数组:[0x00411451, 00411460] -> [virtual ~B , virtual A::t]
对应VS debug:
0x00411451 {virtualptr.exe!B::`vector deleting destructor'(unsigned int)} // 析构~B
0x00411460 {virtualptr.exe!A::t(void)} // A::t
可以看到:
父类虚表指向自己的数组:[virtual ~A, virtual A::t]
子类指向自己的数组: [virtual ~B, virtual A:t]
防止父类调用了子类虚函数
析构函数反汇编, 也是同样的操作,防止在析构中调用子类虚函数
~B() {
mov dword ptr [this],ecx // ecx == this
mov eax,dword ptr [this] // eax = this
// *this = B的虚表指针
mov dword ptr [eax],offset B::`vftable' (0419B44h)
// ecx = this
mov ecx,dword ptr [this]
// 调用父类析构
call A::~A (041144Ch)
}
~A(){
mov dword ptr [this],ecx // ecx == this
// eax = this
mov eax,dword ptr [this]
// *this = A的虚表
mov dword ptr [eax],offset A::`vftable' (0419B34h)
}
做个测试?
现在假设, 还有一个基类:
class Base {
public:
Base() {
base = 111;
}
virtual ~Base() {
}
int base;
};
class A : public Base{
//与上面一样
}
class B: public A{
//与上面一样
}
这时会有3个虚表 , Base的构造析构 还会赋值一次Base自己的虚表;
Base 虚表-> [virtual ~Base]
A 虚表 -> [virtual ~A , virtual A::t]
B 虚表 -> [virtual ~B, virtual A::t]
内存布局 , 上面说过按继承方式排放:
首地址: 虚表, Base::base, A::a, B::b
如果把Base改成这样 , 没有虚函数了:
class Base {
public:
Base() {
base = 111;
}
~Base() {
}
int base;
};
这时. Base没有虚表了其构造和析构也不会赋值自己虚表
A虚表和B虚表不变
内存布局也不变
例如:
假如
int main(){
B b; // &b == 0x0018FEE4
b的内存数据
0x0018FEE4 48 9b 41 00 6f 00 00 00 01 00 00 00 02 00 00 00
vs debug对应的数据:
+ __vfptr 0x00419b48 {virtualptr.exe!void(* B::`vftable'[3])()}
base 0x0000006f int
a 0x00000001 int
b 0x00000002 int
}
多继承: