0x00000000004006fb
0x00000000004006fe
0x00000000004006ff
//传入需要开辟的内存空间大小,为8字节,通过%edi寄存器传递
0x0000000000400703
0x0000000000400708
//开辟的内存空间首地址通过%rax返回,再次赋值给%rdi,调用构造函数
0x000000000040070d
0x0000000000400710
0x0000000000400713
//将首地址保存为局部变量,判断开辟的内存空间是否为空
0x0000000000400718
0x000000000040071c
0x0000000000400721
//这里开始准备调用析构函数,this指针赋给%rax
0x0000000000400723
//将this指针所指内存区域的首8个字节,赋值给%rax,这时%rax内的值,是一张虚标的首地址
0x0000000000400727
//将虚表首地址的值加8,实际上是跳到了虚表的第二项,因为虚表的每一项是8个字节
0x000000000040072a
//将第二项的内容,即析构函数的地址赋值给%rax,并将this首地址赋值给%rdi
0x000000000040072e
0x0000000000400731
//调用这个析构函数
0x0000000000400735
// 平衡堆栈,返回
0x0000000000400737
0x000000000040073c
0x0000000000400740
0x0000000000400741
0x0000000000400742
这里就产生了一个疑问,从CChinese的继承关系来看,需表内应该只有两项,就是析构函数和ShowSpeak。析构函数应该处于虚表的第一个位置,这里为什么要跳到第二个表项呢?那么第一个表项到底是什么?它总共有几个表现呢?下面,我们用gdb走一遍,将其虚表里面的信息都整理出来。
//执行到断点后,采用info reg命令查看到rip的地址,然后用x/10i $rip来查看反汇编代码
//取得存放this指针的地址
(gdb) p $rbp-0x10
$2 = (void *) 0x7fffbb5b6640
//this的值为0x48d3010
(gdb) x/1xg 0x7fffbb5b6640
0x7fffbb5b6640: 0x00000000048d3010
//虚表的地址为0x4009b0
(gdb) x/10xg 0x00000000048d3010
0x48d3010: 0x00000000004009b0 0x0000000000000000
//虚表第一项值为0x40085a,第二项的值为0x4007cc,第三项的值为0x400788
(gdb) x/10g 0x00000000004009b0
0x4009b0 <_ZTV8CChinese+16>: 0x000000000040085a 0x00000000004007cc
0x4009c0 <_ZTV8CChinese+32>: 0x0000000000400788 0x0000000000000000
(gdb) x/20i 0x000000000040085a
0x40085a <~CChinese>: push %rbp
0x40085b <~CChinese+1>: mov %rsp,%rbp
0x40085e <~CChinese+4>: sub $0x10,%rsp
0x400862 <~CChinese+8>: mov %rdi,-0x8(%rbp)
//调用CChinese的析构函数,由于每个析构函数内,都需要将属于这个类的虚表地址重新赋值一遍,
//因此这里会赋予0x4009b0,刚好是CChinese的虚表地址
0x400866 <~CChinese+12>: mov $0x4009b0,%edx
0x40086b <~CChinese+17>: mov -0x8(%rbp),%rax
0x40086f <~CChinese+21>: mov %rdx,(%rax)
//调用CPerson的析构函数
0x400872 <~CChinese+24>: mov -0x8(%rbp),%rdi
0x400876 <~CChinese+28>: callq 0x4007a0 <~CPerson>
0x40087b <~CChinese+33>: mov $0x0,%eax
//由于eax为0,因此后面的test和je肯定会跳转到0x40088d,即越过对_ZdlPv@plt的调用
0x400880 <~CChinese+38>: test %al,%al
0x400882 <~CChinese+40>: je 0x40088d <~CChinese+51>
0x400884 <~CChinese+42>: mov -0x8(%rbp),%rdi
0x400888 <~CChinese+46>: callq 0x4005c0 <_ZdlPv@plt>
0x40088d <~CChinese+51>: leaveq
0x40088e <~CChinese+52>: retq
//这个函数和上面相比,唯一的区别是调用了_ZdPv@plt
(gdb) x/20i 0x00000000004007cc
0x4007cc <~CChinese>: push %rbp
0x4007cd <~CChinese+1>: mov %rsp,%rbp
0x4007d0 <~CChinese+4>: sub $0x10,%rsp
0x4007d4 <~CChinese+8>: mov %rdi,-0x8(%rbp)
0x4007d8 <~CChinese+12>: mov $0x4009b0,%edx
0x4007dd <~CChinese+17>: mov -0x8(%rbp),%rax
0x4007e1 <~CChinese+21>: mov %rdx,(%rax)
0x4007e4 <~CChinese+24>: mov -0x8(%rbp),%rdi
0x4007e8 <~CChinese+28>: callq 0x4007a0 <~CPerson>
//会调用_ZdlPv@plt
0x4007ed <~CChinese+33>: mov $0x1,%eax
0x4007f2 <~CChinese+38>: test %al,%al
0x4007f4 <~CChinese+40>: je 0x4007ff <~CChinese+51>
0x4007f6 <~CChinese+42>: mov -0x8(%rbp),%rdi
0x4007fa <~CChinese+46>: callq 0x4005c0 <_ZdlPv@plt>
0x4007ff <~CChinese+51>: leaveq
0x400800 <~CChinese+52>: retq
现在一切都明白了,_ZdPv@plt是用于释放内存的实现,由于CChinese的内存是被new出来的,因此需要采用带_ZdPv@plt调用的析构函数版本来实现析构;而该版本的析构在虚表里面处于第二项,因此在外部调用析构时,需要add 0x08
虚表的第三项就是ShowSpeak函数的地址
(gdb) x/20i 0x0000000000400788
0x400788 <_ZN8CChinese9ShowSpeakEv>: push %rbp
0x400789 <_ZN8CChinese9ShowSpeakEv+1>: mov %rsp,%rbp
0x40078c <_ZN8CChinese9ShowSpeakEv+4>: sub $0x10,%rsp
0x400790 <_ZN8CChinese9ShowSpeakEv+8>: mov %rdi,-0x8(%rbp)
0x400794 <_ZN8CChinese9ShowSpeakEv+12>: mov $0x400990,%edi
0x400799 <_ZN8CChinese9ShowSpeakEv+17>: callq 0x4005b0
0x40079e <_ZN8CChinese9ShowSpeakEv+22>: leaveq
0x40079f <_ZN8CChinese9ShowSpeakEv+23>: retq
(gdb) x/20i 0x4007a0
0x4007a0 <~CPerson>: push %rbp
0x4007a1 <~CPerson+1>: mov %rsp,%rbp
0x4007a4 <~CPerson+4>: sub $0x10,%rsp
0x4007a8 <~CPerson+8>: mov %rdi,-0x8(%rbp)
//这里面又用CPerson的虚表地址覆盖了虚表指针
0x4007ac <~CPerson+12>: mov $0x400a30,%edx
0x4007b1 <~CPerson+17>: mov -0x8(%rbp),%rax
0x4007b5 <~CPerson+21>: mov %rdx,(%rax)
//不用释放内存块
0x4007b8 <~CPerson+24>: mov $0x0,%eax
0x4007bd <~CPerson+29>: test %al,%al
0x4007bf <~CPerson+31>: je 0x4007ca <~CPerson+42>
0x4007c1 <~CPerson+33>: mov -0x8(%rbp),%rdi
0x4007c5 <~CPerson+37>: callq 0x4005c0 <_ZdlPv@plt>
0x4007ca <~CPerson+42>: leaveq
0x4007cb <~CPerson+43>: retq
再来看看Speak函数,分析一下虚函数是如何被调用的
(gdb) disas Speak
Dump of assembler code for function _Z5SpeakP7CPerson:
0x00000000004006d8 <_Z5SpeakP7CPerson+0>: push %rbp
0x00000000004006d9 <_Z5SpeakP7CPerson+1>: mov %rsp,%rbp
0x00000000004006dc <_Z5SpeakP7CPerson+4>: sub $0x10,%rsp
0x00000000004006e0 <_Z5SpeakP7CPerson+8>: mov %rdi,-0x8(%rbp)
//rax里面放了this指针的值
0x00000000004006e4 <_Z5SpeakP7CPerson+12>: mov -0x8(%rbp),%rax
//this指针所指对象首部8个字节赋值给了rax,其实就是虚表指针
0x00000000004006e8 <_Z5SpeakP7CPerson+16>: mov (%rax),%rax
//虚表指针往后偏移16个字节,相当于是虚表的第三项,赋值给rax
0x00000000004006eb <_Z5SpeakP7CPerson+19>: add $0x10,%rax
0x00000000004006ef <_Z5SpeakP7CPerson+23>: mov (%rax),%rax
//调用第三个虚表项所对应的函数
0x00000000004006f2 <_Z5SpeakP7CPerson+26>: mov -0x8(%rbp),%rdi
0x00000000004006f6 <_Z5SpeakP7CPerson+30>: callq *%rax
0x00000000004006f8 <_Z5SpeakP7CPerson+32>: leaveq
0x00000000004006f9 <_Z5SpeakP7CPerson+33>: retq