天天看点

C++虚函数机制(Linux版)

一、准备工作

实验环境及使用的软件版本:

$ 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、  若程序崩溃,通过堆栈分析到时该执行虚函数时,没有执行导致的崩溃,该从哪里入手定位?

继续阅读