关于怎样输出类的虚函数地址,网上已经有很多了。
例如下面的例子
//...
#pragma pack(4)
//...
class A {
public:
double base_data;
A() {
base_data = 233;
}
void func() { cout << "fvck you" << endl; }
virtual void func1() { cout << "base_func1" << endl; }
virtual void func2() { cout << "base_func2" << endl; }
virtual void func3() { cout << "base_func3" << endl; }
};
int main()
{
Base base;
A a;
cout << sizeof(A) << endl;
typedef void(*Fun)(void);
void (*pFunc)();
//cout << (int*)&a<<endl;
//cout << (int*)&(a.base_data) << endl;
auto kk = (int*)*((int*)&a)+2;
//kk是int*
pFunc = (Fun)(*kk);
pFunc();
//...
在32位平台(vs2017,x86),会输出:
12
base_func3
关键在于得到pFunc整一个式子:
pFunc=(Fun)*((int*)*((int*)&a)+2)
看起来着实有点恐怖,对于其解释我感觉有些人说得并不够细,我在此推导一下。
(注意:以下数字大小都依赖vs2017,x86)
首先了解原理,你会发现上面代码sizeof(A)是12,这12个字节都是什么呢?(注意我使用pack(4)避免内存对齐带来的困扰)
12=4+8。后面的8是double base_data的大小,那前面的4呢?
对于任何有虚函数的类,前4个字节都是用作存储虚函数表的地址。
虚函数表是一个数组vtable,那么A类中这4个字节就用来存储数组头的地址vptr=vtable,vtable[2]=*(vtable+2)=*(vptr+2)。
图中每一个小矩形代表一个字节。不要忘了32位下一个指针大小为4字节(即4*8=32bit)。
PS:
对于double arr[2]={3.2,4.7};
cout<<*(arr+1);就能得到4.7
本质是对地址arr+1*sizeof(double),进行double型取值(也就是往后取8字节数据)。
PS:
这也是32位程序内存不能超过4GB的原因,因为
在32位下,一个4字节的指针只能表示种不同的状态,也就是
个不同的字节位置。寻址的最小单元为字节。
所以,我们明白了两件事情:
1.取&a开始的前4字节,就能得到指针vptr。
2.对vptr进行类似数组的取值,就能得到一个虚函数的指针。
那怎么取4字节呢?用int将这4字节的数据保存
1.先取得vptr
显然,无法将a直接转为int 或指针类型,就能得到其数据的前4字节。
那么只能绕一步,先取得a的地址,并强转为指针类型,这里使用int*,因为后面要取4字节存到int。
int* a_intp=(int*)&a
现在,我要取值,即从首地址开始的4个字节数据,存到一个int里。
int vptr_int=*(a_intp)
=*((int*)&a)
vptr是vtable数组的首地址,也就是一个指针,但它现在类型为int。我们得把他转化为指针,再取值,才能得到vtable。
化为的指针类型还是为int*,原因同上:
int* vptr=(int*)(vptr_int)
=(int*)*((int*)&a)
通过类似数组的取值方式,取得第3个元素,也就是func3的函数指针。但由于vptr是int*类型,取值只能取得int类型,所以我们得到的函数指针被存进了一个int:
int func3_int=*(vptr+2)
=*((int*)*((int*)&a)+2)
最后,对func3_int的数据强转为可调用的函数指针,以便调用:
Fun func3=(Fun)(fun3_int)
=(Fun)*((int*)*((int*)&a)+2)
这样,指针强取虚函数地址的推导就完成了。
PS:
VS2017打断点的时候可以查看a的_vptr的值,可以对照一下与自己算的值是不是一致。