C++多重继承时的二进制兼容问题分析

多重继承下的对象内存布局
参考如下文章:

多重继承时的二进制兼容问题
class Sub : public Base1, public Base2, public Base3
{
.....
}
多重继承的情况下,Sub类的对象的内存布局中,会有三个虚表指针,分别指向Base1、Base2、Base3的虚表。
其中,Base1的虚表布局和Sub类的虚表布局会重合,也就是说,如果在Sub类中新添加一个虚函数,那么该虚函数会出现在第一个虚表中。
需要注意的是,如果Sub中重写(override)非第一基类(如Base2)中的虚函数,那么Sub中该虚函数仍然位于第一个虚表中(见如下编程验证)!


gdb分析多重继承下的子类对象内存布局
===========================================================================
#include <iostream>
using namespace std;
class Base1
{
public:
virtual void foo1(void){
cout<<"Base1 "<<__FUNCTION__<<endl;
};
virtual void foo2(void){
cout<<"Base1 "<<__FUNCTION__<<endl;
};
virtual void foo3(void){
cout<<"Base1 "<<__FUNCTION__<<endl;
};
};
class Base2
{
public:
virtual void test1(void){
cout<<"Base2 "<<__FUNCTION__<<endl;
};
virtual void test2(void){
cout<<"Base2 "<<__FUNCTION__<<endl;
};
};

class Sub: public Base1, public Base2
{
public:
virtual void foo1(void){
cout<<"Sub "<<__FUNCTION__<<endl;
};
virtual void test2(void){ //此处Sub中试图override Base2的函数test2
cout<<"Sub "<<__FUNCTION__<<endl;
};
};

int main(int argc, char**argv)
{
Sub* sub = new Sub();
sub->foo1();

Base2* b2 = (Base2*)sub;
b2->test1();
b2->test2();

return 0;
}
运行结果:
[lubo@localhost multi_vtables]$ g++ multi.cpp -g
[lubo@localhost multi_vtables]$ ./a.out
Sub foo1
Base2 test1
Sub test2 //此处用Base2*的指针访问test2(),执行的确实是Sub的test2,和预期结果是相同的。
[lubo@localhost multi_vtables]$

gdb查看虚表布局:
(gdb) set p pretty
(gdb) set p object
(gdb) set p array
(gdb) p *sub
$1 = (Sub) {
<Base1> = {
_vptr.Base1 = 0x400d70 <vtable for Sub+16>
},
<Base2> = {
_vptr.Base2 = 0x400da0 <vtable for Sub+64>
}, <No data fields>}
(gdb) p ((void**)0x400d70)[0]@4
$3 = {0x400b96 <Sub::foo1()>,
0x400ab6 <Base1::foo2()>,
0x400aee <Base1::foo3()>,
0x400bce <Sub::test2()>} //此处证明Sub::test2位于第一个虚表中!而非对应Base2的第二虚表!
(gdb) p ((void**)0x400da0)[0]@2
$4 = {0x400b26 <Base2::test1()>,
0x400c05 <non-virtual thunk to Sub::test2()>} //此处同样证明第二虚表中没有Sub::test2的地址!
(gdb) info symbol 0x400c05 //这个值是个什么玩意儿???
non-virtual thunk to Sub::test2() in section .text of /media/work/docs/c_test/c++/multi_vtables/a.out
(gdb) info address Base2::test2
Symbol "Base2::test2()" is a function at address 0x400b5e.

那么问题来了
问题1:既然Sub::test2()位于第一个虚表中,那用指针(Base2*)b2访问test2()时,为何跑到了第一虚表中?解答:编译后会按照test2()的实际位置来访问对应的虚表偏移。
问题2:第二虚表中的第2个指针是0x400c05,既不是Base2::test2 (0x400b5e),又不是Sub::test2 (0x400bce),那么0x400c05是什么东西???

子类中重写非第一基类的虚函数带来的二进制不兼容问题
首先我们改一下代码结构:
将Base1、Base2、Sub三个类的声明放到sub.h中,成员函数的实现放到sub.cpp中,调用这些类的main函数放到main.cpp中;将sub.cpp编译成动态库libsub.so。
main.cpp

sub.cpp

sub.h

首先我们不在Sub类中重写Base2::test2(),编译运行结果如下:
[lubo@localhost multi_vtables]$ g++ sub.cpp -fPIC -shared -o libsub.so
[lubo@localhost multi_vtables]$ g++ main.cpp -L./ -lsub
[lubo@localhost multi_vtables]$ export LD_LIBRARY_PATH=$PWD
[lubo@localhost multi_vtables]$ ./a.out
Sub foo1
Base2 test1
Base2 test2
[lubo@localhost multi_vtables]$

此时我们看到,(Base*)b2->test2()确实执行到了Base2的虚函数,此时Sub的第2虚表的第2项,恰好是Base2::test2(),这个虚表是和Base2的虚表完全一致的。

那么,从上述gdb分析结论看,假设我们在Sub类中重写Base2::test2(),那么Sub::test2()应该位于第1虚表的第4项,而第2虚表的第2项是一个很奇怪的值,并不是我们预期的Sub::test2。如果我们重新编译生成libsub.so,而不重新编译main.cpp,直接运行a.out的话,理论上会导致程序崩溃,因为a.out中执行b2->test2()时,访问的仍然是第2虚表的第2项!

运行验证:
[lubo@localhost multi_vtables]$ g++ sub.cpp -fPIC -shared -o libsub.so
[lubo@localhost multi_vtables]$ ./a.out //运行老的a.out
./a.out: Symbol `_ZTV3Sub' has different size in shared object, consider re-linking
Sub foo1
段错误(吐核)
[lubo@localhost multi_vtables]$

结论
当动态库的某个导出类存在多重继承关系时,不得在此类中重写非第一基类中的虚函数,否则会引起新动态库与老可执行程序间的二进制不兼容!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值