构造函数反汇编中奇怪的 vtable 设置

逆向工程 拆卸 部件 C++ 手臂 虚表
2021-06-23 06:58:41

在我试图恢复数据结构的程序中,我发现了以下奇怪的 (ARM) 反汇编代码:

ctor_1:
    ldr  r1, =vtable_base
    str  r1, [r0]             ;r0 always contains object instance ptr
    ;... more setup
    bx   lr

ctor_2:
    push {r4,lr}
    mov  r4, r0
    bl   ctor_1
    ldr  r1, =vtable_derived
    str  r1, [r0]             ;vtable override in derived class
    add  r0, r0, #0x20
    bl   obj_ctor             ;calls an object's ctor at r0+0x20
    ldr  r1, =vtable_derived_so
    str  r1, [r0, 0x20]       ;overrides object vtable
    ;...
    pop  {r4,lr}
    bx   lr

到目前为止看起来一切正常。在调用基类 ctor 之后,似乎有一个派生类覆盖了 vptr。首先在其中初始化内部子对象obj_ctor,然后将 vtable 设置为派生子对象。第一个奇怪的事情是为什么ctor_2不直接调用子对象的派生构造函数,而后者又首先设置基础子对象。我想这是因为调用已被编译器内联。

然而,当整个对象再次被子类化时,事情就会变得棘手:

ctor_3:
    push {r4,lr}
    mov  r4, r0
    bl   ctor_2
    ldr  r1, =vtable_derived2
    ldr  r2, =vtable_derived2_so
    str  r1, [r0]           ;vtable to the new subclass
    str  r2, [r0, 0x20]     ;what??
    ;...
    pop  {r4,lr}
    bx   lr

我完全不知道这怎么可能。子类如何“更改”已经在超类中设置的成员类型(甚至绝对不是指针)?确认ctor_2ctor_3创建两个有效的不透明对象。

我是否误解了 vtables 在反汇编中的工作原理?编译器能否从有效的 C++ 生成这样的代码?

我不知道这是否重要,但是符号ctor_2ctor_2调用的ctor_3对象实际上是不同的,尽管执行的是完全相同的代码(可能是因为不同的构造函数?)。

编辑:

这是析构函数的样子:

dtor_1:
    push {r4, lr}
    ldr  r1, =vtable_base
    str  r1, [r0]                      ;why overwrite the vtable with the same value?
    ;...calls to delete for heap objects
    pop  {r4, lr}
    bx   lr

dtor_2:
    push {r4, lr}
    mov  r4, r0
    ldr  r1, =vtable_derived
    ldr  r2, =vtable_derived_so
    str  r1, [r0]
    str  r2, [r0, #0x20]
    add  r0, r0, #0x20
    bl   dtor_base_so
    mov  r0, r4
    bl   dtor_1
    pop  {r4, lr}
    bx   lr

dtor_3:
    push {r4, lr}
    mov  r4, r0
    ldr  r1, =vtable_derived2
    ldr  r2, =vtable_derived2_so
    str  r1, [r0]
    str  r2, [r0, #0x20]
    ;...
    bl   dtor_2
    pop  {r4, lr}
    bx   lr

如您所见,vtables 被相同的值覆盖。没有调用dtor_derived2_so,因此 vtable 覆盖似乎是不必要的。更有趣的是,当子对象应该被销毁时,总是会调用dtor_base_so和 not dtor_derived_so我检查了derived_soand的 vtables,derived2_so它们有以下两个析构函数:

dtor_derived_so:
    ldr  r12, =0xFFFFFFE0              ;-0x20
    add  r0, r0, r12
    b    dtor_2

dtor_derived2_so:
    ldr  r12, =0xFFFFFFE0              ;-0x20
    add  r0, r0, r12
    b    dtor_3

当他们被调用时,他们会立即调用相应的 dtor。由于它们引用了应该销毁对象的固定位置,因此子对象似乎只存在于derived2的类中。这里发生了什么?如果子对象被销毁,为什么要强制对象销毁?或者我们这里有一个虚拟继承的特例吗?

下面是虚表:

vtable_base:
    dcd  0x82016D20          ;dtor_1
    dcd  0x82016CE0          ;dtor_1 (destruct and free)
    dcd  0x82016BF8
    dcd  0x82016C98
    dcd  0x82016BB8
    dcd  0x82016B78
vtable_derived:
    dcd  0x8201691C          ;dtor_2
    dcd  0x820168D8          ;dtor_2 (destruct and free)
    dcd  0x82016BF8
    dcd  0x8201686C
    dcd  0x8201682C
    dcd  0x820167F8
    dcd  0x820167C4
vtable_derived2:
    dcd  0x82016364          ;dtor_3
    dcd  0x82016320          ;dtor_3 (destruct and free)
    dcd  0x82016BF8
    dcd  0x8201686C
    dcd  0x8201682C
    dcd  0x820167F8
    dcd  0x820167C4
vtable_base_so:
    dcd  0x82015CE8          ;dtor_base_so
    dcd  0x82015CC4          ;dtor_base_so (destruct and free)
vtable_derived_so:
    dcd  0x82017178          ;dtor_derived_so
    dcd  0x82017168          ;dtor_derived_so (destruct and free)
vtable_derived2_so:
    dcd  0x820171B8          ;dtor_derived2_so
    dcd  0x820171A8          ;dtor_derived2_so (destruct and free)
3个回答

您正确地解释了 C++ 实现类继承的方式,但是您假设“子对象”是类的成员对象可能是不正确的。

仅通过编译后的代码,不可能完全区分成员对象和多继承类中的附加继承,因为两者看起来是一样的。事实上,看到这样的东西是区分成员对象和多重继承的方法之一。另一个是使用RTTI信息(如果存在)。

在 C++ 中,多重继承是通过一个接一个地附加一个基类结构来实现的,其中所有额外的成员通常都被添加到第一个类中(尽管如果我没记错的话,这不是标准所要求的)。你可以阅读有关多重继承的类的内存布局文章,其中也涵盖了菱形继承问题,这是常见的解决方案-虚拟继承-并将得到的内存布局。

下图(摘自文章)说明了多继承类的内存布局:

多继承类结构

我还发现了这个gist 示例代码,它展示了多重继承在幕后如何工作,并在文件顶部的注释中包含了预期的结构。

您绝对应该在编译器资源管理器中查看它您可以轻松地看到这在大多数编译器和架构配置中的样子。

我认为包含名称和符号以及修改时的即时更新和对优化级别的控制使这是理解内存布局和多重继承代码的好方法。

会不会是多重继承?这可以解释为什么假设的子对象的 vptr 被覆盖,ctor_2而不必假设编译器内联任何东西。derived类可能实际上有两个基地班,“基地”和“子对象”。如果是这种情况,那么编译器为什么ctor_3要更改两个基类的 vptr 而不仅仅是其中一个基类,这就有点道理了。不过,我不太确定这对析构函数究竟意味着什么。

您在这里有多重继承,其中两个基类都有虚拟析构函数。您在其中看到的模式dtor_derived_so是所谓的“非虚拟 thunk”,它this在调用整个类的析构函数之前进行调整通常,您还应该0xFFFFFFE0在辅助 vtable 之前的第二个 dword 中看到(offset to base)。我能够使用以下源代码生成与您的示例非常相似的代码和 vtable 布局:

class A
{
  int a, b, c, d;
public:
  A() {};
  virtual ~A() {};
  virtual int f1() { return 0;};
};

class B
{
  int x;
public:
  virtual ~B() {};
};


class C: public A, B
{
public:
  virtual int f1() { return 2;};
};

class D: public C
{
public:
  virtual int f1() { return 3;};
};


int main()
{
 D d;
}

有关更多信息,请参阅Itanium C++ ABI,特别是2.5 Virtual Table Layout