讨论虚拟函数的技术内幕——后期联编(Late binding),
一,进入内存
首先,一个含有虚拟函数的类在内存中的结构。
假设一个这样的类:
class CShape
{
int b1;
public:
void MyTest()
{
cout << "CShape::MyTest /n";
}
};
在栈区,它仅仅只是占据了四个字节,用于存放成员数据——b1。 sizeof(CShape)
奇怪,那么它的成员函数在那里呢?在VC++6.0,对于普通的成员函数,它将CShape::MyTest()编修改为:“?MyTest@CTestA@@QAEXXZ”,真是个奇怪的名字,但是在这个名字中却保存了重要的信息,比如所属类,参数类型等。
另外的一个类:
class CShape_V
{
int b1;
public:
virtual void play()
{
cout << "CShape::play /n";
}
virtual void display()
{
cout <<b1<< "Shape /n";
}
};
在栈区,它占据了八个字节,用于存放成员数据b1和指向一个一维数组首地址的指针。
二,vtable
为了达到后期联编的目的,VC编译器通过一个表,在执行期间接地调用了实际上需要调用的函数(注意是“间接”),这个表可称为“虚拟函数地址表”(在很多影印版的图书中常称之为vtable),每个类中含有虚拟函数的对象,编译器都会为它们指定一个虚拟函数地址表,虚拟函数地址表是个函数指针数组,保存在数据区,它由此类对象所共用(静态)。此外,编译器当然也会为它加上一个成员变量,一个指向自己的“虚拟函数地址表”的指针(常称之为vptr),并且放在了对象的首地址上。
每一个由此类分配出的对象,都有这么个vptr,每当我们通过这个对象调用虚拟函数时,实际上是通过vptr找到vtable,再通过偏移量找出真正的函数地址。
奥妙在于这个vtable以及这种间接调用方式,vtable是按照类中虚拟函数声明的顺序,一一填入函数地址。派生类会继承基类的vtable(当然还有其他可以继承的成员),当我们在派生类里修改了虚拟函数时,派生类的vtable中的内容也被修改,表中相应的元素不在是基类的函数地址,而是派生类的函数地址
三,分析:
//vTest.cpp
#include <iostream.h>
//--------------------------------------------
class CShape
{
int b1;
public:
void MyTest(){
cout << "CShape::MyTest /n";
}
virtual void play()
{
cout << "CShape::play /n";
}
virtual void display()
{
cout <<b1<< "Shape /n";
}
};
//--------------------------------------------
class int b2;
public:
void MyTest()
{
cout << "CRect::MyTest /n";
}
void display()
{
cout <<b2<< "Rectangle /n";
}
};
//--------------------------------------------
class int b3;
public:
void MyTest()
{
cout << "CSquare::MyTest /n";
}
void display()
{
cout <<b3<< "Square /n";
}
};
//--------------------------------------------
void main()
{
CShape aShape;
CRect aRect;
CSquare aSquare;
CShape* pShape[3] = { &aShape,&aRect,&aSquare };
for (int i=0; i< 3; i++)
{
pShape[i]->display();
pShape[i]->MyTest();
}
}
=====================第二部分====================
107: void main()
108: {
004117D0 push ebp
004117D1 mov ebp,esp
004117D3 sub esp,74h
004117D6 push ebx
004117D7 push esi
004117D8 push edi
004117D9 lea edi,[ebp-74h]
004117DC mov ecx,1Dh
004117E1 mov eax,0CCCCCCCCh
004117E6 rep stos dword ptr [edi]
109: CShape aShape; //aShape 地址0x0012ff78
004117E8 lea ecx,[ebp-8] ;ebp 为0x0012ff80,执行后ecx为0012ff78,
004117EB call @ILT+65(ostream::operator<<) (00401046)
110: CRect1 aRect; //aRect 0x0012ff6c
004117F0 lea ecx,[ebp-14h] ;ebp 为0x0012ff80,执行后eex为0012ff6c,即指向aRect,也
就是aRect的this指针,相关教材上说this一般保存在ecx。
004117F3 call @ILT+80(CRect::CRect) (00401055) ;构造函数(后面详细讲解)
111: CSquare aSquare; //aSquare 地址0x0012ff5c
004117F8 lea ecx,[ebp-24h] //ecx为0x0012ff5c,ebp为0012ff80
004117FB call @ILT+60(CSquare::CSquare) (00401041) ;构造函数
112: CShape* pShape[3] = { &aShape,&aRect,&aSquare }; //pShape为0x0012ff50
00411800 lea eax,[ebp-8] //ebp=0x0012ff80;eax=0x0012ff78,即为aShape的地址
00411803 mov dword ptr [ebp-30h],eax;把eax发到ebp-30h的内容空间里(4个字节),
ebp=0x0012ff80,[ebp-30]=0x0012ff50即pShape的地址。
00411806 lea ecx,[ebp-14h];ecx=0x0012ff6c即为aRect
00411809 mov dword ptr [ebp-2Ch],ecx //[ebp-2c]=0x0012ff54,即为pShape[1]
0041180C lea edx,[ebp-24h] //同理
0041180F mov dword ptr [ebp-28h],edx //同上
113: for (int i=0; i< 3; i++) //i地址0x0012ff4c
00411812 mov dword ptr [ebp-34h],0 //内存为0x0012ff80-34h=012ff4c,即i
00411819 jmp main+54h (00411824) ;跳到00411824
0041181B mov eax,dword ptr [ebp-34h] ;把i给eax;
0041181E add eax,1 ;eax加1
00411821 mov dword ptr [ebp-34h],eax ;eax再加1,上面三条指令完成i++
00411824 cmp dword ptr [ebp-34h],3 ;把i和3做比较
00411828 jge main+84h (00411854) ;如果不小于则跳到00411854
114: {
115: pShape[i]->display(); ; //下面以i=1为例,aRect1
0041182A mov ecx,dword ptr [ebp-34h] ;ebp-34h为i的内存,把i给ecx;
0041182D mov ecx,dword ptr [ebp+ecx*4-30h] ;基址+变址寻址 ebp-30h为pShape的地址
,ecx(i)为变址,通过i索引数组里不同的对象;最后ecx得到了当前对象的this指针即对象的地址
00411831 mov edx,dword ptr [ebp-34h] ;i给edx
00411834 mov eax,dword ptr [ebp+edx*4-30h];eax同样得到了对象的地址,eax=0x0012ff6c
00411838 mov edx,dword ptr [eax] ;[eax]放着对像,对象的第一个数据是vtable的地址,
所以[edx]里面是对象的第一个虚函数;edx=0x00429114,[edx]=0x00401050
0041183A mov esi,esp ;
0041183C call dword ptr [edx+4] ;[edx+4]为第二个虚函数的地址。[edx+4]=0x0040105a,虚函数的地址。间接。这个虚函数的调用:取得this指针,得vtable,根据vtable里面的地址,得出虚函数。
0041183F cmp esi,esp ;//??
00411841 call __chkesp (004011e0) ;堆栈清除
116: pShape[i]->MyTest();
00411846 mov eax,dword ptr [ebp-34h] ;i
00411849 mov ecx,dword ptr [ebp+eax*4-30h] ;对象地址
0041184D call @ILT+25(CShape::MyTest) (0040101e) ;//调用CShape::MyTest函数,注意这2个函数的调用方法的区别
117: }
00411852 jmp main+4Bh (0041181b) ;跳回
118:
119: }
00411854 pop edi
00411855 pop esi
00411856 pop ebx
00411857 add esp,74h
0041185A cmp ebp,esp
0041185C call __chkesp (004011e0)
00411861 mov esp,ebp
00411863 pop ebp
00411864 ret
分析CRect1 aRect的构造函数:
110: CRect1 aRect;
004117F0 lea ecx,[ebp-14h]
004117F3 call @ILT+80(CRect::CRect) (00401055)
函数从call 进入:跳转到0x00401055可以看到:
00401055 jmp CRect1::CRect1 (00411530)
0040105A jmp CRect1::display (00411580)
0040105F int 3
00401060 int 3
00401061 int 3
在跟进去:
73: CRect1():b2(2){};
00411530 push ebp
00411531 mov ebp,esp
00411533 sub esp,44h
00411536 push ebx
00411537 push esi
00411538 push edi
00411539 push ecx //堆栈保护
0041153A lea edi,[ebp-44h] //ebp=0x0012fef8 ,edi=0x0012ffb4
0041153D mov ecx,11h ;
00411542 mov eax,0CCCCCCCCh
00411547 rep stos dword ptr [edi] ;以上三条把
00411549 pop ecx ;0x0012ff6c(好熟悉啊,就是aRect1的地址,this指针)
0041154A mov dword ptr [ebp-4],ecx ;
0041154D mov ecx,dword ptr [ebp-4]
00411550 call @ILT+65(ostream::operator<<) (00401046) ;00401046为00401046 jmp CShape::CShape (004010a0)一看就知道是CShape的构造函数。
00411555 mov eax,dword ptr [ebp-4] ;把ecx给eax,即this,首地址为vtable的地址
00411558 mov dword ptr [eax+8],2 ;[eax+4]为b1,[eax+8]为b2,b2=0
0041155F mov ecx,dword ptr [ebp-4] ;ecx=this
00411562 mov dword ptr [ecx],offset CRect1::`vftable' (00429114);把vftable的地址付给[ecx]这个内存,即[ecx]里面是aftable的地址,[ecx]=0x00429114
跟踪:00429114里面有2个元素,一个0x00401050 ostream::operator,另外一个0x0040105a :CRect1::display(),初始化的时候,把虚函数的地址放进去。上面调用。
00411568 mov eax,dword ptr [ebp-4]
0041156B pop edi
0041156C pop esi
0041156D pop ebx
0041156E add esp,44h
00411571 cmp ebp,esp
00411573 call __chkesp (004011e0)
00411578 mov esp,ebp
0041157A pop ebp
0041157B ret
主要部分在蓝色部分, 2个地方,前一个是如何调用虚函数,后面一个在构造函数中如何初始化构造函数。