Windows API經常需要回調函數,而在C++開發中面向對象當行其道,若能讓C++類的成員函數成為回調函數,簡直就是大善!但是C++成員函數都隱含了一個this指針用於指向當前的對象。要實現回調確實不容易。
我大約一年前就接觸到Thunk技術,甚至也看過利用Thunk實現將成員函數變成回調函數的例子。但是我實在沒了解過C++匯編後的樣子,很容易鑽了牛角尖,看都看不懂,直接用他們的程序又不敢,畢竟出錯後不好處理。前端時間偶爾想起Thunk技術,對未懂技術老這樣懸著很可能影響自己的程序員生涯的,於是決心閉關參悟(沒辦法,資質差啊),終於弄明白了。那種感覺啊,就像誠信禮佛的人突然見到如來一樣,或者換了貼近自己的比喻:就像千年色鬼見到美女一樣的興奮。 我忍不住的模仿小說中的修真人士突悟大道後的感歎:原來如此!
下面的分享一下我的收獲,基本上是出入門徑的寫給初學者的,大俠千萬要止步,小弟皮薄!
稍微研究了一下C++匯編後的代碼,一般調用C++的成員函數之前,都是使用ECX寄存器保存對象的指針,好在C++成員函數的調用約定__thiscall的參數壓棧順序和堆棧平衡的維護都是和回調函數的調用約定__stdcall一樣,所以只需要構造匯編將對象指針保存在ECX寄存器後JMP到成員函數的執行地址就可以了。先寫個C++結構拼湊這兩條匯編碼:
#pragma pack( push, 1 )
struct MemFunToStdCallThunk
{
BYTE m_mov;
DWORD m_this;
BYTE m_jmp;
DWORD m_relproc;
BOOL Init( DWORD_PTR proc, void* pThis )
{
m_mov = 0xB9;
m_this = PtrToUlong(pThis);
m_jmp = 0xe9;
m_relproc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(MemFunToStdCallThunk)));
::FlushInstructionCache( ::GetCurrentProcess(), this, sizeof(MemFunToStdCallThunk) );
return TRUE;
}
void* GetCodeAddress()
{
return this;
}
};
#pragma pack( pop )
這個結構相當於兩條匯編語句:
mov ecx, pThis
jmp [偏移地址]
使用:
class CTestClass
{
private:
int m_nBase;
MemFunToStdCallThunk m_thunk;
void memFun( int m, int n )
{
int nSun = m_nBase + m + n;
CString str;
str.Format( _T("%d"), nSun );
AtlMessageBox( NULL, _U_STRINGorID( str ) );
}
public:
CTestClass()
{
m_nBase = 10;
}
void Test()
{
//UnionCastType:利用聯合將函數指針轉換成DWORD_PTR
m_thunk.Init( UnionCastType<DWORD_PTR>(&CTestClass::memFun), this );
StdCallFun fun = (StdCallFun)m_thunk.GetCodeAddress();
ATLASSERT( fun != NULL );
fun( 1, 3 );
}
};
MemFunToStdCallThunk的Init方法接受成員函數指針和對象指針後就構造成2條匯編碼,當調用fun(1,3)時,
首先將參數3和1壓入堆棧,之後跳轉到m_thunk處,也就是那構造的2條匯編碼處,將對象指針保存到ECX寄存器,之後跳轉到指定的成員函數處執行。一切OK了。
UnionCastType方法的代碼如下:
template< typename TDst, typename TSrc >
TDst UnionCastType( TSrc src )
{
union
{
TDst uDst;
TSrc uSrc;
}uMedia;
uMedia.uSrc = src;
return uMedia.uDst;
}