談到虛函數,我想很多朋友都應該知道虛函數表指標VPTR和虛函數表VTABLE,如果不清楚的朋友,建議先看看侯捷先生翻譯的《深度探索C++物件模型》:)
剛開始的時候,我僅僅知道虛函數的多態機制是通過VPTR和VTABLE操控的,完全地相信書上所描述的,並沒有親自去證明過,或許是因為那時候我還沒有接觸到逆向分析吧:)
提幾個問題:
1. VPTR的大小如何確定?
2. VPTR在類執行個體中的位移值是多少?
3. VPTR是如何索引到需要調用的函數的?
4. VTABLE中函數的順序是如何確定的?
瞭解上述問題的朋友,可以關閉這個視窗了:)
考慮如下代碼:
#include <cstdio>class Base{public:int i;char c;double d;Base(){i = 4;c = 'A';d = 2.0;}virtual void Virtual_Func_A(){printf("Virtual_Func_A()\n");}virtual void Virtual_Func_B(){printf("Virtual_Func_B()\n");}};int main(void){Base* b = new Base;b->Virtual_Func_A();b->Virtual_Func_A();return 0;}
分析工具:VC 6.0
分析過程:
在解答這個問題前,讀者有必要瞭解一下“資料對齊”的概念,如果不瞭解的讀者,可以參考下:)
http://blog.csdn.net/yeweiouyang/article/details/8636458
注意,Inter cpu 採用的是小端法
VPTR對程式員來說是隱形的,在Win32平台下,不考慮資料對齊的情況,VPTR佔用4個位元組,用於儲存VTABLE的地址
考慮如下程式:
#include <cstdio>class Base{public:virtual void Common_Func(){printf("Base::Common_Func()\n");}virtual void Base_Func(){printf("Base::Func()\n");}};class Derived : public Base{public:virtual void Common_Func(){printf("Derived::Common_Func()\n");}virtual void Derived_Func(){printf("Derived::Func()\n");}};int main(void){Base* b = new Base;b->Common_Func();b->Base_Func();Derived* d = new Derived;d->Common_Func();d->Base_Func();d->Derived_Func();return 0;}
分析工具:IDA Pro
靜態反組譯碼:
.text:00401000 push esi.text:00401001 push 4 ; 為 Base::VPTR 申請 4 個位元組的棧空間.text:00401003 call ??2@YAPAXI@Z ; operator new(uint).text:00401008 add esp, 4.text:0040100B test eax, eax.text:0040100D jz short loc_401019.text:0040100F mov dword ptr [eax], offset Base_VPTR ; *b 的堆空間放入 Base::VPTR.text:00401015 mov esi, eax.text:00401017 jmp short loc_40101B
對應的offset Base_VPTR:
.rdata:004060BC Base_VPTR dd offset Base_Common_Func ; DATA XREF: _main+Fo.rdata:004060C0 dd offset Base_Func.rdata:004060C4 align 8
不難發現Base_VPTR正是VTABLE的首地址,即Base::VPTR所指向之處
由這一句:
.text:0040100F mov dword ptr [eax], offset Base_VPTR ;
不難發現,VPTR存放的位置是Base類執行個體b的開始處,即位移值為0
.text:00401019.text:00401019 loc_401019: ; CODE XREF: _main+Dj.text:00401019 xor esi, esi.text:0040101B.text:0040101B loc_40101B: ; CODE XREF: _main+17j.text:0040101B mov eax, [esi] ; eax = Base::VTABLE.text:0040101D mov ecx, esi.text:0040101F call dword ptr [eax] ; call Base::Common_Func().text:00401021 mov edx, [esi] ; edx = Base::VTABLE.text:00401023 mov ecx, esi.text:00401025 call dword ptr [edx+4] ; call Base::Base_Func().text:00401028 push 4 ; 為 Derived::VPTR 申請 4 個位元組的棧空間.text:0040102A call ??2@YAPAXI@Z ; operator new(uint).text:0040102F add esp, 4.text:00401032 test eax, eax.text:00401034 jz short loc_401040.text:00401036 mov dword ptr [eax], offset Derived_VPTR ; *d 的堆空間放入 Derived_VPTR.text:0040103C mov esi, eax.text:0040103E jmp short loc_401042.text:00401040 ; ---------------------------------------------------------------------------.text:00401040.text:00401040 loc_401040: ; CODE XREF: _main+34j.text:00401040 xor esi, esi.text:00401042.text:00401042 loc_401042: ; CODE XREF: _main+3Ej.text:00401042 mov eax, [esi] ; eax = Derived::VTABLE.text:00401044 mov ecx, esi.text:00401046 call dword ptr [eax] ; call Derived::Common_Func().text:00401048 mov edx, [esi] ; edx = Derived::VTABLE.text:0040104A mov ecx, esi.text:0040104C call dword ptr [edx+4] ; call Base_Func(),Base_Func()是從Base類中繼承而來的.text:0040104F mov eax, [esi] ; eax = Derived::VTABLE.text:00401051 mov ecx, esi.text:00401053 call dword ptr [eax+8] ; call Derived::Derived_Func().text:00401056 xor eax, eax.text:00401058 pop esi.text:00401059 retn.text:00401059 _main endp
對應的Derived_VPTR:
.rdata:004060B0 Derived_VPTR dd offset Derived_Common_Func ; DATA XREF: _main+36o……(IDA 並沒有將所有函數指標顯示出來)
經過分析,Base和Derived的VTABLE結構大致如下:
在VTABLE中儲存有函數的地址(即函數指標),而非函數本身或函數名,在Win32平台下,函數指標佔用4個位元組,當通過類執行個體調用虛函數時,VPTR就會在VTABLE中進行索引,若找到對應的虛函數,就會取出函數指標並進行調用