IDA中对于菱形继承中是否为虚继承的判断
最近遇到了一种菱形继承关系的类结构,在判断C1、C2是否为虚继承C上产生了疑问,因此这里写一篇博客来分析自己的一些技巧。
如何判断C1、C2是否是虚继承C?
一种比较简单的办法是通过查看typeinfo for C1
的信息,看继承关系中是否包含virtual public
关键字,通常情况下,如果C1是public
虚继承C,那么这里会显示virtual public
,但是如果没有显示virtual
关键字,也有可能是虚继承,这时需要进一步查看函数列表。如果函数列表存在non-virtual thunk to'C1::~C1
等以non-virtual thunk
开头的函数,那说明C1不是虚继承关系,对于是如何判断出这个结果的,可以尝试写两个相同的菱形继承的代码,一个使用虚继承,一个不使用虚继承,然后分别编写为C++动态库,进入IDA中查看。
创建so
动态库的命令。
g++ -fPIC -o0 -shared -o libexample.so example.cpp
菱形继承下的类布局
最近工作中,遇到了菱形继承问题,对于一般菱形继承,一般有两种方式继承基类、一般继承、虚继承。在这两种情况下,类对象的布局是怎么样的,接下来通过两个示例来说明。
一般继承下的菱形继承
在这种情况下,Sub
会存在两个Base类,也就是Base类有两个实体。(虽然不懂这样做的目的,但本着不理解,但尊重的原则,也介绍这种情况)
#include <iostream>
using namespace std;
class Base {
public:
int *iptr;
char *cptr;
virtual ~Base() = default;
};// 24 bytes
class otherA : public Base {
public:
char *cptr;
otherA() = default;
}; // 32 bytes
class otherB : public Base {
public:
otherB() = default;
}; // 24 bytes
class Sub : public otherA, public otherB {
public:
int a;
Sub() = default;
}; // 64 bytes
int main() {
Sub sub;
cout << sizeof(Base) << endl;
cout << sizeof(otherA) << endl;
cout << sizeof(otherB) << endl;
cout << sizeof(sub) << endl;
return 0;
}
使用clang
查看类对象布局:
clang++ -Xclang -fdump-record-layouts Untitled-1.cpp > classlayout
*** Dumping AST Record Layout
0 | class Base
0 | (Base vtable pointer)
8 | int * iptr
16 | char * cptr
| [sizeof=24, dsize=24, align=8,
| nvsize=24, nvalign=8]
*** Dumping AST Record Layout
0 | class otherA
0 | class Base (primary base)
0 | (Base vtable pointer)
8 | int * iptr
16 | char * cptr
24 | char * cptr
| [sizeof=32, dsize=32, align=8,
| nvsize=32, nvalign=8]
*** Dumping AST Record Layout
0 | class otherB
0 | class Base (primary base)
0 | (Base vtable pointer)
8 | int * iptr
16 | char * cptr
| [sizeof=24, dsize=24, align=8,
| nvsize=24, nvalign=8]
*** Dumping AST Record Layout
0 | class Sub
0 | class otherA (primary base)
0 | class Base (primary base)
0 | (Base vtable pointer)
8 | int * iptr
16 | char * cptr
24 | char * cptr
32 | class otherB (base)
32 | class Base (primary base)
32 | (Base vtable pointer)
40 | int * iptr
48 | char * cptr
56 | int a
| [sizeof=64, dsize=60, align=8,
| nvsize=60, nvalign=8]
在这种继承方式下,可以看到,otherA
重新利用了基类的虚表指针,也就是一般继承方式下,otherA
不会生成自己的虚表指针,但是需要注意的是,可能会修改该虚表指针指向的位置,因为在otherA
类中可能重写了基类的函数,一般情况下虚表和虚表指针的变化,参考C++虚函数表底层结构 – 吴奇灵的博客 (wqlblog.cn)
同理otherB
类也是这样,因此当子类Sub
继承otherA 或 otherB
也会遵循这种规则,即首先构造otherA
,在构造otherB
,而在otherA
和otherB
中又会首先构造Base
,最终得到的Sub
类对象,也会存在两个基类对象。
在这种情况下,如何调用Base
类的成员呢,显然直接调用是不行的,得指明所属的类。
Sub sub = new Sub;
sub->otherA::iptr = nullptr;
sub->otherB::iptr = nullptr;
sub->iptr = nullptr; // 错误,会产生二义性问题
为了便于问题分析,我对类的布局调整了一下,确保字节对齐只在
Sub
类中存在,在Sub
类中,int a
占有8个字节单位(后面4字节作为padding)。
class
的字节对齐原则和struct
字节对齐原则相同,参考文章一次讲清楚结构体大小的计算 – 吴奇灵的博客 (wqlblog.cn)
虚继承下的菱形继承
#include <iostream>
using namespace std;
class Base {
public:
int *iptr;
char *cptr;
virtual ~Base() = default;
};// 24 bytes
class otherA : virtual public Base {
public:
char *cptr;
otherA() = default;
}; // 40 bytes
class otherB : virtual public Base {
public:
otherB() = default;
}; // 32 bytes
class Sub : public otherA, public otherB {
public:
int a;
Sub() = default;
}; // 56 bytes
int main() {
Sub sub;
cout << sizeof(Base) << endl;
cout << sizeof(otherA) << endl;
cout << sizeof(otherB) << endl;
cout << sizeof(sub) << endl;
return 0;
}
*** Dumping AST Record Layout
0 | class Base
0 | (Base vtable pointer)
8 | int * iptr
16 | char * cptr
| [sizeof=24, dsize=24, align=8,
| nvsize=24, nvalign=8]
*** Dumping AST Record Layout
0 | class otherA
0 | (otherA vtable pointer)
8 | char * cptr
16 | class Base (virtual base)
16 | (Base vtable pointer)
24 | int * iptr
32 | char * cptr
| [sizeof=40, dsize=40, align=8,
| nvsize=16, nvalign=8]
*** Dumping AST Record Layout
0 | class otherB
0 | (otherB vtable pointer)
8 | class Base (virtual base)
8 | (Base vtable pointer)
16 | int * iptr
24 | char * cptr
| [sizeof=32, dsize=32, align=8,
| nvsize=8, nvalign=8]
*** Dumping AST Record Layout
0 | class Sub
0 | class otherA (primary base)
0 | (otherA vtable pointer)
8 | char * cptr
16 | class otherB (base)
16 | (otherB vtable pointer)
24 | int a
32 | class Base (virtual base)
32 | (Base vtable pointer)
40 | int * iptr
48 | char * cptr
| [sizeof=56, dsize=56, align=8,
| nvsize=28, nvalign=8]
可以看出otherA
与otherB
类中,会生成独立的虚表指针,用于指向自己的虚函数表,并没有使用基类的虚表指针。
而在Sub
类的布局中,otherA,otherB
中没有Base
类的类布局,而是将Base
类的类布局放到了末位,因为关键字class Base (virtual base)
,指明了Base
类是虚继承,因此之后根据该布局创建对象时,由于只有一个Base
类布局,所以只会创建一个Base
实例。
细心的朋友可能会发现,
Base
类在类布局的末位,而不在一开始,这是为什么呢?两个原因:
- 如果放在开始,放在那个派生类的前面合适?例如有两个派生类继承
Base
类,你放在第一个类之前,还是放在第二个类之前?同时,如果在菱形继承的基础上在嵌套一层菱形继承,那么Base
类应该放在哪里,才能确保所有的类都能访问到?- 为了保证动态访问,虚基类实例的访问使用虚基类指针(
vbptr
)来动态访问,这种设计让类的布局更加灵活。综合上述两个条件,显然将虚基类放在对象的末位更加合适。(再创建一个新的类
Sub2
,它继承Sub
类,在这种情况下,虚基类也会放在类布局的末位)
gnu下的调试结果
当然呢也可以使用gcc来输出类似的类布局,不过相较于clang
来说,没有这么直观,gcc主要突出的是虚表结构:
linux环境下:
gcc -dfump-lang-class example.cpp
macos环境下:
在macos下,gcc命令被占用,这里使用brew安装gcc后,通过如下命令得到同linux下的结果:
gcc-14 --std=c++11 -fdump-lang-class example.cpp
一般继承情况下
在55
行,可以看到Sub
类也是存在两个Base
类(地址不同,说明有两个实体),同时也可以得知每个类的虚表结构(虚表存储在只读数据段.rodata
),不跟随对象,对象中只是有一个指针指向这个虚表所在的地址。
Vtable for Base
Base::_ZTV4Base: 4 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI4Base)
16 (int (*)(...))Base::~Base
24 (int (*)(...))Base::~Base
Class Base
size=24 align=8
base size=24 base align=8
Base (0x0x10ba1c300) 0
vptr=((& Base::_ZTV4Base) + 16) // Base虚表指针
Vtable for otherA
otherA::_ZTV6otherA: 4 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI6otherA)
16 (int (*)(...))otherA::~otherA
24 (int (*)(...))otherA::~otherA
Class otherA
size=32 align=8
base size=32 base align=8
otherA (0x0x10b9cab60) 0
vptr=((& otherA::_ZTV6otherA) + 16) // 指向的还是Base虚表指针,因为构造otherA时先构造Base,因此otherA的起始位置就是Base的起始位置
Base (0x0x10ba1c360) 0
primary-for otherA (0x0x10b9cab60)
Vtable for otherB
otherB::_ZTV6otherB: 4 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI6otherB)
16 (int (*)(...))otherB::~otherB
24 (int (*)(...))otherB::~otherB
Class otherB
size=24 align=8
base size=24 base align=8
otherB (0x0x10b9cadd0) 0
vptr=((& otherB::_ZTV6otherB) + 16) // 同otherA
Base (0x0x10ba1c420) 0
primary-for otherB (0x0x10b9cadd0)
Vtable for Sub
Sub::_ZTV3Sub: 8 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI3Sub)
16 (int (*)(...))Sub::~Sub
24 (int (*)(...))Sub::~Sub
32 (int (*)(...))-32
40 (int (*)(...))(& _ZTI3Sub)
48 (int (*)(...))Sub::_ZThn32_N3SubD1Ev
56 (int (*)(...))Sub::_ZThn32_N3SubD0Ev
Class Sub
size=64 align=8
base size=60 base align=8
Sub (0x0x10ba490e0) 0
vptr=((& Sub::_ZTV3Sub) + 16) // 还是Base的虚表指针
otherA (0x0x10ba52000) 0
primary-for Sub (0x0x10ba490e0)
Base (0x0x10ba1c480) 0
primary-for otherA (0x0x10ba52000)
otherB (0x0x10ba52068) 32
vptr=((& Sub::_ZTV3Sub) + 48) // otherB的虚表指针,其实就是otherB继承Base的那个虚表指针
Base (0x0x10ba1c4e0) 32
primary-for otherB (0x0x10ba52068)
虚继承情况下
在120
行,可以看到Sub
类也是存在两个Base
类(地址相同,说明是一个实体)
Vtable for Base
Base::_ZTV4Base: 4 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI4Base)
16 (int (*)(...))Base::~Base
24 (int (*)(...))Base::~Base
Class Base
size=24 align=8
base size=24 base align=8
Base (0x0x10d7d3300) 0
vptr=((& Base::_ZTV4Base) + 16) // Base虚表指针
Vtable for otherA
otherA::_ZTV6otherA: 10 entries
0 16
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI6otherA)
24 (int (*)(...))otherA::~otherA
32 (int (*)(...))otherA::~otherA
40 18446744073709551600
48 (int (*)(...))-16
56 (int (*)(...))(& _ZTI6otherA)
64 (int (*)(...))otherA::_ZTv0_n24_N6otherAD1Ev
72 (int (*)(...))otherA::_ZTv0_n24_N6otherAD0Ev
VTT for otherA
otherA::_ZTT6otherA: 2 entries
0 ((& otherA::_ZTV6otherA) + 24)
8 ((& otherA::_ZTV6otherA) + 64)
Class otherA
size=40 align=8
base size=16 base align=8
otherA (0x0x10d781b60) 0
vptridx=0 vptr=((& otherA::_ZTV6otherA) + 24) // otherA的虚表指针
Base (0x0x10d7d3360) 16 virtual
vptridx=8 vbaseoffset=-24 vptr=((& otherA::_ZTV6otherA) + 64)
Vtable for otherB
otherB::_ZTV6otherB: 10 entries
0 8
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI6otherB)
24 (int (*)(...))otherB::~otherB
32 (int (*)(...))otherB::~otherB
40 18446744073709551608
48 (int (*)(...))-8
56 (int (*)(...))(& _ZTI6otherB)
64 (int (*)(...))otherB::_ZTv0_n24_N6otherBD1Ev
72 (int (*)(...))otherB::_ZTv0_n24_N6otherBD0Ev
VTT for otherB
otherB::_ZTT6otherB: 2 entries
0 ((& otherB::_ZTV6otherB) + 24)
8 ((& otherB::_ZTV6otherB) + 64)
Class otherB
size=32 align=8
base size=8 base align=8
otherB (0x0x10d781dd0) 0 nearly-empty
vptridx=0 vptr=((& otherB::_ZTV6otherB) + 24) // otherB的虚表指针
Base (0x0x10d7d3420) 8 virtual
vptridx=8 vbaseoffset=-24 vptr=((& otherB::_ZTV6otherB) + 64)
Vtable for Sub
Sub::_ZTV3Sub: 15 entries
0 32
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI3Sub)
24 (int (*)(...))Sub::~Sub
32 (int (*)(...))Sub::~Sub
40 16
48 (int (*)(...))-16
56 (int (*)(...))(& _ZTI3Sub)
64 (int (*)(...))Sub::_ZThn16_N3SubD1Ev
72 (int (*)(...))Sub::_ZThn16_N3SubD0Ev
80 18446744073709551584
88 (int (*)(...))-32
96 (int (*)(...))(& _ZTI3Sub)
104 (int (*)(...))Sub::_ZTv0_n24_N3SubD1Ev
112 (int (*)(...))Sub::_ZTv0_n24_N3SubD0Ev
Construction vtable for otherA (0x0x10d80c068 instance) in Sub
Sub::_ZTC3Sub0_6otherA: 10 entries
0 32
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI6otherA)
24 0
32 0
40 18446744073709551584
48 (int (*)(...))-32
56 (int (*)(...))(& _ZTI6otherA)
64 0
72 0
Construction vtable for otherB (0x0x10d80c0d0 instance) in Sub
Sub::_ZTC3Sub16_6otherB: 10 entries
0 16
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI6otherB)
24 0
32 0
40 18446744073709551600
48 (int (*)(...))-16
56 (int (*)(...))(& _ZTI6otherB)
64 0
72 0
VTT for Sub
Sub::_ZTT3Sub: 7 entries
0 ((& Sub::_ZTV3Sub) + 24)
8 ((& Sub::_ZTC3Sub0_6otherA) + 24)
16 ((& Sub::_ZTC3Sub0_6otherA) + 64)
24 ((& Sub::_ZTC3Sub16_6otherB) + 24)
32 ((& Sub::_ZTC3Sub16_6otherB) + 64)
40 ((& Sub::_ZTV3Sub) + 104)
48 ((& Sub::_ZTV3Sub) + 64)
Class Sub
size=56 align=8
base size=28 base align=8
Sub (0x0x10d8000e0) 0
vptridx=0 vptr=((& Sub::_ZTV3Sub) + 24) // otherA的虚表指针
otherA (0x0x10d80c068) 0
primary-for Sub (0x0x10d8000e0)
subvttidx=8
Base (0x0x10d7d3480) 32 virtual
vptridx=40 vbaseoffset=-24 vptr=((& Sub::_ZTV3Sub) + 104) // Base的虚表指针
otherB (0x0x10d80c0d0) 16 nearly-empty
subvttidx=24 vptridx=48 vptr=((& Sub::_ZTV3Sub) + 64) // otherB的虚表指针
Base (0x0x10d7d3480) alternative-path // 就是上个Base,因为地址相同
可以看到,分析gnu输出的布局信息,非常复杂,但是它也弥补了clang的缺陷,比如输出了虚表结构等。