在一个试图恢复数据结构的程序中,我发现了以下奇怪的(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,后者又首先设置了基础子对象。我想发生这种情况是因为该调用已被编译器内联。

但是,当整个对象再次被子类化时,事情变得很棘手:




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都创建了两个有效的不透明对象。

我是否误解了vtable在反汇编中的工作方式?

我不知道这是否重要,但是从ctor_2调用的符号ctor_2ctor_3实际上是不同的,尽管它们执行的是完全相同的代码(可能是由于不同的ctor?)。

编辑:

这就是析构函数的样子:



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

/>
您可以看到,vtable被相同的值覆盖。没有调用dtor_derived2_so,因此vtable覆盖似乎不必要。更有趣的是,当应该销毁子对象时,总是调用dtor_base_so而不是dtor_derived_so。我检查了derived_soderived2_so的vtable,它们具有以下两个析构函数:



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:



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)


评论

如果它们不为零,是否还可以添加vtable,包括每个前的两个dword?

您的意思是vptr指向的地址之前的数据?是的,可以肯定,vtables附近总是有一个指向其他位置的地址。

我的意思是在vtable_base,vtable_derived等处的DCD xxx列表。

感谢您的vtables。我会检查它们并尽快更新答案。

#1 楼

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

仅通过编译代码,就不可能完全区分多个继承类中来自其他继承的成员对象看起来都一样。实际上,看到这样的内容是区分成员对象与多重继承的一种方法。另一种方法是使用RTTI信息(如果存在)。

在C ++中,通过在一个基类结构之后附加一个基类结构来实现多重继承,其中通常将所有其他成员添加到第一类(尽管我没记错,这不是标准要求的。您可以在本文中阅读有关多个继承类的内存布局的信息,其中还介绍了钻石继承问题及其常见解决方案-虚拟继承-以及由此产生的内存布局。
本文)说明了多重继承类的内存布局:


文件顶部注释中的预期结构。

您绝对应该在编译器资源管理器中检出它。您可以轻松地在大多数编译器和体系结构配置中看到所有外观。

我认为将名称和符号与修改后的即时更新以及对优化级别的控制结合在一起,是一种绝佳的方法了解多重继承的内存布局和代码。

#2 楼

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

#3 楼

您在这里具有多重继承,其中两个基类都具有虚拟析构函数。在dtor_derived_so中看到的模式是所谓的“非虚拟重击”,它会在调用整个类的析构函数之前先对this进行调整。通常,您还应该在辅助vtable之前的第二个双字中看到0xFFFFFFE0(相对于基数的偏移)。我可以使用以下源代码生成与您的示例非常相似的代码和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虚拟表布局。