探索虚函数表的位置

来源:互联网 发布:淘宝服装代理 编辑:程序博客网 时间:2024/05/26 12:58

引言:近日一个问题引起了我的注意:"请问虚函数表放在哪里?"。我也曾经思考过这个问题,零零散散也有一定的收获,这次正好趁这个机会把我对这一部分的理解整理一下。首先值得声明的是,本文的编译环境是VS2002+WinXP。C++标准并没有对虚函数的实现作出任何的说明,甚至都没有提到虚函数的实现需要用虚表来实现,只不过主流的C++编译器的虚函数机制都是通过虚表来实现的,所以用虚表来实现虚函数就成了"不是标准的标准"。但是这并不代表所有编译器在实现细节上的处理都是完全一致的,它们或多或少都存在一定的个体差异。所以,本文的结论不一定适用于其他的编译情况。

虚函数/虚表的基础知识
一个类存在虚函数,那么编译器就会为这个类生成一个虚表,在虚表里存放的是这个类所有虚函数的地址。当生成类对象的时候,编译器会自动的将类对象的前四个字节设置为虚表的地址,而这四个字节就可以看作是一个指向虚表的指针。虚表里依次存放的是虚函数的地址,每个虚函数的地址占4个字节。

编译模块内部虚表存放的位置
如果一个模块定义了拥有虚表的类,那么这个类的虚表存放在那里呢?要回答这个问题,我们还是需要用汇编代码入手,我首先建立了一个简单的Win32 Console Application,然后定义了一个带虚函数的类,在相应的汇编代码中,我找到了重要的破解虚表存放位置的重要线索:
CONST SEGMENT
??_7CDerived@@6B@ ; CDerived::`vftable'
DD FLAT:?foobar@CDerived@@UAEXXZ
DD FLAT:?callMe@CDerived@@UAEXXZ
; Function compile flags: /Odt /RTCsu /ZI
CONST ENDS

以上的汇编代码给了我们这样的信息:
1> 虚表存放的位置应该实在模块的常量段中;
2> 这个类有两个虚函数,它们分别是?foobar@CDerived@@UAEXXZ和?callMe@CDerived@@UAEXXZ。

外部模块虚表存放的位置
当一个模块导出了一个带虚表的类,而另外一个模块又使用了这个导出类,这时候情况又是什么样的呢?这里存在两种很自然的处理方式:
1。维护一份虚表。虚表放在定义导出类的那个模块,任何使用这个导出类的其他模块都要通过这个模块来使用导出类。
2。维护多份虚表。这时候每一个使用导出类的模块都会有一份虚表的拷贝。
VS2002是使用哪一种情况呢?
在假设存在多份虚表的前提下,我们可以使用这样的策略来判断VS2002使用那种方式:
1。在类定义模块中创建一个类对象,并在另外一个模块中使用这个类对象。在类定义模块中创建类对象保证编译器用类定义模块中的虚表来初始化类对象。
2。在模块(非类定义模块)中创建并类对象并使用它。这样就保证编译器会用模块中的虚表来初始化类对象。
3。分别获取两种情况下两个类对象的虚表指针。如果它们的值相等,就说明只存在一份虚表;如果它们的值不等,就说明存在多份虚表。
4。如果两个虚表指针的值相等,则虚表来自于两个模块中的一个模块,判断这个虚表来自于哪个模块。

应用上面的策略,我们首先建立一个Win32 DLL工程导出一个带虚表的类,再建立一个Win32 Consle Application使用这个导出类。在Win32 Consle Application的主函数中,我写了以下的代码:
CDllInDepth* pObjInAnotherDLL = createObject();
int vTableAdress = *reinterpret_cast<int*>(pObjInAnotherDLL);
int vFuncAddress = *reinterpret_cast<int*>(vTableAdress);
pObjInAnotherDLL->dumpMe();

CDllInDepth* pObjInMyApp = new CDllInDepth;
int vTableAdress2 = *reinterpret_cast<int*>(pObjInMyApp);
int vFuncAddress2 = *reinterpret_cast<int*>(vTableAdress);
pObjInMyApp->dumpMe();

对这段代码做如下的解释:
1。createObject()是DLL导出了一个全局函数。这个全局函数实现的功能就是生成一个类对象并将类对象的地址传出。这样做的目的就是为了在类定义模块中生成一个类对象。
2。获得虚表指针和虚函数的代码可以这样分析:由于虚表指针存放在类对象的前4个字节中,我们首先需要将类对象的首地址转化成int型指针,并通过这个int 型指针获得前4个字节的内容,这个内容就是虚表的地址。接着我们将这个虚表的地址再转化成int型指针, 并通过这个int型指针获得虚表的前4个字节的内 容,这个内容就是虚表的第一项的值,也就是一个虚函数的地址

通过调试,我们得出这样的结果:
vTableAdress= 0x1001401C vFuncAddress= 0x1001103C
vTableAdress2 = 0x1001401C vFuncAddress2 = 0x1001103C

比较vTableAdress和vTableAdress2的值我们发现它们的值是完全一样的,这就说明我们的假设是不正确的,这里是存在一份虚表。那最后的一个问题就是这个虚表是来自于哪个模块呢?这个答案我们需要通过比较虚表的地址以及模块所占的内存空间来解答。在调试状态下,打开"模块"窗口,我们就可以找到模块的地址:
TestApp.exe 00400000-00417000
DllInDepth.dll 10000000-10019000
其中的DllInDepth.dll模块就是定义导出类的模块,而TestApp.exe就是使用这个类的模块。通过比较不难发现,虚表的地址落在DllInDepth.dll的地址范围内,这就说明了虚表存放于类定义的模块。

到了现在,关于虚表存放的问题基本上都得到了圆满的解决,但是我又有了一个新的问题:为什么会是这样的情况呢?我想,大概应该是这样的原因吧:类对象虚表指针的初始化应该发生在构造函数被调用的时候,更具体的说应该实在进入到构造函数"{"之前,这个时机就是通常所说的构造函数"初始化列表"被执行的时候。由于构造函数是在类定义模块中执行的,当然虚表也应该是类定义模块的虚表,对于其他的模块而言就是导入函数的调用,这样就没有必要维护多份虚表了。

总结一下,大多数的虚函数机制都是用虚函数表来实现的。虚函数表实际上是一块连续的内存,每四字节存放一个虚函数的地址(每四字节是一个虚函数指针),按虚函数声明顺序依次存放。编译有虚函数的类时编译器会在类的数据成员最前面安插一个指针成员,并生成虚函数表,并在该类的构造函数的初始化列表中安插将指针指向虚函数表的代码,在生成该类的对象时,调用构造函数,将对象的虚函数表指针初始化使其指向虚函数表(将对象的前四字节存储虚函数表地址)。当用基类指针指向它的子类对象的话,会根据虚函数表索引到对应的虚函数地址,从而进行调用。

0 0
原创粉丝点击