2

I decompile a VC++ application in IDA 7, and I often find vftables referencing the same function multiple times, like the one I called "Object__pure" here, part of a "heavy-base" Object class inherited by almost every other class in the application:

seg002:008F4748     const Object::`vftable' dd offset Object__free ; DATA XREF: Object__ctor+12↑o Object__dtor+A↑o Object__copy+16↑o
seg002:008F474C         dd offset Object__vsub_7D0990
seg002:008F4750         dd offset Object__vsub_7D09A0
seg002:008F4754         dd offset Object__pure
seg002:008F4758         dd offset Object__pure
seg002:008F475C         dd offset Object__pure
seg002:008F4760         dd offset Object__pure
seg002:008F4764         dd offset Object__vsub_47B660

Child classes in my executable inheriting from Object typically have their own custom functions instead of those "pure" (how I called them) ones in their vtable.

Not really knowing what that could be, I gave it the name "pure", thinking about it like a virtual or pure virtual call. The function itself does nothing other than calling a completely empty Object__vsub_80CD50:

seg000:007E4580     Object__pure proc near ; CODE XREF: <lots!>
seg000:007E4580
seg000:007E4580     a1  = dword ptr -4
seg000:007E4580     arg_0= dword ptr  8
seg000:007E4580
seg000:007E4580 000     push    ebp
seg000:007E4581 004     mov     ebp, esp
seg000:007E4583 004     push    ecx
seg000:007E4584 008     mov     [ebp+a1], ecx
seg000:007E4587 008     mov     eax, [ebp+arg_0]
seg000:007E458A 008     push    eax
seg000:007E458B 00C     mov     ecx, [ebp+a1] ; this
seg000:007E458E 00C     call    Object__vsub_80CD50 ; Call Procedure
seg000:007E4593 00C     mov     esp, ebp
seg000:007E4595 004     pop     ebp
seg000:007E4596 000     retn    4 ; Return Near from Procedure
seg000:007E4596     Object__pure endp

...

seg000:0080CD50     Object__vsub_80CD50 proc near ; CODE XREF: <lots again!>
seg000:0080CD50
seg000:0080CD50     var_4= dword ptr -4
seg000:0080CD50
seg000:0080CD50 000     push    ebp
seg000:0080CD51 004     mov     ebp, esp
seg000:0080CD53 004     push    ecx
seg000:0080CD54 008     mov     [ebp+var_4], ecx
seg000:0080CD57 008     mov     esp, ebp
seg000:0080CD59 004     pop     ebp
seg000:0080CD5A 000     retn    4 ; Return Near from Procedure
seg000:0080CD5A     Object__vsub_80CD50 endp

Why is such a function referenced multiple times? Is it due to optimization, unifying functions that do nothing? Are those functions typically virtual / pure virtual?

4
  • 2
    These two functions don't exactly do "nothing". They both consume an integer from the stack, even if they don't do anything with it. Thus, it may be remnants of debuggin helpers. A more typical "empty" routine would just be retf; a 'pure' virtual function may output a warning.
    – Jongware
    Dec 25, 2017 at 20:39
  • Thanks for the information, the note about debug remnants is interesting. About the methods doing nothing, isn't the last method (vsub_80CD50) even effectively doing nothing if interpreted like a __thiscall (which it seems to be), just pushing over the this pointer in ecx, but then returning immediately?
    – Ray
    Dec 25, 2017 at 22:21
  • The second routine ends with retn *immediate*, so it also removes a value from the stack. If you look up where it is called from, you will see an extra argument is pushed – apart from initializing ecx (and possibly other registers).
    – Jongware
    Dec 26, 2017 at 10:16
  • 4
    Under certain conditions, the compiler may optimise different source code functions with the same implementation to use the same object code when compiled.
    – Ian Cook
    Dec 26, 2017 at 10:56

1 Answer 1

2

As I guessed and was confirmed in the comments, this is apparently some compiler optimization reusing methods executing the same logic.

This got clear to me when I reversed the methods of one of the more specific objects, like the data stream reader / writer here:

seg002:008F4FC4     const DataStream::`vftable' dd offset DataStream__readByte ; DATA XREF: DataStream__ctor+12↑o
seg002:008F4FC8         dd offset DataStream__readWord
seg002:008F4FCC         dd offset DataStream__readDword
seg002:008F4FD0         dd offset DataStream__readBytes
seg002:008F4FD4         dd offset DataStream__canReadWrite
seg002:008F4FD8         dd offset DataStream__writeByte
seg002:008F4FDC         dd offset DataStream__writeWord
seg002:008F4FE0         dd offset DataStream__writeDword
seg002:008F4FE4         dd offset DataStream__writeBytes
seg002:008F4FE8         dd offset DataStream__canReadWrite

You can see that the methods canRead and canWrite were simply optimized into one method (which I named canReadWrite) as the logic for both is the same (hexrays output):

bool __thiscall DataStream::canReadWrite(DataStream *this, int lengthRequired)
{
    return this->members.pData <= this->members.pDataEnd
        && this->members.pDataEnd - this->members.pData >= lengthRequired;
}

It may be the case that some other data stream class (like a read- or write-only one) will implement each method differently (returning simply false for said cases), but not in this class.

Thus, for an even more generic base class like Object above, a lot of methods do nothing specific in particular, and are optimized into one.

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.