假设以下C ++源文件:
#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
给了我:
[...]
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)
反汇编显示:
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
现在您已经有足够的时间从第一个代码块中的致命钻石中恢复,实际问题。
调用t3->gB()
后,我会看到以下disas(t3
类型为TriTest
,gB()
为虚方法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
第一个mov将vtable移动到rax中,下一个取消引用它(现在我们在vtable中)
之后的那个取消引用 以获取指向该函数的指针,并在该粘贴的底部call
编辑。
到目前为止一切顺利,但这带来了一些问题。
哪里有this
?
我假设this
通过rdi
在+70和+74加载到mov
,但它与vtable的指针相同,这意味着它是指针到一个TriTest
类,根本不应该有SubTest
个成员。 linux thiscall约定是否处理被调用方法中的虚拟强制转换而不是外部?
This was answered by rodrigo here
如何反汇编虚拟方法?
如果我知道这一点,我可以自己回答上一个问题。 disas EvilTest::gB
给了我:
Cannot reference virtual member function "gB"
在call
之前设置断点,运行info reg rax
和disas
唱歌给我:
(gdb) info reg rax
rax 0x4008a1 4196513
(gdb) disas 0x4008a14196513
No function contains specified address.
(gdb) disas *0x4008a14196513
Cannot access memory at address 0x4008a14196513
为什么vtable(显然)距离彼此只有8个字节?
fdump
表示第一个和第二个&vtable
之间有16个字节(适合64位指针和2个整数),但第二个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]
与前一个呼叫([rbp-0x20]
)相距仅8个字节。发生了什么事?
Answered by 500 in the comments
我忘记了对象是堆分配的,只有它们的指针在堆栈上
答案 0 :(得分:6)
免责声明:我不是海湾合作委员会内部的专家,但我会尝试解释我的想法。另请注意,您没有使用虚拟继承,而是使用普通多重继承,因此您的EvilTest
对象实际上包含两个BaseTest
子对象。您可以尝试在this->a
中使用EvilTest
来查看情况:您将收到模糊的引用错误。
首先要知道,每个VTable在负偏移中都有2个值:
-2
:this
偏移量(稍后会详细介绍)。-1
:指向此类的运行时类型信息的指针。然后,从0
开始,将会有指向虚函数的指针:
考虑到这一点,我将编写类的VTable,并且易于阅读:
[-2]: 0
[-1]: typeof(BaseTest)
[ 0]: BaseTest::gB
[-2]: 0
[-1]: typeof(SubTest)
[ 0]: BaseTest::gB
[-2]: 0
[-1]: typeof(TriTest)
[ 0]: BaseTest::gB
到目前为止,没有什么太有趣了。
[-2]: 0
[-1]: typeof(EvilTest)
[ 0]: EvilTest::gB
[ 1]: -16
[ 2]: typeof(EvilTest)
[ 3]: EvilTest::thunk_gB
现在这很有趣!它更容易看到它工作:
EvilTest * t2 = new EvilTest;
t2->gB();
此代码调用VTable[0]
处的函数,即EvilTest::gB
,一切正常。
但是你这样做:
TriTest * t3 = t2;
由于TriTest
不是EvilTest
的第一个基类,t3
的实际二进制值与t2
的实际二进制值不同。也就是说,强制转换使指针N字节前进。编译器在编译时知道确切的数量,因为它仅依赖于表达式的静态类型。在你的代码中它是16个字节。请注意,如果指针是NULL
,则它不能被提前,因此反汇编程序中的分支。
此时有趣的是看到EvilTest
对象的内存布局:
[ 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
正如您所看到的,当您将EvilTest*
投射到TriTest*
时,您必须将this
推进到元素[3]
,即8 + 4 + 4 = 64位系统中的16个字节。
t3->gB();
现在您使用该指针调用gB()
。这是使用VTable的元素[0]
完成的,如前所述。但由于该函数实际上来自EvilTest
,因此this
指针必须在调用EvilTest::gB()
之前移回16个字节。这是EvilTest::thunk_gB()
的工作,这是一个小函数,它读取VTable[-1]
值并将该值减去this
。现在一切都匹配了!
值得注意的是,EvilTest
的完整VTable是EvilTest-as-BaseTest的VTable与EvilTest-as-TriTest的VTable的串联。
答案 1 :(得分:1)
第一件事:对象不包含vtable,它包含一个指针到vtable。您所说的第一个mov
并未加载vtable,而是加载this
。第二个mov
将指针加载到vtable,该vtable似乎位于对象中的偏移0
。
第二件事:使用多重继承,您将获得多个vtable,因为从一种类型转换为另一种类型需要this
以使二进制布局与转换类型兼容。在这种情况下,您要将EvilTest*
投射到TriTest*
。这就是add rax,0x10
正在做的事情。
答案 2 :(得分:0)
如何拆卸虚拟方法?如果我知道这一点,则可以自己回答前面的问题。
disas EvilTest::gB
给了我:Cannot reference virtual member function "gB"
我遇到了同样的问题,并使用断点信息解决了该问题,以获取方法地址以进行反汇编:
(gdb) disassemble cSimpleChannel::deliver(cMessage*, double)
Cannot reference virtual member function "deliver"
(gdb) break cSimpleChannel::deliver
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x000000000003ef50 in cSimpleChannel::deliver(cMessage*, double) at libs/sim/cchannel.cc:345
(gdb) disassemble 0x000000000003ef50
Dump of assembler code for function cSimpleChannel::deliver(cMessage*, double):
...
...