概述:虚函数的相关问题整理及逆向分析,以及如何通过数组指定调用虚函数。

相关文章推荐:

环境说明:

基本说明

  1. 虚函数表属于类,类的所有对象共享这个类的虚函数表。

    意思就是创建的所有类对象指向的虚函数表都是同一个地址。

  2. 继承状态下的虚函数表内存
    1. 没有重写时,继续使用父类的元素地址
    2. 重写后,使用当前类的元素地址
  3. 派生类函数中多出来的虚函数的访问(基类指针指向派生类成员)

以上说明在文中都有 Demo 。

代码演示

类的大小

这块内容比较简单,写个代码简单跑下即可。

// 类大小为1
class Base {
	int m_ch;
}
 
// 类大小为4
class Base {
	int m_val;
}
 
// 类大小为8
class Base {
	virtual void FunA();
	// 补充,这里不过有多少个虚函数,其虚函数总和大小都为4,因为虚函数都会被保存在虚函数表中。
	virtual void FunB();
}
 
// 类大小为8
// 要考虑到大小对齐的问题,参考结构体
class Base {
	char m_ch;
	int m_val;
}
 
// 类大小为8,和父类大小一直
class BaseA : public Base {
 
}

子类和父类数据对齐的情况

// 大小为1
class Base {
	char m_ch;
};
 
// 类大小为8
class BaseA : public Base {
	int m_val;
};

数据成员和虚函数对齐的情况

// 类大小为16
// 要考虑到大小对齐的问题,参考结构体
class Base {
	char m_ch;
	virtual void func();
	// 补充,这里不过有多少个虚函数,其虚函数总和大小都为4,因为虚函数都会被保存在虚函数表中。
 	virtual void FunB();
};
 
// 类大小为16
class BaseA : public Base {
	int m_val;
};

子类有额外的虚函数时的大小

// 类大小为 16
class Base {
	char m_ch; // 去掉这个大小变为 8,子类大小不变
	virtual void funcA() = 0;
	virtual void funcB();
	
};
 
// 类大小为 16
class BaseA : public Base {
	int m_val;
	virtual void funcC();
};

通过指针访问虚函数表

#include <iostream>
 
using namespace std;
 
class Base {
    
public:
	// char m_ch;
	virtual void funcA() {
	    cout << "Base::funcA" << endl;
	};
	
	virtual void funcB(){
	    cout << "Base::funcB" << endl;
	};
	
	virtual void funcC(){
	    cout << "Base::funcC" << endl;
	};
	
};
 
using U8 = long long;
using FunCall = void(*)();
int main()
{
    Base obj;
    
    U8* objAddr = (U8*)&obj;
    U8* objRef = (U8*)*objAddr;
 
	// 这里有必要单独再去了解下 RTTI 的概念
    ((FunCall)objRef[0])();
    ((FunCall)objRef[1])();
    ((FunCall)objRef[2])();
    
 
    return 0;
}

输出如下所示:

Base::funcA
Base::funcB
Base::funcC

关于 RPPI : 调试的时候看到的类指针是这样的,如下所示:

名称类型
__vfptr0x00007ff74a8dbcf8 {VirtualFun.exe!void(* Base::`vftable’[4])()} {0x00007ff74a8d14ab {VirtualFun.exe!Base::funcA(void)}, …}void * *
[0x00000000]0x00007ff74a8d14ab {VirtualFun.exe!Base::funcA(void)}void *
[0x00000001]0x00007ff74a8d1212 {VirtualFun.exe!Base::funcB(void)}void *
[0x00000002]0x00007ff74a8d11cc {VirtualFun.exe!Base::funcC(void)}void *
可以看到类指针指向的位置不是 3个,而是 4个。这就涉及到了 RTTI 的知识了。
RTTI (Run Time Type Identification) 即通过运行时类型识别,程序能够使用基类的指针或引用来检查着这些指针或引用所指的对象的实际派生类型。

虚函数表的地址

#include <iostream>
using namespace std;
class Base {
    
public:
	// char m_ch;
	virtual void funcA() {
	    cout << "Base::funcA" << endl;
	};
	
	virtual void funcB(){
	    cout << "Base::funcB" << endl;
	};
	
	virtual void funcC(){
	    cout << "Base::funcC" << endl;
	};
	
};
 
// 重写 funcB 函数
class BaseA : public Base {
    virtual void funcB() {
        cout << "BaseA::funcB" << endl;
    }
};
 
using U8 = long long;
using FunCall = void(*)();
int main()
{
    Base obj;
    BaseA objA;
    
    U8* objAddr = (U8*)&obj;
    U8* objRef = (U8*)*objAddr;
    
    for(int i = 0; i < 3; i++) {
        printf("%p ", objRef[i]);
    }
    
    cout << endl;
    
    U8* objAddrA = (U8*)&objA;
    U8* objRefA = (U8*)*objAddrA;
     for(int i = 0; i < 3; i++) {
        printf("%p ", objRefA[i]);
    }
    return 0;
}

输出如下所示:

0x5b2da1042388 0x5b2da10423c6 0x5b2da1042404 
0x5b2da1042388 0x5b2da1042442 0x5b2da1042404

可以看到 funcAfuncC 的地址是一样的。FuncB 的地址发生了变化,这是因为在派生类中重写了 funcB 函数。

基类指针访问派生类成员

#include <iostream>
 
using namespace std;
class Base {
 
public:
    // char m_ch;
    virtual void funcA() {
        cout << "Base::funcA" << endl;
    };
 
    virtual void funcB() {
        cout << "Base::funcB" << endl;
    };
 
    virtual void funcC() {
        cout << "Base::funcC" << endl;
    };
 
};
 
class BaseA : public Base {
public:
    virtual void funcB() {
        cout << "BaseA::funcB" << endl;
    }
 
    virtual void funcD() {
        cout << "BaseA::funcD" << endl;
    }
};
 
using U8 = long long;
using FunCall = void(*)();
int main()
{
    {
        Base* obj = new BaseA;
        U8* objAddr = (U8*)obj;
        U8* objRef = (U8*)*objAddr;
        ((FunCall)objRef[2])();
	((FunCall)objRef[3])(); // 通过下标就可以访问到虚函数表的第4个,也就是子类的虚函数
 
    }
 
    return 0;
}