一、准备工作
实验环境及使用的软件版本:
$ uname -a Linux 2.6.32-431.el6.x86_64 #1 SMP Fri Nov 22 03:15:09 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux $ cat /etc/issue CentOS release 6.5 (Final) $ g++ -v Target: x86_64-redhat-linux …… Thread model: posix gcc version 4.4.7 20120313 (Red Hat 4.4.7-17) (GCC) $ gdb -v GNU gdb (GDB) Red Hat Enterprise Linux (7.2-90.el6) …… |
示例代码: virtual_fun.cpp (单一继承模型)
#include <iostream> using namespace std; #define tracepoint() cout<<"line="<<__LINE__<<",func="<<__FUNCTION__<<endl; //基类 class CBase { public: CBase(); ~CBase(); public: virtual void vFun1(); virtual void vFun2(); virtual void vFun3(); public: void baseFun(); }; CBase::CBase() { tracepoint(); } CBase::~CBase() { tracepoint(); } void CBase::vFun1() { tracepoint(); } void CBase::vFun2() { tracepoint(); } void CBase::vFun3() { tracepoint(); } void CBase::baseFun() { tracepoint(); } //派生类 class CDerived:public CBase { public: CDerived(); ~CDerived(); public: virtual void vFun1(); virtual void vFun2(); public: void derivedFun(); }; CDerived::CDerived() { tracepoint(); } CDerived::~CDerived() { tracepoint(); } void CDerived::vFun1() { tracepoint(); } void CDerived::vFun2() { tracepoint(); } void CDerived::derivedFun() { tracepoint(); } int main() { CDerived derived; //通过基类指针调用派生类实现的虚函数 CBase *pBase = &derived; pBase->vFun1(); pBase->vFun2(); //通过基类指针调用派生类未实现的虚函数 pBase->vFun3(); //通过基类指针访问普通成员函数 pBase->baseFun(); return 0; } |
编译并执行一下(为方便后面调试,在编译时加了-g选项):
$ g++ -g virtual_fun.cpp -o virtual_fun $ ./virtual_fun line=22,func=CBase line=60,func=CDerived line=68,func=vFun1 line=72,func=vFun2 line=38,func=vFun3 line=42,func=baseFun line=64,func=~CDerived line=26,func=~CBase |
二、ELF文件虚表分析
先介绍一个知识点:
“g++在编译链接时,会为每个带虚函数的类生成一个虚表,该虚表类似类的静态只读数组成员。生成可执行文件后,类的“虚表地址”、及“虚表里填充的虚函数的地址”和“填充顺序”都已经确定下来,存放存放在可执行文件的.rodata段”
下面我们通过分析可执行文件来查看下:
$ file virtual_fun [注释:查看可执行文件的类型,可看出是ELF格式] virtual_fun: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.18, not stripped [注释:通过以下命令解析ELF可执行文件,分别得到基类、派生类的虚表信息 内容共八列每列对应的含义如下: Num: Value Size Type Bind Vis Ndx Name] $ readelf -Ws virtual_fun | c++filt | grep vtable | grep Cderived 95: 0000000000401020 40 OBJECT WEAK DEFAULT 15 vtable for CDerived $readelf -Ws virtual_fun | c++filt | grep vtable | grep CBase 115: 0000000000401060 40 OBJECT WEAK DEFAULT 15 vtable for Cbase 其中0000000000401020 、 0000000000401060分别对应类的虚表16进制地址。 分别查看下里面的内容: $gdb virtual_fun GNU gdb (GDB) Red Hat Enterprise Linux (7.2-90.el6) [注释:这里省略一些gdb的打印信息,此时函数仅仅是加载到内存,还没有运行哦----] (gdb) x /5xg 0x0000000000401060 [注释:由于大小为40字节,我们打印5个8字节内存即可] 0x401060 <_ZTV5CBase>: 0x0000000000000000 0x00000000004010c0 0x401070 <_ZTV5CBase+16>: 0x0000000000400a4c 0x0000000000400a9e 0x401080 <_ZTV5CBase+32>: 0x0000000000400af0 (gdb) info symbol 0x00000000004010c0 [注释:查看对应地址的符号表] typeinfo for CBase in section .rodata (gdb) info symbol 0x0000000000400a4c CBase::vFun1() in section .text (gdb) info symbol 0x0000000000400a9e CBase::vFun2() in section .text (gdb) info symbol 0x0000000000400af0 CBase::vFun3() in section .text [注释:可以看出虚表的前16个字节存放类的typeinfo信息,从后面开始每八个字节,按照声明的顺序存放一个虚函数的地址,真正的虚函数表地址是从0x401060+0x10 = 0x401070开始的,填充顺序为“基类”中虚函数的声明顺序。从后面的执行期汇编分析可看出,使用虚表时,编译器自动跳过16字节,汇编代码直接使用0x401070这个地址] [注释:同理打印出派生类的虚表信息,真正虚函数表地址是从0x401020+0x10=0x401030开始的。派生类的虚函数延用基类虚函数的排序规则:有则替换,无则使用基类的,新增则按声明顺序往后排,并不改变原有顺序] (gdb) x /5xg 0x0000000000401020 0x401020 <_ZTV8CDerived>: 0x0000000000000000 0x0000000000401090 0x401030 <_ZTV8CDerived+16>: 0x0000000000400cbc 0x0000000000400d0e 0x401040 <_ZTV8CDerived+32>: 0x0000000000400af0 (gdb) info symbol 0x0000000000401090 typeinfo for CDerived in section .rodata (gdb) info symbol 0x0000000000400cbc CDerived::vFun1() in section .text (gdb) info symbol 0x0000000000400d0e CDerived::vFun2() in section .text (gdb) info symbol 0x0000000000400af0 CBase::vFun3() in section .text 亦可直接分析.rodata段来获取上述信息 $ readelf -Wa virtual_fun_01 | grep .rodata | grep PROGBITS [15] .rodata PROGBITS 0000000000400fa0 000fa0 000137 00 A 0 0 32 $(gdb) x /40xg 0x0000000000400fa0 [注释:段大小为0x137= 311,打印40*8 = 320个内存字节即可] 0x400fa0 <_IO_stdin_used>: 0x0000000000020001 0x0000000000000000 0x400fb0 <__dso_handle+8>: 0x662c003d656e696c 0x726564003d636e75 0x400fc0 <_ZZN8CDerived10derivedFunEvE12__FUNCTION__+3>: 0x006e754664657669 0x467600326e754676 0x400fd0 <_ZZN8CDerived5vFun1EvE12__FUNCTION__+2>: 0x6544437e00316e75 0x4443006465766972 0x400fe0 <_ZZN8CDerivedC1EvE12__FUNCTION__+2>: 0x6200646576697265 0x76006e7546657361 0x400ff0 <_ZZN5CBase5vFun3EvE12__FUNCTION__+1>: 0x75467600336e7546 0x316e75467600326e 0x401000 <_ZZN5CBase5vFun1EvE12__FUNCTION__+5>: 0x0065736142437e00 0x0000006573614243 0x401010: 0x0000000000000000 0x0000000000000000 0x401020 <_ZTV8CDerived>: 0x0000000000000000 0x0000000000401090 0x401030 <_ZTV8CDerived+16>: 0x0000000000400cbc 0x0000000000400d0e 0x401040 <_ZTV8CDerived+32>: 0x0000000000400af0 0x0000000000000000 0x401050: 0x0000000000000000 0x0000000000000000 0x401060 <_ZTV5CBase>: 0x0000000000000000 0x00000000004010c0 0x401070 <_ZTV5CBase+16>: 0x0000000000400a4c 0x0000000000400a9e 0x401080 <_ZTV5CBase+32>: 0x0000000000400af0 0x0000000000000000 0x401090 <_ZTI8CDerived>: 0x0000000000601810 0x00000000004010a8 0x4010a0 <_ZTI8CDerived+16>: 0x00000000004010c0 0x6576697265444338 0x4010b0 <_ZTS8CDerived+8>: 0x0000000000000064 0x0000000000000000 0x4010c0 <_ZTI5CBase>: 0x0000000000601690 0x00000000004010d0 0x4010d0 <_ZTS5CBase>: Cannot access memory at address 0x4010d0 |
二、运行期汇编分析
下面我们通过汇编分析代码的执行过程:
(gdb)start [注释:输入该命令让函数执行到main处暂停,类似在main处加断点] Temporary breakpoint 1 at 0x400dbc: file virtual_fun_01.cpp, line 83. Starting program: /home05/23628/tmp/virtual_fun_01 Temporary breakpoint 1, main () at virtual_fun_01.cpp:83 83 CDerived derived; Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.192.el6.x86_64 libgcc-4.4.7-17.el6.x86_64 libstdc++-4.4.7-17.el6.x86_64 (gdb) show disassembly-flavor [注释:输入该命令查看稍后汇编使用的指令集格式] The disassembly flavor is "att". [注释:这里看到是att格式的指令集] (gdb) disassemble [注释:输入该命令汇编一些,查看这种指令集汇编的内容,由于篇幅较长这里列出部分内容;若不熟悉该指令格式可以切换到intel格式汇编] Dump of assembler code for function main(): 0x0000000000400db1 <+0>: push %rbp 0x0000000000400db2 <+1>: mov %rsp,%rbp 0x0000000000400db5 <+4>: push %r12 0x0000000000400db7 <+6>: push %rbx 0x0000000000400db8 <+7>: sub $0x10,%rsp => 0x0000000000400dbc <+11>: lea -0x20(%rbp),%rax 0x0000000000400dc0 <+15>: mov %rax,%rdi …由于篇幅较长,省略部分代码… (gdb) set disassembly-flavor intel [注释:输入该命令切换至intel汇编格式] (gdb) show disassembly-flavor The disassembly flavor is "intel". [注释:下面以intel格式的汇编为例进行分析,熟悉att格式的可参考下面的分析过程] (gdb) disassemble /rm [注释:disassemble命令参数: /m指示显示汇编指令的同时,显示相应的程序源码; /r指示显示十六进制的计算机指令 以下输出每行指示一条汇编指令,除行号和程序源码外共有四列,各列含义为: 1. 0x0000000000400db1: 该指令对应的虚拟内存地址 2.<+0>: 该指令的虚拟内存地址偏移量 3.55: 该指令对应的计算机指令 4.push bp: 汇编指令 =>:带该符号的语句表示,下一步要执行的语句] Dump of assembler code for function main(): 81 { 0x0000000000400db1 <+0>: 55 push rbp 0x0000000000400db2 <+1>: 48 89 e5 mov rbp,rsp 0x0000000000400db5 <+4>: 41 54 push r12 0x0000000000400db7 <+6>: 53 push rbx 0x0000000000400db8 <+7>: 48 83 ec 10 sub rsp,0x10 82 83 CDerived derived; => 0x0000000000400dbc <+11>: 48 8d 45 e0 lea rax,[rbp-0x20] 0x0000000000400dc0 <+15>: 48 89 c7 mov rdi,rax 0x0000000000400dc3 <+18>: e8 cc fd ff ff call 0x400b94 <CDerived::CDerived()> 84 //通过基类指针调用派生类实现的虚函数 85 CBase *pBase = &derived; 0x0000000000400dc8 <+23>: 48 8d 45 e0 lea rax,[rbp-0x20] 0x0000000000400dcc <+27>: 48 89 45 e8 mov QWORD PTR [rbp-0x18],rax 86 pBase->vFun1(); 0x0000000000400dd0 <+31>: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 0x0000000000400dd4 <+35>: 48 8b 00 mov rax,QWORD PTR [rax] 0x0000000000400dd7 <+38>: 48 8b 10 mov rdx,QWORD PTR [rax] 0x0000000000400dda <+41>: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 0x0000000000400dde <+45>: 48 89 c7 mov rdi,rax 0x0000000000400de1 <+48>: ff d2 call rdx 87 pBase->vFun2(); 0x0000000000400de3 <+50>: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 0x0000000000400de7 <+54>: 48 8b 00 mov rax,QWORD PTR [rax] 0x0000000000400dea <+57>: 48 83 c0 08 add rax,0x8 0x0000000000400dee <+61>: 48 8b 10 mov rdx,QWORD PTR [rax] 0x0000000000400df1 <+64>: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 0x0000000000400df5 <+68>: 48 89 c7 mov rdi,rax 0x0000000000400df8 <+71>: ff d2 call rdx 88 89 //通过基类指针调用派生类未实现的虚函数 90 pBase->vFun3(); 0x0000000000400dfa <+73>: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 0x0000000000400dfe <+77>: 48 8b 00 mov rax,QWORD PTR [rax] 0x0000000000400e01 <+80>: 48 83 c0 10 add rax,0x10 0x0000000000400e05 <+84>: 48 8b 10 mov rdx,QWORD PTR [rax] 0x0000000000400e08 <+87>: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 0x0000000000400e0c <+91>: 48 89 c7 mov rdi,rax 0x0000000000400e0f <+94>: ff d2 call rdx 91 92 //通过基类指针访问普通成员函数 93 pBase->baseFun(); 0x0000000000400e11 <+96>: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 0x0000000000400e15 <+100>: 48 89 c7 mov rdi,rax 0x0000000000400e18 <+103>: e8 25 fd ff ff call 0x400b42 <CBase::baseFun()> 94 return 0; 0x0000000000400e1d <+108>: bb 00 00 00 00 mov ebx,0x0 0x0000000000400e22 <+113>: 48 8d 45 e0 lea rax,[rbp-0x20] 0x0000000000400e26 <+117>: 48 89 c7 mov rdi,rax 0x0000000000400e29 <+120>: e8 fa fd ff ff call 0x400c28 <CDerived::~CDerived()> 0x0000000000400e2e <+125>: 89 d8 mov eax,ebx 0x0000000000400e39 <+136>: 89 d3 mov ebx,edx 0x0000000000400e3b <+138>: 49 89 c4 mov r12,rax 0x0000000000400e3e <+141>: 48 8d 45 e0 lea rax,[rbp-0x20] 0x0000000000400e42 <+145>: 48 89 c7 mov rdi,rax 0x0000000000400e45 <+148>: e8 de fd ff ff call 0x400c28 <CDerived::~CDerived()> 0x0000000000400e4a <+153>: 4c 89 e0 mov rax,r12 0x0000000000400e4d <+156>: 48 63 d3 movsxd rdx,ebx 0x0000000000400e50 <+159>: 48 89 c7 mov rdi,rax 0x0000000000400e53 <+162>: e8 40 fa ff ff call 0x400898 <[email protected]> 95 } 0x0000000000400e30 <+127>: 48 83 c4 10 add rsp,0x10 0x0000000000400e34 <+131>: 5b pop rbx 0x0000000000400e35 <+132>: 41 5c pop r12 0x0000000000400e37 <+134>: c9 leave 0x0000000000400e38 <+135>: c3 ret End of assembler dump. [注释:先来看下在栈上创建临时对象derived时发生了什么?] (gdb)si 3[注释:输入该命令,往下执行3条指令,进入CDerived::CDerived()构造函数] CDerived::CDerived (this=0x4007f3) at virtual_fun_01.cpp:58 58 CDerived::CDerived() (gdb) disassemble /rm Dump of assembler code for function CDerived::CDerived(): 58 CDerived::CDerived() => 0x0000000000400b94 <+0>: 55 push rbp 0x0000000000400b95 <+1>: 48 89 e5 mov rbp,rsp 0x0000000000400b98 <+4>: 41 54 push r12 0x0000000000400b9a <+6>: 53 push rbx 0x0000000000400b9b <+7>: 48 83 ec 10 sub rsp,0x10 0x0000000000400b9f <+11>: 48 89 7d e8 mov QWORD PTR [rbp-0x18],rdi 0x0000000000400ba3 <+15>: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 0x0000000000400ba7 <+19>: 48 89 c7 mov rdi,rax [注释:从该汇编中可以看出,派生类先调用基类的构造函数,再执行自身的构造函数] 0x0000000000400baa <+22>: e8 e5 fd ff ff call 0x400994 <CBase::CBase()> 0x0000000000400baf <+27>: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 0x0000000000400bb3 <+31>: 48 c7 00 30 10 40 00 mov QWORD PTR [rax],0x401030 59 { 60 tracepoint(); …… [注释:由于篇幅原因,省略部分对分析无用的汇编代码] End of assembler dump. [注释:继续往下执行运行到基类的构造函数 0x0000000000400baa <+22>: e8 e5 fd ff ff call 0x400994 <CBase::CBase()> ] (gdb) si 9 CBase::CBase (this=0x400f76) at virtual_fun_01.cpp:20 20 CBase::CBase() (gdb) disassemble /rm Dump of assembler code for function CBase::CBase(): 20 CBase::CBase() => 0x0000000000400994 <+0>: 55 push rbp 0x0000000000400995 <+1>: 48 89 e5 mov rbp,rsp 0x0000000000400998 <+4>: 48 83 ec 10 sub rsp,0x10 0x000000000040099c <+8>: 48 89 7d f8 mov QWORD PTR [rbp-0x8],rdi 0x00000000004009a0 <+12>: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] [注释:将基类的虚表指针存入寄存器rax所指的地址内存中] 0x00000000004009a4 <+16>: 48 c7 00 70 10 40 00 mov QWORD PTR [rax],0x401070 21 { 22 tracepoint(); …… [注释:由于篇幅原因,省略部分对分析无用的汇编代码] End of assembler dump. [注释:重点分析这句, 0x401070为基类的虚表指针vptr. 0x00000000004009a4 <+16>: 48 c7 00 70 10 40 00 mov QWORD PTR [rax],0x401070] (gdb) x /3xg 0x401070 [注释:0x401070 该地址即为在ELF文件里分析出的虚函数表的真正地址,编译器汇编时直接使用] 0x401070 <_ZTV5CBase+16>: 0x0000000000400a4c 0x0000000000400a9e 0x401080 <_ZTV5CBase+32>: 0x0000000000400af0 (gdb) info symbol 0x0000000000400a4c [注释:查看地址对应的符号] CBase::vFun1() in section .text of /home05/23628/tmp/virtual_fun_01 (gdb) info symbol 0x0000000000400a9e CBase::vFun2() in section .text of /home05/23628/tmp/virtual_fun_01 (gdb) info symbol 0x0000000000400af0 CBase::vFun3() in section .text of /home05/23628/tmp/virtual_fun_01 (gdb) si 6[注释:继续向下执行6次指令] 22 tracepoint(); (gdb) info r rax[注释:查看rax寄存器的内容] rax 0x7fffffffda70 140737488345712 (gdb) x /xg 0x7fffffffda70[注释:查看该地址对应的内容] 0x7fffffffda70: 0x0000000000401070 [注释:经过以上分析,执行基类的构造函数时,会获取基类的虚函数表(vptr)地址0x401070,并把基类的虚表地址存到地址为0x7fffffffda70的内存单元中] (gdb) b *0x0000000000400bb3[注释:返回到上一层派生类的构造函数,继续往下执行,在该地址处加断点] Breakpoint 2 at 0x400bb3: file virtual_fun_01.cpp, line 58. (gdb) c Continuing. line=22,func=Cbase[注释:基类的构造函数已执行] Breakpoint 2, 0x0000000000400bb3 in CDerived::CDerived (this=0x7fffffffda70) at virtual_fun_01.cpp:58 58 CDerived::CDerived() [注释:该语句rax被赋值 0x0000000000400baf <+27>: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 即将执行下面这条语句,0x401030为派生类的虚表指针 0x0000000000400bb3 <+31>: 48 c7 00 30 10 40 00 mov QWORD PTR [rax],0x401030 ] (gdb) info r rax[注释:查看rax寄存器的值] rax 0x7fffffffda70 140737488345712 [注释:该值同分析基类时rax寄存器的值相同] (gdb) x /3xg 0x401030[注释:输入该命令,查看内存对应的内容] 0x401030 <_ZTV8CDerived+16>: 0x0000000000400cbc 0x0000000000400d0e 0x401040 <_ZTV8CDerived+32>: 0x0000000000400af0 (gdb) info symbol 0x0000000000400cbc[注释:查看地址对应的符号] CDerived::vFun1() in section .text of /home05/23628/tmp/virtual_fun_01 (gdb) info symbol 0x0000000000400d0e[注释:查看地址对应的符号] CDerived::vFun2() in section .text of /home05/23628/tmp/virtual_fun_01 (gdb) info symbol 0x0000000000400af0[注释:查看地址对应的符号] CBase::vFun3() in section .text of /home05/23628/tmp/virtual_fun_01[注释:由于派生类没有实现虚函数vFun3,这里仍然为基类的虚函数] (gdb) si [注释:执行一次指令] 60 tracepoint(); (gdb) info r rax[注释:查看rax寄存器的值] rax 0x7fffffffda70 140737488345712 (gdb) x /xg 0x7fffffffda70[注释:查看该地址的内存值] 0x7fffffffda70: 0x0000000000401030 [注释:该值已变为派生类的虚表指针 [注释:经过以上分析,派生类在执行构造函数时,先调用基类的构造函数,获取基类的虚表指针,并执行基类构造函数体。然后派生类的虚表指针会覆盖基类的虚表指针,再执行派生类构造函数体] (gdb) b *0x0000000000400dc8 [注释:继续往下执行,在该处打断点] Breakpoint 2 at 0x400dc8: file virtual_fun_01.cpp, line 85. (gdb) c Continuing. line=60,func=CDerived Breakpoint 2, main () at virtual_fun_01.cpp:85 85 CBase *pBase = &derived; [注释:即将执行下面的指令: 84 //通过基类指针调用派生类实现的虚函数 85 CBase *pBase = &derived; =>0x0000000000400dc8 <+23>: 48 8d 45 e0 lea rax,[rbp-0x20] 0x0000000000400dcc <+27>: 48 89 45 e8 mov QWORD PTR [rbp-0x18],rax ] (gdb) info r rbp[注释:查看rbp寄存器的值] rbp 0x7fffffffda90 0x7fffffffda90 (gdb) si 0x0000000000400dcc 85 CBase *pBase = &derived; (gdb) info r rax[注释查看rax寄存器的值] rax 0x7fffffffda70 140737488345712 (gdb) si 86 pBase->vFun1(); (gdb) x /xg 0x7fffffffda78[注释:查看rbp-0x18内存中的值] 0x7fffffffda78: 0x00007fffffffda70 [注释:即将执行下面的指令 86 pBase->vFun1(); 0x0000000000400dd0 <+31>: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 0x0000000000400dd4 <+35>: 48 8b 00 mov rax,QWORD PTR [rax] 0x0000000000400dd7 <+38>: 48 8b 10 mov rdx,QWORD PTR [rax] 0x0000000000400dda <+41>: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 0x0000000000400dde <+45>: 48 89 c7 mov rdi,rax 0x0000000000400de1 <+48>: ff d2 call rdx ] (gdb) si 0x0000000000400dd4 86 pBase->vFun1(); (gdb) info r rax rax 0x7fffffffda70 140737488345712 (gdb) si 0x0000000000400dd7 86 pBase->vFun1(); (gdb) info r rax rax 0x401030 4198448 [注释:0x401030为派生类虚表地址] (gdb) si 0x0000000000400dda 86 pBase->vFun1(); (gdb) info r rdx rdx 0x400cbc 4197564[注释:找到派生类虚函数vFun1的地址] (gdb) info symbol 0x400cbc CDerived::vFun1() in section .text of /home05/23628/tmp/virtual_fun_01 (gdb) si 0x0000000000400dde 86 pBase->vFun1(); (gdb) info r rax rax 0x7fffffffda70 140737488345712 (gdb) x /xg 0x7fffffffda70 0x7fffffffda70: 0x0000000000401030 [注释:0x401030为派生类虚表地址] (gdb) si 0x0000000000400de1 86 pBase->vFun1(); (gdb) info r rdi rdi 0x7fffffffda70 140737488345712 [注释:接下来要执行的 call rdx,即为调用派生类虚函数vFun1] [注释:接下来继续分析pBase->vFun2()的执行过程,对应的汇编代码如下: 87 pBase->vFun2(); => 0x0000000000400de3 <+50>: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 0x0000000000400de7 <+54>: 48 8b 00 mov rax,QWORD PTR [rax] 0x0000000000400dea <+57>: 48 83 c0 08 add rax,0x8 0x0000000000400dee <+61>: 48 8b 10 mov rdx,QWORD PTR [rax] 0x0000000000400df1 <+64>: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 0x0000000000400df5 <+68>: 48 89 c7 mov rdi,rax 0x0000000000400df8 <+71>: ff d2 call rdx ] (gdb) b *0x0000000000400de3 Breakpoint 3 at 0x400de3: file virtual_fun_01.cpp, line 87. (gdb) c Continuing. line=68,func=vFun1 Breakpoint 3, main () at virtual_fun_01.cpp:87 87 pBase->vFun2(); (gdb) si 0x0000000000400de7 87 pBase->vFun2(); (gdb) info r rax rax 0x7fffffffda70 140737488345712 (gdb) si 0x0000000000400dea 87 pBase->vFun2(); (gdb) info r rax rax 0x401030 4198448 [注释: 0x401030派生类的虚表指针] (gdb) si 0x0000000000400dee 87 pBase->vFun2(); (gdb) info r rax rax 0x401038 4198456 [注释:虚表指针+8] (gdb) si 0x0000000000400df1 87 pBase->vFun2(); (gdb) info r rdx rdx 0x400d0e 4197646 [注释: 0x400d0e派生类的虚函数vFun2地址] (gdb) si 0x0000000000400df5 87 pBase->vFun2(); (gdb) info r rax rax 0x7fffffffda70 140737488345712 (gdb) si 0x0000000000400df8 87 pBase->vFun2(); (gdb) info r rdi rdi 0x7fffffffda70 140737488345712 (gdb) [注释:接下来要执行的 call rdx,即为调用派生类虚函数vFun2] [注释:接下来继续分析pBase->vFun3()的执行过程,派生类未实现该函数,对应的汇编如下: 90 pBase->vFun3(); 0x0000000000400dfa <+73>: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 0x0000000000400dfe <+77>: 48 8b 00 mov rax,QWORD PTR [rax] 0x0000000000400e01 <+80>: 48 83 c0 10 add rax,0x10 0x0000000000400e05 <+84>: 48 8b 10 mov rdx,QWORD PTR [rax] 0x0000000000400e08 <+87>: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 0x0000000000400e0c <+91>: 48 89 c7 mov rdi,rax 0x0000000000400e0f <+94>: ff d2 call rdx ] (gdb) b *0x0000000000400dfa Breakpoint 4 at 0x400dfa: file virtual_fun_01.cpp, line 90. (gdb) c Continuing. line=72,func=vFun2 Breakpoint 4, main () at virtual_fun_01.cpp:90 90 pBase->vFun3(); (gdb) si 0x0000000000400dfe 90 pBase->vFun3(); (gdb) info r rax rax 0x7fffffffda70 140737488345712 (gdb) si 0x0000000000400e01 90 pBase->vFun3(); (gdb) info r rax rax 0x401030 4198448 [注释: 0x401030派生类的虚表指针] (gdb) si 0x0000000000400e05 90 pBase->vFun3(); (gdb) info r rax rax 0x401040 4198464 [注释:虚表指针+16] (gdb) si 0x0000000000400e08 90 pBase->vFun3(); (gdb) info r rdx rdx 0x400af0 4197104[注释: 0x400af0指向基类虚函数vFun3地址] (gdb) si 0x0000000000400e0c 90 pBase->vFun3(); (gdb) info r rax rax 0x7fffffffda70 140737488345712 (gdb) si 0x0000000000400e0f 90 pBase->vFun3(); (gdb) info r rdi rdi 0x7fffffffda70 140737488345712 (gdb) [注释:接下来要执行的 call rdx,即为调用指向基类的虚函数vFun3] [注释:经过以上分析,通过基类指针访问派生类虚函数时,先找到派生类的虚函数表,然后根据偏移(该偏移是编译器在编译连接过程中计算好的)访问具体的虚函数; 调用普通函数时,从汇编代码可以看出是直接调用对应的函数地址的: 0x0000000000400e18 <+103>: e8 25 fd ff ff call 0x400b42 <CBase::baseFun()> ] |
三、虚函数机制的应用
有兴趣的童鞋可以使用上述原理分析下下面常见的问题:
1、 当将CBase的析构函数声明为虚函数时,通过汇编可看出基类和派生类虚函数表会各有两个虚析构函数,为什么?析构的时候什么时候用第一个?什么时候用第二个?
2、 有如下调用关系的的程序,如果库A中带有虚函数的基类,如果在虚函数之间增加一个新的虚函数,只更新main调用时的A的头文件,库B中A的头文件不更新,程序可否正常运行?
3、 根据2的实验结果,作为对外头文件时,如何增加虚函数才稳妥?部分库设计时会预留部分虚函数接口这样做有什么好处?
4、 如果基类和派生类虚函数有默认参数,且默认参数不同时。通过基类指针调用派生类继承的虚函数时(默认参数),能否得到想要的结果?[提示:用汇编分析下栈帧及传参机制,顺便可看下this指针是如何传递的]
5、 为何EffectiveC++中明确说“绝不在构造和析构过程中调用virtual函数”?[提示:写个小例子用汇编分析下看]
6、 若一个带有虚函数的对象A,sizeof(A)大小为多少?为什么?
7、 若程序崩溃,通过堆栈分析到时该执行虚函数时,没有执行导致的崩溃,该从哪里入手定位?