假设下面的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()
调用时,我看到了下面的das(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中,下一个dereferences它(现在我们在vtable中)。
后面的那句是贬义的 该 得到一个指向函数的指针,并在底部粘贴它的 call
ed.
到目前为止还不错,但这带来了几个问题。
哪里是 this
? 我想 this
被装入 rdi
经由 mov
的+70和+74,但这与vtable的指针是一样的,这意味着它是一个指向一个 TriTest
类,它不应该有 SubTest
的b成员。linux的thiscall惯例是在被调用的方法内部处理虚拟铸造,而不是在外部?
如何拆解虚拟方法? 如果我知道这个,我就可以自己回答前面的问题了。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
为什么vtables(显然)之间只有8个字节的距离? 这个 fdump
说在第一和第二之间有16个字节的 &vtable
(这符合64位指针和2个ints),但从第二种解体的 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]
离前一个调用只有8个字节的距离([rbp-0x20]
). 到底是怎么回事?
我忘了对象是堆分配的,只有它们的指针在堆栈上。
声明:我不是GCC内部的专家,但我会试着解释我认为是怎么回事。另外请注意,你没有使用虚拟继承,而是普通的多重继承,所以你的 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]
在64位系统中,就是8+4+4=16个字节。
t3->gB();
现在你使用这个指针来调用 gB()
. 这是用 [0]
的函数,和之前一样。但由于该函数实际上是来自 EvilTest
జజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజ this
指针必须向后移动16个字节,然后才可使用。EvilTest::gB()
可谓。这是由 EvilTest::thunk_gB()
,这是一个读取 VTable[-1]
值,并将该值减去 this
. 现在一切都匹配了!
值得注意的是,完整的VTable中的 EvilTest
是EvilTest-as-BaseTest的VTable加上EvilTest-as-TriTest的VTable的连接。
首先:这个对象不包含一个vtable,它包含一个 指针 到一个vtable。第一个 mov
你所说的不是加载vtable,而是加载了 this
. 第二个 mov
加载指向vtable的指针,该指针位于偏移量为 0
的对象中。
第二件事:在多重继承的情况下,你会得到多个vtables,因为从一个类型到另一个类型的每一次投掷都需要用到 this
的二进制布局,以使其与铸造类型兼容。在这种情况下,您正在铸造 EvilTest*
到 TriTest*
. 那是什么 add rax,0x10
在做什么。
如何拆解虚拟方法? 如果我知道这个,我就可以自己回答前面的问题了。
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):
...
...