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 协议进行许可,转载请遵循协议
暂无评论

发送评论 编辑评论


				
默认
贴吧
上一篇
下一篇