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)
DCD xxx
atvtable_base
,vtable_derived
etc.