设为首页 加入收藏

TOP

C++虚函数在g++中的实现分析
2019-01-03 20:09:09 】 浏览:44
Tags:函数 实现 分析

本文是我在追查一个诡异core问题的过程中收获的一点心得,把公司项目相关的背景和特定条件去掉后,仅取其中通用的C++虚函数实现部分知识记录于此。


在开始之前,原谅我先借用一张图黑一下C++


如果你也在写C++,请一定小心…至少,你要先有所了解: 当你在写虚函数的时候,g++在写什么?


为了探索C++虚函数的实现,我们首先编写几个用来测试的类,代码如下:


代码采用了多继承,是为了更多的分析出g++的实现本质,用UML简单的画一下继承关系:


代码的输出结果和预期的一致,C++实现了虚函数覆盖功能,代码输出如下:


通过本文,我将尝试解答下面这三个问题:


首先是第一个问题:


这个问题乍看简单,大家都知道是通过vptr和vtbl实现的,那就让我们刨根问底的看一看,g++是如何利用vptr和vtbl实现的。


第一步,使用 -fdump-class-hierarchy 参数导出g++生成的类内存结构:


如果看不明白这些乱七八糟的输出,没关系(当然能看懂更好),把上面的输出转换成图的形式就清楚了:


其中有几点尤其值得注意:


有了内存布局后,接下来观察g++是如何在这样的内存布局上进行动态绑定的。


g++对每个类的指针或引用对象,如果是其类声明中虚函数,使用位于其内存空间首地址上的vptr寻找找到vtbl进而得到函数地址。如果是父类声明而子类未覆盖的虚函数,使用对应父类的vptr进行寻址。


先来验证一下,使用 objdump -S 得到 b1.f() 的汇编指令:


其过程和我们的分析完全一致,聪明的你可能发现了,b2怎么办呢?Derived类的实例内存首地址上的vptr并不是Base2类的啊!答案实际上是因为g++???引用赋值语句 Base2 &b2 = ins 上动了手脚:


虽然是指向同一个实例的引用,根据引用类型的不同,g++编译器会为不同的引用赋予不同的地址。例如b2就获得一个指针的偏移量,因此才保证了vptr的正确性。


PS:我们顺便也证明了C++中的引用的真实身份就是指针…


接下来进入第二个问题:


既然我们已经知道了g++是如何通过vptr和vtbl来实现虚函数魔法的,那么vptr和vtbl又是在什么时候被创建的呢?


vptr是一个相对容易思考的问题,因为vptr明确的属于一个实例,所以vptr的赋值理应放在类的构造函数中。 g++为每个有虚函数的类在构造函数末尾中隐式的添加了为vptr赋值的操作


同样通过生成的汇编代码验证:


可以看到在代码中,Derived类的构造函数为实例的两个vptr赋初值,可是,这两个初值居然是立即数!立即数!立即数! 这说明了vtbl的生成并不是运行时的,而是在编译期就已经确定了存放在这两个地址上的


这个地址不出意料的属于.rodata(只读数据段),使用 objdump -s -j .rodata 提取出对应的内存观察:


由于程序运行的机器是小端机,经过简单的转换就可以得到第一个vptr所指向的内存中的第一条数据为0x80488834,如果把这个数据解释为函数地址到汇编文件中查找,会得到:


Bingo! g++在编译期就为每个类确定了vtbl的内容,并且在构造函数中添加相应代码使vptr能够指向已经填好的vtbl的地址


这也同时为我们解答了第三个问题:


在Linux中运行的C++程序虚拟存储器中,vptr、vtbl存放在虚拟存储的什么位置?


直接看图:


图中灰色部分应该是你已经熟悉的,彩色部分内容和相关联的箭头描述了虚函数调用的过程(图中展示的是通过new在堆区创建实例的情况,与示例代码有所区别,小失误,不要在意): 当调用虚函数时,首先通过位于栈区的实例的指针找到位于堆区中的实例地址,然后通过实例内存开头处的vptr找到位于.rodata段的vtbl,再根据偏移量找到想要调用的函数地址,最后跳转到代码段中的函数地址执行目标函数


研究这些问题的起因是因为公司代码出现了非常奇葩的行为,经过追查定位到虚函数表出了问题,因此才有机会脚踏实地的对虚函数实现进行一番探索。


也许你会想,即使我不明白这些底层原理,也一样可以正常的使用虚函数,也一样可以写出很好的面相对象的代码啊?


这一点儿也没有错,但是,C++作为全宇宙最复杂的程序设计语言,它提供的功能异常强大,无异于武侠小说中锋利无比的屠龙宝刀。但武功不好的菜鸟如果胡乱舞弄宝刀,却很容易反被其所伤。只有了解了C++底层的原理和机制,才能让我们把C++这把屠龙宝刀使用的更加得心应手,变化出更加华丽的招式,成为真正的武林高手。



编程开发网
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇C++虚函数表实例分析 下一篇C 语言中的结构体和共用体(联合..

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容:

array(4) { ["type"]=> int(8) ["message"]=> string(24) "Undefined variable: jobs" ["file"]=> string(32) "/mnt/wp/cppentry/do/bencandy.php" ["line"]=> int(214) }