IDA判断是否虚继承,C++菱形继承下的类布局

IDA 中对于菱形继承中是否为虚继承的判断

最近遇到了一种菱形继承关系的类结构,在判断 C1、C2 是否为虚继承 C 上产生了疑问,因此这里写一篇博客来分析自己的一些技巧。

未命名绘图.drawio

如何判断 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 类有两个实体。(虽然不懂这样做的目的,但本着不理解,但尊重的原则,也介绍这种情况)

未命名绘图.drawio

#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,而在 otherAotherB 中又会首先构造 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]

可以看出 otherAotherB 类中,会生成独立的虚表指针,用于指向自己的虚函数表,并没有使用基类的虚表指针。

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

作者:WuQiling
文章链接:https://www.wqlblog.cn/ida判断是否虚继承,c菱形继承下的类布局/
文章采用 CC BY-NC-SA 4.0 协议进行许可,转载请遵循协议
暂无评论

发送评论 编辑评论


				
默认
贴吧
上一篇
下一篇