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 的缺陷,比如输出了虚表结构等。