討論虛擬函數的技術內幕——後期聯編(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個地方,前一個是如何調用虛函數,後面一個在建構函式中如何初始化建構函式。