最近,看帖发现不少人对虚函数表很迷惑,说虚函数表中函数的地址怎么和用函数指针获取的地址不一致?示例代码如下:
#include<iostream> #include<stdio.h> using namespace std; class Base { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } }; typedef void(Base::*Fun)(void); Fun pFun = NULL; int main() { Base b; printf("%p/n",(int*)*(int*)*(int*)(&b)); pFun = &Base::f; printf("%p/n",pFun); return 0; }
运行结果是:
004010F0004019D0请按任意键继续. . .
同是虚函数f的地址,为啥打印出来就不一样呢?
其实,事实的真相并不像你想象的那样,(int*)*(int*)*(int*)(&b)是虚函数表中的虚函数f地址,但是它并不是虚函数f真正的地址,而是编译后程序符号表段中的该函数索引的地址,通过符号表中的索引找到虚函数f对应的表项,其中有一项就是存储这其真正的函数地址;而
可能现在有人开始迷惑了?程序的符号表段是啥?
答案其实也很简单,符号表段是程序编译连接后产生的一个段,用于标识程序中全局静态变量、函数等符号和其真实地址之间的映射,就像代码段、数据段一样是程序编译连接后的一部分!
不过上述只是我们的设想,但如果没有示例证明,那么一切都只是“浮云”。那我们就看看下面代码编译后虚函数f具体的表现形式。示例代码:
#include<iostream>using namespace std;
class Base {
public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } };
typedef void(Base::*Fun)(void); Fun pFun = NULL;
int main(){ Base b; b.f(); printf("通过虚函数表获取的函数地址:/n");
printf("%p/n",(int*)*(int*)*(int*)(&b));
printf("/n通过函数指针赋值获取的函数地址:/n"); pFun = &Base::f; printf("%p/n/n",pFun);
return 0;}运行结果:Base::f通过虚函数表获取的函数地址:004111D1
通过函数指针赋值获取的函数地址:004112AD
请按任意键继续. . .
可以看到两种方式获取的地址值是完全不一样的!为什么会是这样呢?到底哪个才是函数真正的地址?好,先别急,我们用强大的IDA反编译来看个究竟!首先,用IDA打开上面示例代码生成的exe文件,分别找到地址004111D1和004112AD所在的代码:1、地址004111D1处
IDAview汇编显示:; Attributes: thunk
; void __thiscall Base__f(Base *this)j_?f@Base@@UAEXXZ proc nearjmp ?f@Base@@UAEXXZ ; Base::f(void)j_?f@Base@@UAEXXZ endp
反编译后结果:void __thiscall Base__f(Base *this){ Base__f(this);}
双击Base__f(this);,继续跟进,终于找到了虚函数f的定义,但是f函数真正的定义位于地址004116D0处:void __thiscall Base__f(Base *this){ std::basic_ostream<char,std::char_traits<char> > *v1; // eax@1 char v2; // [sp+Ch] [bp-CCh]@1 Base *v3; // [sp+D0h] [bp-8h]@1
memset(&v2, -858993460, 0xCCu); v3 = this; v1 = std__operator___std__char_traits_char__(std__cout, "Base::f"); std__basic_ostream_char_std__char_traits_char____operator__(v1, std__endl); j___RTC_CheckEsp(); j___RTC_CheckEsp();}因此,从上面分析可知,通过虚函数表获取的f函数的地址并不是f函数真正的入口地址,而是通过一个中间过渡函数跳转到f的入口地址。为了便于理解,我们这里称地址004111D1为函数符号映射地址,其实代码中对函数f不是通过函数指针而是直接符号调用的,其最终都会进入地址004111D1,然后再跳转到真正的函数入口地址。不信!我们可以看看main函数中语句b.f();的调用用方式为“call j_?f@Base@@UAEXXZ ; Base::f(void)”,如下加粗红色语句:
; Attributes: bp-based frame
; int __cdecl main()_main proc near
var_CC= byte ptr -0CChb= Base ptr -8
push ebpmov ebp, espsub esp, 0CChpush ebxpush esipush edilea edi, [ebp+var_CC]mov ecx, 33hmov eax, 0CCCCCCCChrep stosdlea ecx, [ebp+b] ; thiscall j_??0Base@@QAE@XZ ; Base::Base(void)lea ecx, [ebp+b] ; thiscall j_?f@Base@@UAEXXZ ; Base::f(void) // b.f()编译后调用形式mov esi, esppush offset Format ; "通?call ds:__imp__printfadd esp, 4cmp esi, espcall j___RTC_CheckEspmov esi, espmov eax, [ebp+b.vtable]mov ecx, [eax]push ecxpush offset aP_0 ; "%p/n"call ds:__imp__printfadd esp, 8cmp esi, espcall j___RTC_CheckEspmov esi, esppush offset aI ; "/n通?call ds:__imp__printfadd esp, 4cmp esi, espcall j___RTC_CheckEspmov ?pFun@@3P8Base@@AEXXZQ1@, offset j_??_9Base@@$BA@AE ; [thunk]: Base::`vcall'{0,{flat}}mov esi, espmov eax, ?pFun@@3P8Base@@AEXXZQ1@ ; void (Base::*pFun)(void)push eaxpush offset aP ; "%p/n/n"call ds:__imp__printfadd esp, 8cmp esi, espcall j___RTC_CheckEspxor eax, eaxpush edxmov ecx, ebp ; framepush eaxlea edx, v ; vcall j_@_RTC_CheckStackVars@8 ; _RTC_CheckStackVars(x,x)pop eaxpop edxpop edipop esipop ebxadd esp, 0CChcmp ebp, espcall j___RTC_CheckEspmov esp, ebppop ebpretn
因此,通过上面的分析,可以得出结论:通过虚函数表获取的函数指针是函数符号的映射地址,也可认为是函数真正的调用地址。但通过函数指针获取的地址又是什么呢?好,我们下面进一步分析。
2、地址004112AD处
看看该程序地址004112AD处的汇编代码:; Attributes: thunk
; [thunk]: __thiscall Base::`vcall'{0,{flat}}j_??_9Base@@$BA@AE proc nearjmp ??_9Base@@$BA@AE ; [thunk]: Base::`vcall'{0,{flat}}j_??_9Base@@$BA@AE endp
反编译后代码:int __cdecl Base___vcall_(){ return Base___vcall_();}继续跟进,发现内部调用Base___vcall_()位于地址00411740处,反编译后代码如下:int __thiscall Base___vcall_(void *this){ return (**(int (***)(void))this)();}
哈哈,这时是否有点明白了!总的来说,函数指针pFun所指向的也并不是虚函数f的入口地址,而是编译器做了一个令人意想不到的处理,在代码编译后,会针对每个函数指针的类型,定义各自的调用函数,而且每个调用函数也不是真的指向原函数的真正的入口地址。原理:类的成员函数指针和普通函数指针不一样,成员函数指针是一个结构体指针,里面包含了偏移量,标志(是否是虚函数),真实地址等。怎么样?经过上面的分析,是否让你大跌眼镜!所有的一切均和起初预想的完全不一样!原因是你对程序编译想象的太简单了!其实,编译器为了优化和实现c++的某些特性,做了你根本想象不到的事情!而且不同的编译器编译的策略也是不一样的,所以这个代码如果用g++编译,可能又有另一番奇特景象!
综上所述,虚函数表中所指向的函数地址和函数指针所指向的地址都不是该函数的真正入口地址,虚函数表所指向的函数地址是该函数的符号地址;而函数指针所指向的地址则是编译器为了满足函数指针类型定义而生成的函数指针的调用地址。总之,他们是无法进行比较的!
如果你还是锱铢必较,请省点力气,因为里面的模型很是复杂,不同的编译器处理方式也不一样!建议你找一本《程序员自我修养》,看看就会恍然大悟,叫道:“何以解忧,唯有杜康!”。
ok!结贴!