3

In a program I'm trying to recover data structures I've discovered the following strange (ARM) disassembly code:

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

So far it looks all fine. There seems to be a derived class overriding the vptr after the base class ctor has been called. An internal subobject is first initialized in obj_ctor and then the vtable is set to a derived subobject. The first strange thing is why ctor_2 doesn't directly call the subobject's derived ctor which in turn sets up first the base subobject. I suppose this happened because the call has been inlined by the compiler.

However, things get spicy when the whole object is again subclassed:

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

I have absolutely no idea how this is possible. How can a subclass 'change' a member type (which is definitely not even a pointer) which has already been setup in a superclass? It is confirmed that ctor_2 and ctor_3 create both valid opaque objects.

Do I misunderstand how vtables work in disassembly? Could a compiler generate such code from valid C++?

I don't know if that's important, but the symbols ctor_2 and the ctor_2 called from ctor_3 are actually different albeit executing the exact same code (maybe because of different ctors?).

EDIT:

This is how the destructors look like:

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

As you can see, the vtables are overwritten with the same value. There is no call to dtor_derived2_so, so the vtable overwrite seems unneccesary. Even more interesting is that when the subobject should be destructed, there's always a call to dtor_base_so and not dtor_derived_so. I checked the vtables of derived_so and derived2_so and they have the following two destructors:

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

When they're called they call immediately the corresponding dtor. Since they reference fixed locations at which the object should be destroyed, the subobjects seem to only exist inside derived2's class. What is going on here? Why would one force the object's destruction if a subobject is destroyed? Or do we have here a special case of virtual inheritance?

Here are the vtables:

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)
4
  • can you also add the vtables, including the two dwords before each if they're not zero?
    – Igor Skochinsky
    Nov 18, 2019 at 18:47
  • You mean the data before the address the vptr points to? Yeah sure, there is always an address to some other location close to the vtables.
    – Overblade
    Nov 18, 2019 at 18:50
  • I mean a list of DCD xxx at vtable_base, vtable_derived etc.
    – Igor Skochinsky
    Nov 18, 2019 at 19:02
  • Thanks for vtables. I’ll check them and update my answer some time soon.
    – Igor Skochinsky
    Nov 20, 2019 at 0:14

3 Answers 3

4

You're correctly interpreting C++'s way of implementing class inheritance, however your assumption that the "subobject" is a member object of the class may be incorrect.

Through compiled code alone, It is impossible to completely distinguish member objects from additional inheritance in multiple inheritance classes as both appear the same. As a matter of fact, seeing something like this is one of the ways to distinguish a member object from multiple inheritance. Another is using RTTI information, if it exists.

In C++, multiple inheritance is implemented by appending one base class structure after the other, where all additional members are often be added to the first class (although if I remember correctly, that's not required by the standard). You can read about the memory layout of multiple inheritance classes in this article, which also covers the diamond inheritance problem and it's common solution - virtual inheritance - and the resulting memory layout.

The following figure (taken from the article) illustrates the memory layout of a multiple-inheritance class:

multiple inheritance class structure

I also found this gist example code that shows how multiple inheritance works under the hood and includes the expected structure in the comments at the top of the file.

You should definitely check it out in compiler explorer. Where you could easily see how this all looks in most compiler and architecture configurations.

I think the inclusion of names and symbols together with instant update upon modifications and the control over the optimization levels makes this a great way to understand the memory layout and code of multiple inheritance.

3

Could it be multiple inheritance? That could explain why the vptr of the supposed subobject is overwritten by ctor_2 without having to assume the compiler to inline anything. The derived class might actually have two base classes, the "base" and the "subobject". If this is the case, it kinda makes sense why the compiler would make ctor_3 to change the vptr of both base classes instead of just one of them. I'm not quite sure what exactly this would mean for the destructors, though.

1
+50

You have here multiple inheritance where both base classes have virtual destructors. The pattern you see in dtor_derived_so is a so-called "non-virtual thunk", which adjusts this before calling the whole class destructor. Normally you should also see 0xFFFFFFE0 (offset to base) in the second dword before the secondary vtables. I was able to produce code and vtable layout very similar to your example with the following source code:

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;
}

For more info, see the Itanium C++ ABI, in particular 2.5 Virtual Table Layout.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.