福建电脑
2010年第2期(下转第76页)
深入C++虚表
董士珍
(中煤国际工程集团武汉设计研究院电气所湖北武汉430064)
【摘要】:多态是C++语言中最重要的特性之一,而虚表以及虚函数是实现多态的重要手段。许多C++语言的教材对于虚函数的使用以及调用机制有着详细的阐述,但是对于虚表的一些细节内容阐述却并不是很深,对于虚表我们可能会有很多疑问。本文就试图通过使用汇编语言对于虚表实现的细节进行分析,从而加深对多态机制的理解。
我们很难通过C++语言本身来找到答案。C++标准给编译器实现者定义了语法规范,但是被并没有定义如何实现这些语法规范,不同的编译器实现者可能有不同的实现方法,可以肯定的是他们的编译器必须符合这些语法规范。汇编语言作为最接近机器语言的计算机语言,可以为我们揭示一些隐藏在编译器内部的细节。接下来本来就试图通过对C++源码进行反汇编的方式来解答这些疑惑。二、分析
这里我选用WinXP和VS2008作为我们这次分析的平台。我们建立一个最简单的Win32控制台程序,并定义两个简单的类:
接下来我们可以直接编译这些C++源码就可以得到相应的汇编代码。通过分析这些汇编代码我们就找到许多有用的信息。我们可以找到这样的汇编代码:
依然是查看编译后的汇编代码,我们又一次幸运的找到有用的信息:
我们知道类的非静态成员函数调用时,编译器会传入一个"隐藏"的参数。这个参数就是通常我们说的"this"指针,它的值就是对象的地址。在上面的代码中,寄存器ECX保存的就是这个"this"指针,同时它的值又赋给了寄存器EAX。"??_7CD-szBase@@6B@"就是上面提到的虚表,同时它也代表了虚表的地址。接下来,虚表的地址被赋给了由寄存器EAX指定的内存中。由此可见,虚表的地址被存放在对象的起始位置,即对象的第一个数据成员就是它的虚表指针。同时我们还可以注意到,虚表指针的初始化确实发生在构造函数的调用过程中,但是在执行构造函数体之前,即进入到构造函数的"{"和"}"之前。为了更好的理解这一问题,我们可以把构造函数的调用过程细分为两个阶段,即:
1.进入到构造函数体之间。在这个阶段如果存在虚函数的话,虚表指针被初始化。如果存在构造函数的初始化列表的话,初始化列表也会被执行。
2010年第2期(下转第76页)
深入C++虚表
董士珍
(中煤国际工程集团武汉设计研究院电气所湖北武汉430064)
【摘要】:多态是C++语言中最重要的特性之一,而虚表以及虚函数是实现多态的重要手段。许多C++语言的教材对于虚函数的使用以及调用机制有着详细的阐述,但是对于虚表的一些细节内容阐述却并不是很深,对于虚表我们可能会有很多疑问。本文就试图通过使用汇编语言对于虚表实现的细节进行分析,从而加深对多态机制的理解。
【关键词】:C++,虚函数,虚表,反汇编一、引言
数据抽象,继承以及多态是构成C++面向对象编程思想的三个核心特性。在C++中,多态是通过函数调用的迟绑定来实现的,而迟绑定又是通过虚函数的运行时调用来实现的。具体来说,对于有虚函数声明的类,它的每个对象都拥有一个指向虚表的指针。虚表中存放的是类的所有虚函数地址。在调用虚函数的时候,首先找到虚表指针,通过虚表指针找到虚表,然后在虚表中查询到需要调用的虚函数地址,最终调用这个虚函数。由于这一切都是编译器在幕后完成的,我们对虚表的了解比较少。围绕着它我们可能有许多疑惑:·
虚表是怎么实现的?虚表存放在哪里?·虚表中的数据是在什么时候确定的?
·对象中的虚表指针又在什么时候赋值的?我们很难通过C++语言本身来找到答案。C++标准给编译器实现者定义了语法规范,但是被并没有定义如何实现这些语法规范,不同的编译器实现者可能有不同的实现方法,可以肯定的是他们的编译器必须符合这些语法规范。汇编语言作为最接近机器语言的计算机语言,可以为我们揭示一些隐藏在编译器内部的细节。接下来本来就试图通过对C++源码进行反汇编的方式来解答这些疑惑。二、分析
这里我选用WinXP和VS2008作为我们这次分析的平台。我们建立一个最简单的Win32控制台程序,并定义两个简单的类:
接下来我们可以直接编译这些C++源码就可以得到相应的汇编代码。通过分析这些汇编代码我们就找到许多有用的信息。我们可以找到这样的汇编代码:
以上的汇编代码定义了两个数据段,而这两个数据段中的内容恰好就是类的虚表。至此虚表的"庐山真面目"完全展示在我们的面前。根据这些信息,我们可以推理出很多有用的
结论:
·拥有虚函数的类会有一个虚表,而且这个虚表存放在类定义模块的数据段中。模块的数据段通常存放定义在该模块的全局数据和静态数据,这样我们可以把虚表看作是模块的全局数据或者静态数据
·虚表中存放的是虚函数的地址,正好也验证了C++教材中的说法。
另外一个大的疑惑就是对象的虚表指针是在什么时候被赋值的?我们都知道,类的对象是通过构造函数来完成初始化的,但是我们从来没有在构造函数中初始化虚表指针,那么编译器在幕后又做了哪些事情呢?我们依然还是通过反汇编来找到答案。在这个控制台程序的main函数中我们构建一个类对象:依然是查看编译后的汇编代码,我们又一次幸运的找到有用的信息:
我们知道类的非静态成员函数调用时,编译器会传入一个"隐藏"的参数。这个参数就是通常我们说的"this"指针,它的值就是对象的地址。在上面的代码中,寄存器ECX保存的就是这个"this"指针,同时它的值又赋给了寄存器EAX。"??_7CD-szBase@@6B@"就是上面提到的虚表,同时它也代表了虚表的地址。接下来,虚表的地址被赋给了由寄存器EAX指定的内存中。由此可见,虚表的地址被存放在对象的起始位置,即对象的第一个数据成员就是它的虚表指针。同时我们还可以注意到,虚表指针的初始化确实发生在构造函数的调用过程中,但是在执行构造函数体之前,即进入到构造函数的"{"和"}"之前。为了更好的理解这一问题,我们可以把构造函数的调用过程细分为两个阶段,即:
1.进入到构造函数体之间。在这个阶段如果存在虚函数的话,虚表指针被初始化。如果存在构造函数的初始化列表的话,初始化列表也会被执行。
2.进入到构造函数体内。这一阶段是我们通常意义上说的构造函数。
三.总结
本文深入解读了一些关于虚表的细节内容,而这些内容反过来加深了我们对虚函数的理解。同时,本文也展现了如何使用反汇编技术来探究隐藏在编译器幕后的秘密。通过熟练地使用反汇编技术,我们还可以探究诸如多继承下的虚函数调用,动态链接库(DLL)的实现机制等等更复杂的问题。由此可见,我们需要适当地掌握一些汇编语言的知识,这对于我们加深对高级计算机语言的学习以及更好的理解整个计算机体系有很大的帮助。