我們再看看虛成員函數(shù)的調用。類C041中含有虛成員函數(shù),它的定義如下:
struct C041
{
C041() : c_(0x01) {}
virtual void foo() { c_ = 0x02; }
char c_;
};
執(zhí)行如下代碼:
C041 obj;
PRINT_DETAIL(C041, obj)
PRINT_VTABLE_ITEM(obj, 0, 0)
obj.foo();
C041 * pt = &obj;
pt->foo();
結果如下:
The detail of C041 is 14 b3 45 00 01
obj : objadr:0012F824 vpadr:0012F824 vtadr:0045B314 vtival(0):0041DF1E
我們打印出了C041的對象內存布局及它的虛表信息。
先看看obj.foo();的匯編代碼:
004230DF lea ecx,[ebp FFFFF948h]
004230E5 call 0041DF1E
和前一篇文章中看過的普通的成員函數(shù)調用產生的匯編代碼一樣。這說明了通過對象進行函數(shù)調用,即使被調用的函數(shù)是虛函數(shù)也是靜態(tài)綁定,即在編譯時決議出函數(shù)的地址。不會有多態(tài)的行為發(fā)生。
我們跟蹤進去看看函數(shù)的匯編代碼。
01 004263F0 push ebp
02 004263F1 mov ebp,esp
03 004263F3 sub esp,0CCh
04 004263F9 push ebx
05 004263FA push esi
06 004263FB push edi
07 004263FC push ecx
08 004263FD lea edi,[ebp FFFFFF34h]
09 00426403 mov ecx,33h
10 00426408 mov eax,0CCCCCCCCh
11 0042640D rep stos dword ptr [edi]
12 0042640F pop ecx
13 00426410 mov dword ptr [ebp-8],ecx
14 00426413 mov eax,dword ptr [ebp-8]
15 00426416 mov byte ptr [eax 4],2
16 0042641A pop edi
17 0042641B pop esi
18 0042641C pop ebx
19 0042641D mov esp,ebp
20 0042641F pop ebp
21 00426420 ret
值得注意的是第14、15行。第14行把this指針的值移到eax寄存器中,第15行給類的第一個成員變量賦值,這時我們可以看到在取變量的地址時用的是[eax 4],即跳過了對象布局最前面的4字節(jié)的虛表指針。
接下來我們看看通過指針進行的虛函數(shù)調用pt->foo();,產生的匯編代碼如下:
01 004230F6 mov eax,dword ptr [ebp FFFFF900h]
02 004230FC mov edx,dword ptr [eax]
03 004230FE mov esi,esp
04 00423100 mov ecx,dword ptr [ebp FFFFF900h]
05 00423106 call dword ptr [edx]
第1行把pt指向的地址移入eax寄存器,這樣eax中就保存了對象的內存地址,同時也是類的虛表指針的地址。第2行取eax中指針指向的值(注意不是 eax的值)到edx寄存器中,實際上也就是虛表的地址。執(zhí)行完這兩條指令后,我們看看eax和edx中的值,果然和我們前面打印的obj的虛表信息中的 vpadr和vtadr的值是一樣的,分別為0x0012F824和0x0045B314。第4行同樣用ecx寄存器來保存并傳遞對象地址,即 this指針的值。第5行的call指令,我們可以看到目的地址不象通過對象來調用那樣,是一個直接的函數(shù)地址。而是將edx中的值做為指針來進行間接調用。前面我們已經知道edx中存放的實際是虛表的地址,我們也知道虛表實際是一個指針數(shù)組。這樣第5行的調用實際就是取到虛表中的第一個條目的值,即 C041::foo()函數(shù)的地址。如果被調用的虛函數(shù)對應的虛表條目的索引不是0,將會看到edx后加上一個索引號乘4后的偏移值。繼承跟蹤可以發(fā)現(xiàn), ptr[edx]的值為0x0041DF1E,也和我們打印的vtival(0)的值相同。前面已經提到過,這個地址實際也不是真正的函數(shù)地址,是一個跳轉指令,繼續(xù)執(zhí)行就到了真正的函數(shù)代碼部分(即前面列出的代碼)。
struct C041
{
C041() : c_(0x01) {}
virtual void foo() { c_ = 0x02; }
char c_;
};
執(zhí)行如下代碼:
C041 obj;
PRINT_DETAIL(C041, obj)
PRINT_VTABLE_ITEM(obj, 0, 0)
obj.foo();
C041 * pt = &obj;
pt->foo();
結果如下:
The detail of C041 is 14 b3 45 00 01
obj : objadr:0012F824 vpadr:0012F824 vtadr:0045B314 vtival(0):0041DF1E
我們打印出了C041的對象內存布局及它的虛表信息。
先看看obj.foo();的匯編代碼:
004230DF lea ecx,[ebp FFFFF948h]
004230E5 call 0041DF1E
和前一篇文章中看過的普通的成員函數(shù)調用產生的匯編代碼一樣。這說明了通過對象進行函數(shù)調用,即使被調用的函數(shù)是虛函數(shù)也是靜態(tài)綁定,即在編譯時決議出函數(shù)的地址。不會有多態(tài)的行為發(fā)生。
我們跟蹤進去看看函數(shù)的匯編代碼。
01 004263F0 push ebp
02 004263F1 mov ebp,esp
03 004263F3 sub esp,0CCh
04 004263F9 push ebx
05 004263FA push esi
06 004263FB push edi
07 004263FC push ecx
08 004263FD lea edi,[ebp FFFFFF34h]
09 00426403 mov ecx,33h
10 00426408 mov eax,0CCCCCCCCh
11 0042640D rep stos dword ptr [edi]
12 0042640F pop ecx
13 00426410 mov dword ptr [ebp-8],ecx
14 00426413 mov eax,dword ptr [ebp-8]
15 00426416 mov byte ptr [eax 4],2
16 0042641A pop edi
17 0042641B pop esi
18 0042641C pop ebx
19 0042641D mov esp,ebp
20 0042641F pop ebp
21 00426420 ret
值得注意的是第14、15行。第14行把this指針的值移到eax寄存器中,第15行給類的第一個成員變量賦值,這時我們可以看到在取變量的地址時用的是[eax 4],即跳過了對象布局最前面的4字節(jié)的虛表指針。
接下來我們看看通過指針進行的虛函數(shù)調用pt->foo();,產生的匯編代碼如下:
01 004230F6 mov eax,dword ptr [ebp FFFFF900h]
02 004230FC mov edx,dword ptr [eax]
03 004230FE mov esi,esp
04 00423100 mov ecx,dword ptr [ebp FFFFF900h]
05 00423106 call dword ptr [edx]
第1行把pt指向的地址移入eax寄存器,這樣eax中就保存了對象的內存地址,同時也是類的虛表指針的地址。第2行取eax中指針指向的值(注意不是 eax的值)到edx寄存器中,實際上也就是虛表的地址。執(zhí)行完這兩條指令后,我們看看eax和edx中的值,果然和我們前面打印的obj的虛表信息中的 vpadr和vtadr的值是一樣的,分別為0x0012F824和0x0045B314。第4行同樣用ecx寄存器來保存并傳遞對象地址,即 this指針的值。第5行的call指令,我們可以看到目的地址不象通過對象來調用那樣,是一個直接的函數(shù)地址。而是將edx中的值做為指針來進行間接調用。前面我們已經知道edx中存放的實際是虛表的地址,我們也知道虛表實際是一個指針數(shù)組。這樣第5行的調用實際就是取到虛表中的第一個條目的值,即 C041::foo()函數(shù)的地址。如果被調用的虛函數(shù)對應的虛表條目的索引不是0,將會看到edx后加上一個索引號乘4后的偏移值。繼承跟蹤可以發(fā)現(xiàn), ptr[edx]的值為0x0041DF1E,也和我們打印的vtival(0)的值相同。前面已經提到過,這個地址實際也不是真正的函數(shù)地址,是一個跳轉指令,繼續(xù)執(zhí)行就到了真正的函數(shù)代碼部分(即前面列出的代碼)。