Dissassembling virtual methods in multiple inheritance. How is the vtable working?

Disclaimer: I'm no expert in the GCC internal, but I'll try to explain what I think is going on. Also note that you are not using virtual inheritance, but plain multiple inheritance, so your EvilTest object actually contains two BaseTest subobjects. You can see that is the case by trying to use this->a in EvilTest: you'll get an ambiguous reference error.

First of all be aware that every VTable has 2 values in the negative offsets:

  • -2: the this offset (more on this later).
  • -1: pointer to run-time type information for this class.

Then, from 0 on, there will be the pointers to virtual functions:

With that in mind, I'll write the VTable of the classes, with easy to read names:

VTable for BaseTest:

[-2]: 0
[-1]: typeof(BaseTest)
[ 0]: BaseTest::gB

VTable for SubTest:

[-2]: 0
[-1]: typeof(SubTest)
[ 0]: BaseTest::gB

VTable for TriTest

[-2]: 0
[-1]: typeof(TriTest)
[ 0]: BaseTest::gB

Up until this point nothing too interesting.

VTable for EvilTest

[-2]: 0
[-1]: typeof(EvilTest)
[ 0]: EvilTest::gB
[ 1]: -16
[ 2]: typeof(EvilTest)
[ 3]: EvilTest::thunk_gB

Now that is interesting! It is easier to see it working:

EvilTest * t2 = new EvilTest;
t2->gB();

This code calls the function at VTable[0], that is simply EvilTest::gB and all goes fine.

But then you do:

TriTest * t3 = t2;

Since TriTest is not the first base class of EvilTest, the actual binary value of t3 is different from that of t2. That is, the cast advances the pointer N bytes. The exact amount is known by the compiler at compile time, because it depends only on the static types of the expressions. In your code it is 16 bytes. Note that if the pointer is NULL, then it must not be advanced, thus the branch in the disassembler.

At this point is interesting to see the memory layout of the EvilTest object:

[ 0]: pointer to VTable of EvilTest-as-BaseTest
[ 1]: BaseTest::a
[ 2]: SubTest::b
[ 3]: pointer to VTable of EvilTest-as-TriTest
[ 4]: BaseTest::a
[ 5]: TriTest::c

As you can see, when you cast a EvilTest* to a TriTest* you have to advance this to the element [3], that is 8+4+4 = 16 bytes in a 64-bit system.

t3->gB();

Now you use that pointer to call the gB(). That is done using the element [0] of the VTable, as before. But since that function is actually from EvilTest, the this pointer must be moved back 16 bytes before EvilTest::gB() can be called. That is the work of EvilTest::thunk_gB(), this is a little function that reads the VTable[-1] value and substract that value to this. Now everything matches!

It is worth noting that the full VTable of EvilTest is the concatenation of the VTable of EvilTest-as-BaseTest plus the VTable of EvilTest-as-TriTest.


First thing: the object doesn't contain a vtable, it contains a pointer to a vtable. The first mov you speak of isn't loading the vtable, it's loading this. The second mov loads the pointer to the vtable which appears to be at offset 0 in the object.

Second thing: with multiple inheritance you will get multiple vtables, because each cast from one type to another requires this to have a binary layout compatible with the casted type. In this case you're casting EvilTest* to TriTest*. That's what the add rax,0x10 is doing.