今天通過彙編角度在次理解一下虛函數.
工作環境VC6
方法DEBUG 跟蹤MEMORY
測試程式碼 雖然是測試程式但希望大家還是養成良好習慣 不要汙染命名空間
測試代碼:
#include "iostream"
using std::cout;
using std::endl;
class a
{
int m_a;
public:
a(int x):m_a(x) {}
virtual void display()
{
cout<<"a is running !"<<endl;
}
};
class b : public a
{
public:
b(int y):a(y) {}
void display()
{
cout<<"b is running !"<<endl;
}
};
class c : public b
{
public:
c(int z):b(z) {}
void display()
{
cout<<"c is running !"<<endl;
}
};
int main()
{
a ca(1);
b cb(2);
c cc(3);
a * p[3]={ &ca , &cb , &cc };
p[0]->display();
p[1]->display();
p[2]->display();
return 0;
}
輸出結果就不用多說了,如果你還不能想到正確結果 那先看一下基礎知識在看這篇文章.
先看一下產生的彙編代碼
39: int main()
40: {
004011A0 push ebp
004011A1 mov ebp,esp
004011A3 sub esp,64h
004011A6 push ebx
004011A7 push esi
004011A8 push edi
004011A9 lea edi,[ebp-64h]
004011AC mov ecx,19h
004011B1 mov eax,0CCCCCCCCh
004011B6 rep stos dword ptr [edi]
41: a ca(1);
004011B8 push 1
004011BA lea ecx,[ebp-8]
004011BD call @ILT+95(a::a) (00401064)
42: b cb(2);
004011C2 push 2
004011C4 lea ecx,[ebp-10h]
004011C7 call @ILT+10(b::b) (0040100f)
43: c cc(3);
004011CC push 3
004011CE lea ecx,[ebp-18h]
004011D1 call @ILT+105(c::c) (0040106e)
44:
45: a * p[3]={ &ca , &cb , &cc };
004011D6 lea eax,[ebp-8]
004011D9 mov dword ptr [ebp-24h],eax
004011DC lea ecx,[ebp-10h]
004011DF mov dword ptr [ebp-20h],ecx
004011E2 lea edx,[ebp-18h]
004011E5 mov dword ptr [ebp-1Ch],edx
46:
47: p[0]->display();
004011E8 mov eax,dword ptr [ebp-24h]
004011EB mov edx,dword ptr [eax]
004011ED mov esi,esp
004011EF mov ecx,dword ptr [ebp-24h]
004011F2 call dword ptr [edx]
004011F4 cmp esi,esp
004011F6 call __chkesp (004092c0)
48: p[1]->display();
004011FB mov eax,dword ptr [ebp-20h]
004011FE mov edx,dword ptr [eax]
00401200 mov esi,esp
00401202 mov ecx,dword ptr [ebp-20h]
00401205 call dword ptr [edx]
00401207 cmp esi,esp
00401209 call __chkesp (004092c0)
49: p[2]->display();
0040120E mov eax,dword ptr [ebp-1Ch]
00401211 mov edx,dword ptr [eax]
00401213 mov esi,esp
00401215 mov ecx,dword ptr [ebp-1Ch]
00401218 call dword ptr [edx]
0040121A cmp esi,esp
0040121C call __chkesp (004092c0)
50:
51: return 0;
00401221 xor eax,eax
我們只需要看一下重要的代碼就可以了
41: a ca(1);
004011B8 push 1
004011BA lea ecx,[ebp-8]
004011BD call @ILT+95(a::a) (00401064)
構造A類對象,先壓棧 常量1 作為參數.
將棧中的一個地址給ECX ,此時ECX 要作為該對象的THIS 指標.現在的這塊記憶體是全CC CC CC CC....
調用A類的建構函式.因為不是VIRTUAL FNCTION 所以是CALL一個常量地址,到一個跳錶
00401064 jmp a::a (00401260)
進入到A類的建構函式.
9: a(int x):m_a(x) {}
00401260 push ebp
00401261 mov ebp,esp
00401263 sub esp,44h
00401266 push ebx
00401267 push esi
00401268 push edi
00401269 push ecx
0040126A lea edi,[ebp-44h]
0040126D mov ecx,11h
00401272 mov eax,0CCCCCCCCh
00401277 rep stos dword ptr [edi]
00401279 pop ecx
0040127A mov dword ptr [ebp-4],ecx
0040127D mov eax,dword ptr [ebp-4]
00401280 mov ecx,dword ptr [ebp+8]
00401283 mov dword ptr [eax+4],ecx
00401286 mov edx,dword ptr [ebp-4]
00401289 mov dword ptr [edx],offset a::`vftable' (0043201c)
0040128F mov eax,dword ptr [ebp-4]
00401292 pop edi
00401293 pop esi
00401294 pop ebx
00401295 mov esp,ebp
00401297 pop ebp
00401298 ret 4
我們只看最重要的幾行就可以了
0040127A mov dword ptr [ebp-4],ecx
0040127D mov eax,dword ptr [ebp-4]
00401280 mov ecx,dword ptr [ebp+8]
00401283 mov dword ptr [eax+4],ecx
00401286 mov edx,dword ptr [ebp-4]
00401289 mov dword ptr [edx],offset a::`vftable' (0043201c)
0040128F mov eax,dword ptr [ebp-4]
此時ECX是該對象的THIS 指標.把THIS指標給一個變數(棧來實現) . 在給EAX 所以此時EAX也是THIS指標.
mov ecx,dword ptr [ebp+8]
實現了將剛才壓入棧中的參數1 給ECX.
mov dword ptr [eax+4],ecx
該值在給以THISI 指標開始+4到+8的4個位元組賦值(因為是 INT型 所以為4個位元組.) 因為A類有虛函數,我們知道 編譯器會給A類對象的前4個位元組賦予類A的VTABLE的地址,這4個位元組就是我們常說的VPTR 它指向虛表.所以VPTR後的4個位元組才是存放對象的唯一一個資料成員(INT).
此時該對象的記憶體為CC CC CC CC 01 00 00 00 VPTR還沒賦值
mov edx,dword ptr [ebp-4]
THIS指標給EDX
mov dword ptr [edx],offset a::`vftable' (0043201c)
把類A虛表地址 給VPTR 也就是THIS指標開始的4個位元組
eax,dword ptr [ebp-4] 還是把THIS指標給EAX 我認為這是一條廢語句
此時該對象的記憶體是
0012FF78 1C 20 43 00 01 00 00 00 虛表指標 資料成員
類B
42: b cb(2);
004011C2 push 2
004011C4 lea ecx,[ebp-10h]
004011C7 call @ILT+10(b::b) (0040100f)
同樣壓棧2 作為參數. 並找到下一個8位元組空間作為類B對象使用的記憶體
並進入跳錶0040100F jmp b::b (00401310)
進入建構函式
20: b(int y):a(y) {}
00401310 push ebp
00401311 mov ebp,esp
00401313 sub esp,44h
00401316 push ebx
00401317 push esi
00401318 push edi
00401319 push ecx
0040131A lea edi,[ebp-44h]
0040131D mov ecx,11h
00401322 mov eax,0CCCCCCCCh
00401327 rep stos dword ptr [edi]
00401329 pop ecx
0040132A mov dword ptr [ebp-4],ecx
0040132D mov eax,dword ptr [ebp+8]
00401330 push eax
00401331 mov ecx,dword ptr [ebp-4]
00401334 call @ILT+95(a::a) (00401064)
00401339 mov ecx,dword ptr [ebp-4]
0040133C mov dword ptr [ecx],offset b::`vftable' (00432034)
00401342 mov eax,dword ptr [ebp-4]
00401345 pop edi
00401346 pop esi
00401347 pop ebx
00401348 add esp,44h
0040134B cmp ebp,esp
0040134D call __chkesp (004092c0)
00401352 mov esp,ebp
00401354 pop ebp
00401355 ret 4
主要來看這段
0040132A mov dword ptr [ebp-4],ecx
0040132D mov eax,dword ptr [ebp+8]
00401330 push eax
00401331 mov ecx,dword ptr [ebp-4]
00401334 call @ILT+95(a::a) (00401064)
00401339 mov ecx,dword ptr [ebp-4]
0040133C mov dword ptr [ecx],offset b::`vftable' (00432034)
00401342 mov eax,dword ptr [ebp-4]
ECX還是該對象的THIS指標.給一個變數
把參數2給EAX,把EAX壓棧 作為參數來調用基類的建構函式. 函數和上邊說的一樣,所以返回時該對象記憶體為
1C 20 43 00 02 00 00 00 此時虛表指標指向的是類A的VTABLE
ecx,dword ptr [ebp-4]
把THIS指標給ECX
dword ptr [ecx],offset b::`vftable' (00432034)
把B類的VTABLE地址給THIS指標開始的4個位元組 (VPTR) 所以現在對象的記憶體是
34 20 43 00 02 00 00 00
類C的和前邊兩個基本相同.最後該對象的記憶體是 4C 20 43 00 03 00 00 00
最後3個對象的記憶體為
4C 20 43 00 03 00 00 00 34 20 43 00 02 00 00 00 燙燙L C.....4 C.....
0012FF78 1C 20 43 00 01 00 00 00
45: a * p[3]={ &ca , &cb , &cc };
004011D6 lea eax,[ebp-8]
004011D9 mov dword ptr [ebp-24h],eax
004011DC lea ecx,[ebp-10h]
004011DF mov dword ptr [ebp-20h],ecx
004011E2 lea edx,[ebp-18h]
004011E5 mov dword ptr [ebp-1Ch],edx
把3個對象的THIS指標賦值給3個4位元組的指標數組.
47: p[0]->display();
004011E8 mov eax,dword ptr [ebp-24h]
004011EB mov edx,dword ptr [eax]
004011ED mov esi,esp
004011EF mov ecx,dword ptr [ebp-24h]
004011F2 call dword ptr [edx]
004011F4 cmp esi,esp
004011F6 call __chkesp (004092c0)
48: p[1]->display();
004011FB mov eax,dword ptr [ebp-20h]
004011FE mov edx,dword ptr [eax]
00401200 mov esi,esp
00401202 mov ecx,dword ptr [ebp-20h]
00401205 call dword ptr [edx]
00401207 cmp esi,esp
00401209 call __chkesp (004092c0)
49: p[2]->display();
0040120E mov eax,dword ptr [ebp-1Ch]
00401211 mov edx,dword ptr [eax]
00401213 mov esi,esp
00401215 mov ecx,dword ptr [ebp-1Ch]
00401218 call dword ptr [edx]
0040121A cmp esi,esp
0040121C call __chkesp (004092c0)
50:
51: return 0;
00401221 xor eax,eax
這裡就是虛函數調用,他們因為虛表不同,所以輸出的結果 調用的函數是不同的.
先看看
47: p[0]->display();
004011E8 mov eax,dword ptr [ebp-24h]
004011EB mov edx,dword ptr [eax]
004011ED mov esi,esp
004011EF mov ecx,dword ptr [ebp-24h]
004011F2 call dword ptr [edx]
004011F4 cmp esi,esp
004011F6 call __chkesp (004092c0)
把第一個對象的THIS指標給EAX,
mov edx,dword ptr [eax]
THIS指標開始的4個位元組取內容.給EDX, 此時EDX就是VPTR,也就是VTABLE的地址
mov ecx,dword ptr [ebp-24h]
把THIS指標在給ECX
call dword ptr [edx]
每個類的虛函數在虛表中的位置就靠它在類中聲明的位置而定,沒個虛函數的位置就做為了VTABLE中的ID,因為測試代碼為每個類只定義了一個VIRTUAL FUNCTION 所以這裡就CALL了EDX指向的VTABLE開始的4位元組的那個為一一個VIRTUAL函數地址
跟蹤一下記憶體
0012FF78 1C 20 43 00
跟蹤[eax] EDX = 0043201C
跟蹤[edx] 0043201C 5F 10 40 00
跟蹤 0040105F jmp a::display (004012b0)
於是調用了A類的DISPLAY()函數
後邊的兩個調用一樣會利用VTABLE來調用虛函數.