一、准备工作
实验环境及使用的软件版本:
$ 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开始的。派生类的虚函数延用基类虚函数的排序规则:有则替换,无则使用基类的,新增则按声明顺序往后排,并不改变原有顺序] |