直接調用類(lèi)成員函數地址
作者:南風(fēng)
摘要:介紹了如何取成員函數的地址以及調用該地址.
關(guān)鍵字:C++成員函數 this指針 調用約定
一、成員函數指針的用法
在C++中,成員函數的指針是個(gè)比較特殊的東西。對普通的函數指針來(lái)說(shuō),可以視為一個(gè)地址,在需要的時(shí)候可以任意轉換并直接調用。但對成員函數來(lái)說(shuō),常規類(lèi)型轉換是通不過(guò)編譯的,調用的時(shí)候也必須采用特殊的語(yǔ)法。C++專(zhuān)門(mén)為成員指針準備了三個(gè)運算符: "::*"用于指針的聲明,而"->*"和".*"用來(lái)調用指針指向的函數。比如:
class tt { public: void foo(int x){ printf("\n %d \n",x); } }; typedef void ( tt::* FUNCTYPE)(int ); FUNCTYPE ptr = tt::foo; //給一個(gè)成員函數指針賦值. tt a; (a.*ptr)(5); //調用成員函數指針. tt *b = new tt; (b->*ptr)(6); //調用成員函數指針.DWORD dwFooAddrPtr= 0; dwFooAddrPtr = (DWORD) &tt::foo; /* Error C2440 */ dwFooAddrPtr = reinterpret_cast你得到只是兩個(gè)c2440錯誤而已。當然你也無(wú)法將成員函數類(lèi)型轉換為其它任何稍有不同的類(lèi)型,簡(jiǎn)單的說(shuō),每個(gè)成員函數指針都是一個(gè)獨有的類(lèi)型,無(wú)法轉換到任何其它類(lèi)型。即使兩個(gè)類(lèi)的定義完全相同也不能在其對應成員函數指針之間做轉換。這有點(diǎn)類(lèi)似于結構體的類(lèi)型,每個(gè)結構體都是唯一的類(lèi)型,但不同的是,結構體指針的類(lèi)型是可以強制轉換的。 有了這些特殊的用法和嚴格的限制之后,類(lèi)成員函數的指針實(shí)際上是變得沒(méi)什么用了。這就是我們平?;究床坏酱a里有"::*", ".*" 和 "->*"的原因。(&tt::foo); /* Error C2440 */
二、取成員函數的地址
當然,引用某位大師的話(huà):"在windows中,我們總是有辦法的"。同樣,在C++中,我們也總是有辦法的。這個(gè)問(wèn)題,解決辦法已經(jīng)存在了多年,并且廣為使用(在MFC中就使用了)。一般有兩個(gè)方法,一是使用內嵌的匯編語(yǔ)言直接取函數地址,二是使用union類(lèi)型來(lái)逃避C++的類(lèi)型轉換檢測。兩種方法都是利用了某種機制逃避C++的類(lèi)型轉換檢測,為什么C++編譯器干脆不直接放開(kāi)這個(gè)限制,一切讓程序員自己作主呢?當然是有原因的,因為類(lèi)成員函數和普通函數還是有區別的,允許轉換后,很容易出錯,這個(gè)在后面會(huì )有詳細的說(shuō)明?,F在先看看取類(lèi)成員函數地址的兩種方法:
第一種方法:
templatevoid GetMemberFuncAddr_VC6(ToType& addr,FromType f){ union { FromType _f; ToType _t; }ut; ut._f = f; addr = ut._t;}
#define GetMemberFuncAddr_VC8(FuncAddr,FuncType){ __asm { mov eax,offset FuncType }; __asm { mov FuncAddr, eax }; }這樣使用:
通過(guò)上面兩個(gè)方法,我們可以取到成員函數的地址。不過(guò),如果不能通過(guò)地址來(lái)調用成員函數的話(huà),那也還是沒(méi)有任何用處。當然,這是可行的。不過(guò)在這之前,需要了解關(guān)于成員函數的一些知識。
我們知道,成員函數和普通函數最大的區別就是成員函數包含一個(gè)隱藏的參數this指針,用來(lái)表明成員函數當前作用在那一個(gè)對象實(shí)例上。根據調用約定(Calling Convention)的不同,成員函數實(shí)現this指針的方式也不同。如果使用__thiscall調用約定,那么this指針保存在寄存器ECX中,VC編譯器缺省情況下就是這樣的。如果是__stdcall或__cdecl調用約定,this指針將通過(guò)棧進(jìn)行傳遞,且this指針是最后一個(gè)被壓入棧的參數,相當于編譯器在函數的參數列表中最左邊增加了一個(gè)this參數。
這里還有件事不得不提,雖然vc將__thiscall類(lèi)型作為成員函數的默認類(lèi)型,但是vc6卻沒(méi)有定義__thiscall關(guān)鍵字!如果你使用__thiscall來(lái)定義一個(gè)函數,編譯器報錯:'__thiscall' keyword reserved for future use。
知道這些就好辦了,我們只要根據不同的調用約定,準備好this指針,然后象普通函數指針一樣的使用成員函數地址就可以了。
對__thiscall類(lèi)型的成員函數(注意,這個(gè)是VC的默認類(lèi)型),我們在調用之前加一句: mov ecx, this; 然后就可以調用成員函數指針。例如:
class tt { public: void foo(int x,char c,char *s)//沒(méi)有指定類(lèi)型,默認是__thiscall. { printf("\n m_a=%d, %d,%c,%s\n",m_a,x,c,s); } int m_a;};typedef void (__stdcall *FUNCTYPE)(int x,char c,char *s);//定義對應的非成員函數指針類(lèi)型,注意指定__stdcall. tt abc; abc.m_a = 123; DWORD ptr; DWORD This = (DWORD)&abc; GetMemberFuncAddr_VC6(ptr,tt::foo); //取成員函數地址. FUNCTYPE fnFooPtr = (FUNCTYPE) ptr;//將函數地址轉化為普通函數的指針. __asm //準備this指針. { mov ecx, This; } fnFooPtr(5,'a',"7xyz"); //象普通函數一樣調用成員函數的地址.class tt {public: void __stdcall foo(int x,char c,char *s)//成員函數指定了__stdcall調用約定. { printf("\n m_a=%d, %d,%c,%s\n",m_a,x,c,s); } int m_a;};typedef void (__stdcall *FUNCTYPE)(void *This,int x,char c,char *s);//注意多了一個(gè)void *參數. tt abc; abc.m_a = 123; DWORD ptr; GetMemberFuncAddr_VC6(ptr,tt::foo); //取成員函數地址. FUNCTYPE fnFooPtr = (FUNCTYPE) ptr;//將函數地址轉化為普通函數的指針. fnFooPtr(&abc,5,'a',"7xyz"); //象普通函數一樣調用成員函數的地址,注意第一個(gè)參數是this指針. 每次都定義一個(gè)函數類(lèi)型并且進(jìn)行一次強制轉化,這個(gè)事是比較煩的,能不能將這些操作寫(xiě)成一個(gè)函數,然后每次調用是指定函數地址和參數就可以了呢?當然是可以的,并且我已經(jīng)寫(xiě)了一個(gè)這樣的函數。 //調用類(lèi)成員函數//callflag:成員函數調用約定類(lèi)型,0--thiscall,非0--其它類(lèi)型.//funcaddr:成員函數地址.//This:類(lèi)對象的地址.//count:成員函數參數個(gè)數.//...:成員函數的參數列表.DWORD CallMemberFunc(int callflag,DWORD funcaddr,void *This,int count,...){ DWORD re; if(count>0)//有參數,將參數壓入棧. { __asm { mov ecx,count;//參數個(gè)數,ecx,循環(huán)計數器. mov edx,ecx; shl edx,2; add edx,0x14; edx = count*4+0x14; next: push dword ptr[ebp+edx]; sub edx,0x4; dec ecx jnz next; } } //處理this指針. if(callflag==0) //__thiscall,vc默認的成員函數調用類(lèi)型. { __asm mov ecx,This; } else//__stdcall { __asm push This; } __asm//調用函數 { call funcaddr; mov re,eax; } return re;}使用這個(gè)函數,則上面的兩個(gè)調用可以這樣寫(xiě):
四、進(jìn)一步的討論
到目前為止,已經(jīng)討論了如何取成員函數的地址,然后如何使用這個(gè)地址。但是還有些重要的情況沒(méi)有討論,我們知道成員函數可分為三種:普通成員函數,靜態(tài),虛擬。另外更重要的是,在繼承甚至多繼承下情況如何。
首先看看最簡(jiǎn)單的單繼承,非虛擬函數的情況。
class tt1{public: void foo1(){ printf("\n hi, i am in tt1::foo1\n"); }};class tt2 : public tt1{public: void foo2(){ printf("\n hi, i am in tt2::foo2\n"); }};注意,tt2中沒(méi)有定義函數foo1,它的foo1函數是從tt1中繼承過(guò)來(lái)的。這種情況下,我們直接取tt2::foo1的地址行會(huì )發(fā)生什么?
DWORD tt2_foo1;tt1 x;GetMemberFuncAddr_VC6(tt2_foo1,&tt2::foo1);CallMemberFunc(0,tt2_foo1,&x,0); // tt2::foo1 = tt1::foo1
運行結果表明,一切正常!當我們寫(xiě)下tt2::foo1的時(shí)候,編譯器知道那實(shí)際上是tt1::foo1,因此它會(huì )暗中作替換。編譯器(VC6)產(chǎn)生的代碼如下:
GetMemberFuncAddr_VC6(tt2_foo1,&tt2::foo1); //源代碼.//VC6編譯器產(chǎn)生的匯編代碼:push offset @ILT+235(tt1::foo1) (004010f0) //直接用tt1::foo1 替換 tt2::foo1....
再看看稍微復雜些的情況,繼承情況下的虛擬函數。
class tt1{public: void foo1(){ printf("\n hi, i am in tt1::foo1\n"); } virtual void foo3(){ printf("\n hi, i am in tt1::foo3\n"); }};class tt2 : public tt1{public: void foo2(){ printf("\n hi, i am in tt2::foo2\n"); } virtual void foo3(){ printf("\n hi, i am in tt2::foo3\n"); }};現在tt1和tt2都定義了虛函數foo3,按C++語(yǔ)法,如果通過(guò)指針調用foo3,應該發(fā)生多態(tài)行為。下面的代碼:
DWORD tt1_foo3,tt2_foo3;GetMemberFuncAddr_VC6(tt1_foo3,&tt1::foo3);GetMemberFuncAddr_VC6(tt2_foo3,&tt2::foo3);tt1 x;tt2 y;CallMemberFunc(0,tt1_foo3,&x,0); // tt1::foo3CallMemberFunc(0,tt2_foo3,&x,0); // tt2::foo3CallMemberFunc(0,tt1_foo3,&y,0); // tt1::foo3CallMemberFunc(0,tt2_foo3,&y,0); // tt2::foo3
輸出如下:
hi, i am in tt1::foo3hi, i am in tt1::foo3hi, i am in tt2::foo3hi, i am in tt2::foo3
請注意第二行輸出,tt2_foo3取的是&tt2::foo3,但由于傳遞的this指針產(chǎn)生是&x,所以實(shí)際上調用了tt1::foo3。同樣,第三行輸出,取的是基類(lèi)的函數地址,但由于實(shí)際對象是派生類(lèi),最后調用了派生類(lèi)的函數。 這說(shuō)明取得的成員函數地址在虛擬函數的情況下仍然保持了正確的行為。
你若真的理解了上面所說(shuō)的,一定會(huì )覺(jué)得奇怪。取函數地址的時(shí)候就得到了一個(gè)整數(成員函數地址),為何調用的時(shí)候卻進(jìn)了不同的函數? 只要看看匯編代碼就都清楚了,"源碼之前,了無(wú)秘密"。源代碼: GetMemberFuncAddr_VC6(tt1_foo3,&tt1::foo3); 產(chǎn)生的匯編代碼如下:
push offset @ILT+90(`vcall') (0040105f)...
原來(lái)取tt1::foo3地址的時(shí)候,并不是真的就將tt1::foo3的地址傳給了函數,而是傳了一個(gè)vcall函數的地址。顧名思義,vcall當然是虛擬調用的意思。我們找到地址0040105f,看看這個(gè)函數到底干了些什么。
@ILT+90(??_9@$BA@AE):0040105F jmp `vcall' (00401380)
該地址只是ILT的一個(gè)項,直接跳轉到真正的vcall函數,00401380。找到00401380,就可以看到vcall的代碼。
`vcall':00401380 mov eax,dword ptr [ecx] ;//將this指針視為dword類(lèi)型,并將指向的內容(對象的首個(gè)dword)放入eax.00401382 jmp dword ptr [eax] ;//跳轉到eax所指向的地址.
代碼執行的時(shí)候,ecx就是this指針,具體說(shuō)就是上面對象x或y的地址。而eax就是對象x或y的第一個(gè)dword的值。我們知道,對于有虛擬函數的類(lèi)對象,其對象的首地址處總是一個(gè)指針,該指針指向一個(gè)虛函數的地址表。上面的對象由于只有一個(gè)虛函數,所以虛函數表也只有一項。因此,直接跳轉到eax指向的地址就好。如果有多個(gè)虛函數,則eax還要加上一個(gè)偏移量,以定位到不同的虛函數。比如,如果有兩個(gè)虛函數,則會(huì )有兩個(gè)vcall代碼,分別對應不同的虛函數,其代碼大概是下面的樣子:
`vcall':00401BE0 mov eax,dword ptr [ecx]00401BE2 jmp dword ptr [eax]`vcall':00401190 mov eax,dword ptr [ecx]00401192 jmp dword ptr [eax+4]
編譯器根據取的是哪個(gè)虛函數的地址,則相應的用對應的vcall地址代替。
總結一下:用前面方法取得的成員函數地址在虛擬函數的情況下仍然保持正確的行為,是因為編譯器實(shí)際上傳遞了對應的vcall地址。而vcall代碼會(huì )根據上下文this指針定位到對應的虛函數表,進(jìn)而調用正確的虛函數。
最后,我們看一下多繼承情況。很明顯,現在情況要復雜得多。如果實(shí)際試一下,會(huì )碰到很多困難。首先,指定成員函數的時(shí)候可能會(huì )碰到?jīng)_突。其次,給定this指針的時(shí)候需要經(jīng)過(guò)調整。另外,對虛擬繼承可能還要特別處理。解決所有這些問(wèn)題已經(jīng)超出了這篇文章的范圍,并且我想要的成員函數指針是一個(gè)真正的指針,而在多繼承的情況下,很多時(shí)候成員函數指針已經(jīng)變成了一個(gè)結構體(見(jiàn)參考文獻),這時(shí)要正確調用該指針就變得格外困難。因此結論是,上面討論的方法并不適用于多繼承的情況,要想在多繼承的情況下直接調用成員函數地址,必須手工處理各種調整,沒(méi)有簡(jiǎn)單的統一方法。
聯(lián)系客服