Destruction of virtual methods with multiple inheritance. How does vtable work?

Assuming the following C ++ source file:

#include <stdio.h> class BaseTest { public: int a; BaseTest(): a(2){} virtual int gB() { return a; }; }; class SubTest: public BaseTest { public: int b; SubTest(): b(4){} }; class TriTest: public BaseTest { public: int c; TriTest(): c(42){} }; class EvilTest: public SubTest, public TriTest { public: virtual int gB(){ return b; } }; int main(){ EvilTest * t2 = new EvilTest; TriTest * t3 = t2; printf("%d\n",t3->gB()); printf("%d\n",t2->gB()); return 0; } 

-fdump-class-hierarchy gives me:

 [...] Vtable for EvilTest EvilTest::_ZTV8EvilTest: 6u entries 0 (int (*)(...))0 8 (int (*)(...))(& _ZTI8EvilTest) 16 (int (*)(...))EvilTest::gB 24 (int (*)(...))-16 32 (int (*)(...))(& _ZTI8EvilTest) 40 (int (*)(...))EvilTest::_ZThn16_N8EvilTest2gBEv Class EvilTest size=32 align=8 base size=32 base align=8 EvilTest (0x0x7f1ba98a8150) 0 vptr=((& EvilTest::_ZTV8EvilTest) + 16u) SubTest (0x0x7f1ba96df478) 0 primary-for EvilTest (0x0x7f1ba98a8150) BaseTest (0x0x7f1ba982ba80) 0 primary-for SubTest (0x0x7f1ba96df478) TriTest (0x0x7f1ba96df4e0) 16 vptr=((& EvilTest::_ZTV8EvilTest) + 40u) BaseTest (0x0x7f1ba982bae0) 16 primary-for TriTest (0x0x7f1ba96df4e0) 

Dismantling shows:

 34 int main(){ 0x000000000040076d <+0>: push rbp 0x000000000040076e <+1>: mov rbp,rsp 0x0000000000400771 <+4>: push rbx 0x0000000000400772 <+5>: sub rsp,0x18 35 EvilTest * t2 = new EvilTest; 0x0000000000400776 <+9>: mov edi,0x20 0x000000000040077b <+14>: call 0x400670 < _Znwm@plt > 0x0000000000400780 <+19>: mov rbx,rax 0x0000000000400783 <+22>: mov rdi,rbx 0x0000000000400786 <+25>: call 0x4008a8 <EvilTest::EvilTest()> 0x000000000040078b <+30>: mov QWORD PTR [rbp-0x18],rbx 36 37 TriTest * t3 = t2; 0x000000000040078f <+34>: cmp QWORD PTR [rbp-0x18],0x0 0x0000000000400794 <+39>: je 0x4007a0 <main()+51> 0x0000000000400796 <+41>: mov rax,QWORD PTR [rbp-0x18] 0x000000000040079a <+45>: add rax,0x10 0x000000000040079e <+49>: jmp 0x4007a5 <main()+56> 0x00000000004007a0 <+51>: mov eax,0x0 0x00000000004007a5 <+56>: mov QWORD PTR [rbp-0x20],rax 38 39 printf("%d\n",t3->gB()); 0x00000000004007a9 <+60>: mov rax,QWORD PTR [rbp-0x20] 0x00000000004007ad <+64>: mov rax,QWORD PTR [rax] 0x00000000004007b0 <+67>: mov rax,QWORD PTR [rax] 0x00000000004007b3 <+70>: mov rdx,QWORD PTR [rbp-0x20] 0x00000000004007b7 <+74>: mov rdi,rdx 0x00000000004007ba <+77>: call rax 0x00000000004007bc <+79>: mov esi,eax 0x00000000004007be <+81>: mov edi,0x400984 0x00000000004007c3 <+86>: mov eax,0x0 0x00000000004007c8 <+91>: call 0x400640 < printf@plt > 40 printf("%d\n",t2->gB()); 0x00000000004007cd <+96>: mov rax,QWORD PTR [rbp-0x18] 0x00000000004007d1 <+100>: mov rax,QWORD PTR [rax] 0x00000000004007d4 <+103>: mov rax,QWORD PTR [rax] 0x00000000004007d7 <+106>: mov rdx,QWORD PTR [rbp-0x18] 0x00000000004007db <+110>: mov rdi,rdx 0x00000000004007de <+113>: call rax 0x00000000004007e0 <+115>: mov esi,eax 0x00000000004007e2 <+117>: mov edi,0x400984 0x00000000004007e7 <+122>: mov eax,0x0 0x00000000004007ec <+127>: call 0x400640 < printf@plt > 41 return 0; 0x00000000004007f1 <+132>: mov eax,0x0 42 } 0x00000000004007f6 <+137>: add rsp,0x18 0x00000000004007fa <+141>: pop rbx 0x00000000004007fb <+142>: pop rbp 0x00000000004007fc <+143>: ret 

Now that you had the right time to recover from a deadly diamond in the first block of code, an urgent question.

When t3->gB() is called, I see the following disas ( t3 is type TriTest , gB() is the virtual method of EvilTest::gB() ):

  0x00000000004007a9 <+60>: mov rax,QWORD PTR [rbp-0x20] 0x00000000004007ad <+64>: mov rax,QWORD PTR [rax] 0x00000000004007b0 <+67>: mov rax,QWORD PTR [rax] 0x00000000004007b3 <+70>: mov rdx,QWORD PTR [rbp-0x20] 0x00000000004007b7 <+74>: mov rdi,rdx 0x00000000004007ba <+77>: call rax 

The first mov moves the vtable to rax, the next play it (we are now in the vtable)

After these dereferences, to get a pointer to the function and below, insert its call ed.

So far, so good, but that raises a few questions.

Where is this ?
I assume that this loaded into rdi via mov at +70 and +74, but the same pointer as vtable, which means its pointer to the TriTest class, which should not have SubTest b at all. Does linux thiscall match conditional virtual casting inside the called method, as opposed to external?

Rodrigo answered here

How to parse a virtual method?
If I knew this, I could answer the previous question myself. disas EvilTest::gB gives me:

 Cannot reference virtual member function "gB" 

setting a breakpoint before call , starting info reg rax and disas singing, which gives me:

 (gdb) info reg rax rax 0x4008a1 4196513 (gdb) disas 0x4008a14196513 No function contains specified address. (gdb) disas *0x4008a14196513 Cannot access memory at address 0x4008a14196513 

Why are vtables (apparently) only 8 bytes from eachother?
fdump says that between the first and second &vtable (which corresponds to a 64-bit pointer and 2 targets) there are 16 bytes, but disassembly from the second call to gB() :

  0x00000000004007cd <+96>: mov rax,QWORD PTR [rbp-0x18] 0x00000000004007d1 <+100>: mov rax,QWORD PTR [rax] 0x00000000004007d4 <+103>: mov rax,QWORD PTR [rax] 0x00000000004007d7 <+106>: mov rdx,QWORD PTR [rbp-0x18] 0x00000000004007db <+110>: mov rdi,rdx 0x00000000004007de <+113>: call rax 

[rbp-0x18] is only 8 bytes from the previous call ( [rbp-0x20] ). What's happening?

Answered by 500 in the comments

I forgot that objects were allocated in a heap, only their pointers are on the stack

+6
source share
2 answers

Disclaimer: I am not an expert in the field of GCC, but I will try to explain what, in my opinion, is happening. Also note that you are not using virtual inheritance, but simple multiple inheritance, so your EvilTest object actually contains two BaseTest BaseTest . You can see that this is the case when trying to use this->a in EvilTest : you will get an ambiguous link error.

First of all, remember that each VTable has 2 values ​​at negative offsets:

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

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

With this in mind, I will write VTable from 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 to this point there was nothing interesting.

VTable for EvilTest

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

Now it is interesting! It’s easier to see how it works:

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

This code calls the function in VTable[0] , that is, just EvilTest::gB and everything goes fine.

But then you do:

 TriTest * t3 = t2; 

Since TriTest not the first EvilTest base class, the actual binary value t3 is different from the actual binary value t2 . That is, the cast advances the N byte pointer. The exact amount is known to the compiler at compile time, since it depends only on static expression types. In your code, this is 16 bytes. Note that if the pointer is NULL , it should not be extended, therefore the branch is in the disassembler.

At this stage, it 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 click EvilTest* on TriTest* you need to go this to element [3] , that is, 8 + 4 + 4 = 16 bytes in a 64-bit system.

 t3->gB(); 

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

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

+6
source

First: the object does not contain vtable, it contains a pointer to vtable. The first mov you are talking about does not load vtable, but download this . The second mov loads the pointer to the vtable, which is apparently at offset 0 in the object.

Second: with multiple inheritance, you will get several vtables, because for each transfer from one type to another, this requires a binary layout compatible with the cast type. In this case, you drop EvilTest* in TriTest* . This is what add rax,0x10 does.

+1
source

All Articles