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)
Disassembly 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've had suitable time to recover from the deadly diamond in the first code block, the actual question.
When t3->gB()
is called I see the following disas (t3
is type TriTest
, gB()
is virtual method 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 into rax, the next dereferences it (Now we're in the vtable)
The one after that dereferences that to get a pointer to the function and at the bottom of that paste it's call
ed.
So far so good, but this brings a few questions.
Where's this
?
I presume this
is loaded into rdi
via the mov
s at +70 and +74, but that's the same pointer as the vtable which means it's a pointer to a TriTest
class which shouldn't have the SubTest
s b member at all. Does the linux thiscall convention handle virtual casting inside the called method as opposed to outside?
This was answered by rodrigo here
How do I disassemble the 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 the call
, running info reg rax
and disas
sing that 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 the vtables (apparently) only 8 bytes away from eachother?
The fdump
says there are 16 bytes between the first and second &vtable
(Which fits the 64bit pointer and 2 ints) but the dissasembly from the second gB()
call is:
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 away from the previous call ([rbp-0x20]
). What's going on?
Answered by 500 in the comments
I forgot the objects were heap allocated, only their pointers are on the stack