After looking at some basic examples of how C++ classes are compiled by MSVC, I tried to apply the knowledge to a DLL I'm working on. While searching for a class to start with, I came across
CRefCountable
. Its constructors are straightforward and it appears to be a base class that almost every other class extends from.
Looking at one of the ctors, it seems like a CRefCountable
is 8 bytes long, containing the vftable
and a DWORD
. I then got the impression that there are some "hidden" fields not initialized, as it is called with a pointer to 16 bytes of memory. Scrolling through the references to this ctor, I was surprised to see that it is also called with a pointer to 572, 500 and/or 20 bytes of memory, never with 8 bytes as I suspected.
How can the size of this class be variable and bigger than the ctor implies? Or could this be a compiler optimization, where memory for a child class or something else is allocated together with the memory for the object, saving a call to malloc
/new
?
I've decided not to include the decompilation or disassembly of the code in question, as it's either trivial or included in a way bigger function of a child class I haven't touched yet. If needed, I can provide some examples though.
EDIT
I completely forgot that the term "base class" was a thing, sorry about that. I'm very sure that this is a base class from the name and from looking around.
This is the constructor that I mentioned above.
// mangled name: ??0CRefCountable@@QAE@XZ
// demangled name: public: __thiscall CRefCountable::CRefCountable(void)
mov eax, ecx
mov dword ptr [eax], offset ??_7CRefCountable@@6B@ ; const CRefCountable::`vftable'
mov dword ptr [eax+4], 0
retn
The values for the size of the object are in constructs such as this:
...
push 10h
call new_or_malloc
mov esi, eax
add esp, 4
mov [esp+18h+var_10], esi
test esi, esi
mov [esp+18h+var_4], 0
jz short loc_1002C12E
mov ecx, esi ; this
call ??0CRefCountable@@QAE@XZ ; CRefCountable::CRefCountable(void)
mov dword ptr [esi], offset off_100443DC
mov eax, [edi+8]
mov [esi+8], eax
mov [esi+0Ch], edi
jmp short loc_1002C130
loc_1002C12E:
xor esi, esi
loc_1002C130:
...
Ghidra's decompiler turns that into constructs like this (which I used to find these more quickly, as it's more concise).
crc = (CRefCountable *)new_or_malloc(0x10);
local_4 = 0;
if (crc == (CRefCountable *)0x0) {
crc = (CRefCountable *)0x0;
}
else {
CRefCountable::CRefCountable(crc );
crc ->vftable = &PTR_AddReference_100443dc;
crc[1].vftable = *(undefined ***)(param_1 + 8);
crc[1].field_0x04 = param_1;
}
The value I suppose is the size of the object is the arg to new_or_malloc
. This function wasn't recognized by IDA Free or ghidra, but it made sense in the context and the function does call HeapAlloc
in the end.
Something that I overlooked yesterday evening was that param_1
(or edi
in the disassembly) sometimes is the this
pointer to a object. In this particular case it's a plugin manager class that doesn't seem to extend CRefCountable.