C++ 通過Thunk在WNDPROC中訪問this指針 [轉]

本文基本只討論原理,具體實現請參見後續文章《C++ 通過Thunk在WNDPROC中訪問this指針實現細節

當註冊窗口類時,WNDCLASSEX結構的lpfnWndProc成員應設置爲窗口過程函數的地址,這是一個C風格的函數指針,所以我們只能使用全局或靜態函數的地址,這在我們將窗口封裝爲C++類時會很麻煩,因爲我們無法在一個全局或靜態的WindowProc函數中直接訪問類實例,這就需要一些手段了(MS的API設計着實不怎麼樣)

  第一種方案,建立一個HWND到C++類實例的映射表,在WindowProc中通過這個映射表從HWND得到C++類實例,由於可能有多線程安全問題,在訪問這個映射表時可能涉及到線程同步,再加上可能應用程序要處理的消息頻率十分高,從而帶來性能問題(一般情況還是可以接受的)。
  另一種方案是通過SetWindowLongPtr/GWLP_USERDATA將類實例指針存放在窗口的用戶數據字段中,這樣就可以在WindowProc中通過調用GetWindowLongPtr/GWLP_USERDATA來獲取類實例指針了,缺點就是當別人使用你的C++類時可能會不留神把你存放的this指針覆蓋,從而導致不可預知的後果;此外每一條window消息都要調用GetWindowLongPtr這個系統API也是一點點額外開銷。
  第三種也就是本文要討論的方案,是傳說中的Thunk方案。這也是MFC/ATL所使用的方現。Thunk在這裏是指一小段代碼,這段代碼無法用C/C++來表示(因爲是動態代碼),只能用機器碼寫(彙編都不好使),這也就造成本方案在跨平臺時有點小麻煩,好在Windows本身也支持不了幾種CPU。這裏僅以x86體系來討論。

  首先說下x86下__stdcall調用約定。 Windows API要求窗口過程必須使用__stdcall調用約定。 該約定通過棧來傳遞參數,通過eax寄存器返回值。參數壓棧順序爲從右到左。 那麼對於窗口過程的定義

LRESULT (CALLBACK *WNDPROC)(HWND,UINT,WPARAM,LPARAM);

來看,當系統調用我們指定的窗口過程時,從右向左依次將LPARAM, WPARAM, UINT, HWND壓入棧中,然後使用call指令進入窗口過程。 THUNK的目標就是在這個時候將棧上的HWND參數替換爲C++類實例指針。看下此時的棧結構先

棧底
......
......
棧頂 + 7
棧頂 + 6
棧頂 + 5
棧頂 + 4    
4-7 原本存放着HWND參數,在執行完Thunk後,其值爲類實例地址
-------------------------------------------
棧頂 + 3    
0-3 存放着窗口過程的返回地址,
棧頂 + 2    WindowProc裏在return之後會返回到該地址繼續運行
棧頂 + 1
棧頂 + 0

因爲THUNK代碼在運行時生成,此時C++類實例的地址已經確定,那麼對於THUNK代碼來說,類實例指針就是個立即數(常數)。 那麼基本指令應該是

mov mov dword ptr [esp+0x4], $class_instance
jmp $real_window_proc

其中 $class_instance 是我們要填入的C++類實際的指針, 而$real_window_proc是我們真正的windowproc的地址,但該windowproc第一個參數不是HWND,而是C++類指針,也就是該函數應該類似於:

複製代碼
LRESULT CALLBACK cpp_window_proc(cpp_window_class* thiz, UINT msg, WPARAM wParam, LPARAM lParam) {
    thiz->window_proc(msg, wParam, lParam); 
    /* 實際上thiscall正好是第一個(隱形)參數爲this指針,
     * 所以這裏也可以直接把 cpp_window_class::window_proc的地址作爲$real_window_proc的值
     * 但那樣對於使用虛函數的情況有些複雜, 所以最好還是用靜態或全局函數轉一下。
     */
}
複製代碼

以下是一個實現這個機制的僞代碼片段

複製代碼
struct wndproc_thunk;
struct window
{
    HWND _handle;
    wndproc_thunk* _thunk;
    HRESULT WINAPI static static_window_proc(HWND, UINT, WPARAM, LPARAM);
    HRESULT WINAPI window_proc(UINT, WPARAM, LPARAM);
};

#pragma pack(push,1)
struct wndproc_thunk
{
     DWORD   mov;          // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)
     DWORD   thiz;         //
     BYTE    jmp;          // jmp WndProc
     DWORD   relproc;      // relative jmp
};
#pragma pack(pop)


window win;
win._thunk = alloc_wndproc_thunk();
win._thunk->mov = 0x042444C7;                                        // mov dword ptr [esp+0x4],
win._thunk->thiz = &win;                                             // thiz
win._thunk->jmp = 0xe9;                                              // jmp
win._thunk->relproc = window::static_window_proc - (win._thunk + 1); // relproc
FlushInstructionCache(GetCurrentProcess(), win._thunk, sizeof(wndproc_thunk));
SetWindowLongPtr(win._handle, GWLP_WNDPROC, (LONG_PTR) win._thunk);
複製代碼

最後補充一句,因爲新版Windows及最新的Server Packs都加入了數據執行保護(DEP)功能,因此如果直接在堆或棧上分配空間構造thunk的話,因爲堆和棧所在內存都被默認標記爲不可執行,從而導致系統異常。這裏就需要VirtualAlloc方法動態爲thunk分配內在,並使用PAGE_EXECUTE_READWRITE標誌,記得最後使用VirtualFree釋放該內存。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章