C++中的Thunk技術 / 非靜態類成員函數作為回調函數 的實現方法


  原文:https://blog.twofei.com/616/

  用我的理解通俗地解釋一下什么是C++中的Thunk技術吧!
  Thunk技術就是申請一段可執行的內存, 並通過手動構造CPU指令的形式來生成一個小巧的, 具有明確作用的代碼塊.

  小巧? 具有明確作用? 你曾經初學C++時, 如果我沒猜錯的話, 肯定嘗試過用C++封裝一個窗口類(因為我也嘗試過 :-) ),
在封裝窗口類的時候,在類內部定義一個私有(或公有)的成員函數來作為窗口回調函數, 並以
CreateWindowEx(...,&MyWindowClass::WindowProc,...)的形式構造一個窗口, 可哪知, 這完全是行不通的, 因為(非靜態)類
成員函數的指針可不是簡單的全局成員函數指針那樣!

  於是, 你不得不把窗口過程定義為全局函數. 但是這樣的話, 每個類都共享一個窗口過程了, 這顯然不行! 於是,你可能又想到了
一種算是解決辦法的辦法, 使用CreateWindowEx的最后一個參數LPARAM來傳遞this指針! 關於窗口類的封裝, 這里我不再多說, 因為
我打算再寫一篇文章介紹用多種方法來實現窗口類的封裝, 當然, 這里將要討論的Thunk技術算是最完美的一種了! 但是,Thunk技術也
不只是用於封裝窗口類, 也可以用來封裝線程類, etc.

  傳言這種技術來自於ATL/WTL, 我不會ATL/WTL, Thunk技術是我在網上學來的.
  MFC不是使用我接下來要介紹的通用(非完全)Thunk方式, 關於MFC的封裝方式, 我將在另一篇文章里面提及.
  這里有一篇介紹通過Thunk技術的文檔:Generic Thunk with 5 combinations of Calling Conventions

  好吧, 言歸正傳, 談談Thunk的原理與實現...

  要理解Thunk的實現, 需要清楚C/C++中的函數調用約定, 如果有不懂的, 可以參考:C/C++/動態鏈接庫DLL中函數的調用約定與名稱修飾

  C++的成員函數(不討論繼承)在調用時和普通的函數並沒有太大的區別, 唯一很重要的是, 需要在調用每個非靜態成員函數時悄悄地
傳入this指針. 在類內部調用時的直接調用, 或在類外部調用時通過obj->MemberFunction的形式調用時, 編譯器都在生成代碼的時候
幫我們傳入了this指針, 所以我們能正確訪問類內部的數據.

  但是, 像Windows的窗口回調函數WindowProc, 線程的回調函數ThreadProc, SQLite3的回調函數sqlite3_callback在被傳給主調函數時,
它們是不能被直接使用的, 因為主調函數不屬於類的成員函數, 他們也沒有this指針!

  看看下面的代碼:

    A a1,a2;
    a1.foo(1,2,3);
    a2.foo(4,5,6);

 


  
    這是我們的書寫方式, 編譯器在編譯時將生成如下調用(只考慮__cdecl和__stdcall,沒有哪一個全局函數需要__thiscall的回調):

    foo(&a1,1,2,3);
    foo(&a2,4,5,6);

 


    我在C/C++/動態鏈接庫DLL中函數的調用約定與名稱修飾中已經討論過這個東西了...

  好了, 現在我們知道foo函數的原型可以是如下的形式 int __cdecl foo(int a,int b,intc);
  假如我們有一個全局的函數, 她的原型是這樣的:

int func( int (__cdecl*)(int,int,int) );

   你會怎樣把A類里面的foo作為回調, 傳遞給func?  func(&A::foo); ? 這是不可行的, 我們需要借助Thunk!


  1.下面將拿Windows中的WindowProc窗口回調函數來作具體講解__stdcall的回調函數Thunk應用.

  Windows的窗口管理在調用我們提供的全局窗口過程時, 此時的堆棧形式如下:
    低                                               高
  -----------------------------------------------------------
   返回地址     hWnd      uMsg       wParam      lParam


  如果我們將WindowProc定義為類成員的形式, 並在類內調用她, 則參數棧應該是如下形式(__cdecl,__stdcall):
    低                                               高
  --------------------------------------------------------------
   返回地址     this   hWnd      uMsg       wParam      lParam

  
  好了, 現在我們就可以動動手腳, 修改一下堆棧, 傳入this指針, 然后就可以交給我們的成員WindowProc函數來處理啦~

  我們申請一段可執行的內存, 並把他作為回調函數傳遞給DialogBoxParam/CreateDialogParam,(這里只討論對話框)
  申請可執行內存, 使用 VirtualAlloc
  
  因為是WindowProc是__stdcall調用約定, 就算我們多壓入了一個this參數, 也不管調用者的事, 因為堆棧是由被調用者(windowProc)
來清理的. 雖然只有4個顯式參數, 但作為成員函數的WindowProc在結束的時候是用ret 14h返回的, this被自動清除, 你知道為什么嗎?
  我們只需構造如下的3條簡單的指令即可: 

    machine code                    assembly code                       comment
    ------------------------------------------------------------------------------------------
    FF 34 24                        push    dword ptr[esp]              ;再次壓入返回地址
    C7 44 24 04 ?? ?? ?? ??         mov     dword ptr[esp+4],this       ;修改前面那個返回地址為this指針
    E9 ?? ?? ?? ??                  jmp     (relative target)           ;轉到成員函數

 

  你沒有看錯, 真的就只需要這么幾條簡單的指令~~~~ :-)


  2.下面再看一個__cdecl的回調函數的Thunk技術的實現
    __cdecl形式的回調函數的特點:
      1.參數個數比函數聲明要多一個this
      2.參數棧由調用者清理

    我們需要以同樣的方式壓入this指針, 但是__cdecl約定是由調用者來清理參數棧, 我們多傳了一個this指針進去, 如果直接返回,
  勢必會導致堆棧指針ESP錯誤, 所以, this指針必須由我們的程序來清除, 返回時保持被調用前一樣就行了.

    作為一個完整的函數, 我們不可能在函數的最后插入一條"add esp,4"來解決問題, 這辦不到.
    __cdecl的Thunk的實現, 我在網上也沒找到答案, 由於我匯編也不咋樣, 所以搞了較長一段時間才把她搞出來~ 也算一勞永逸了.

    我的處理辦法(較__stdcall復雜, 但也只有幾條指令而已):
      1.彈出並保存原來的返回地址
      2.壓入this指針
      3.壓入我的返回地址
      4.轉到成員函數執行
      5.清理this參數棧
      6.跳轉到原返回地址

    匯編機器指令的實現(我並不擅長匯編, 你應該覺得還可以再優化一下):

    3E 8F 05 ?? ?? ?? ??            pop     dword ptr ds:[?? ?? ?? ??]  ;彈出並保存返回地址(我的變量)
    68 ?? ?? ?? ??                  push    this                        ;壓入this指針
    68 ?? ?? ?? ??                  push    my_ret                      ;壓入我的返回地址
    9E ?? ?? ?? ??                  jmp     (relative target)           ;跳轉到成員函數
    83 C4 04                        add     esp,4                       ;清除this棧
    3E FF 25 ?? ?? ?? ??            jmp     dword ptr ds:[?? ?? ?? ??]  ;轉到原返回地址

 

 






  下面貼出我寫的完整代碼:

//Thunk.h
//
ts=sts=sw=4
//女孩不哭 2013-09-11 22:00
//保留所有權利 #ifndef __THUNK_H__ #define __THUNK_H__ class AThunk { public: AThunk(); ~AThunk(); public: template<typename T> void* Stdcall(void* pThis,T mfn) { return fnStdcall(pThis,getmfn(mfn)); } template<typename T> void* Cdeclcall(void* pThis,T mfn) { return fnCdeclcall(pThis,getmfn(mfn)); } private: typedef unsigned char byte1; typedef unsigned short byte2; typedef unsigned int byte4; void* fnStdcall(void* pThis,void* mfn); void* fnCdeclcall(void* pThis,void* mfn); template<typename T> void* getmfn(T t) { union{ T t; void* p; }u; u.t = t; return u.p; } private: #pragma pack(push,1) struct MCODE_STDCALL{ byte1 push[3]; byte4 mov; byte4 pthis; byte1 jmp; byte4 addr; }; struct MCODE_CDECL{ byte1 pop_ret[7]; byte1 push_this[5]; byte1 push_my_ret[5]; byte1 jmp_mfn[5]; byte1 add_esp[3]; byte1 jmp_ret[7]; byte4 ret_addr; }; #pragma pack(pop) private: MCODE_CDECL m_cdecl; MCODE_STDCALL m_stdcall; AThunk* m_pthis; }; #endif//!__THUNK_H__

 

//Thunk.cpp
//ts=sts=sw=4
//女孩不哭 2013-09-11 22:00
//保留所有權利
#include <Windows.h>
#include "Thunk.h"

AThunk::AThunk()
{
    m_pthis = (AThunk*)VirtualAlloc(NULL,sizeof(*this),MEM_COMMIT,PAGE_EXECUTE_READWRITE);
}

AThunk::~AThunk()
{
    if(m_pthis){
        VirtualFree(m_pthis,0,MEM_RELEASE);
    }
}

void* AThunk::fnStdcall(void* pThis,void* mfn)
{
    /****************************************************************************************
    machine code                    assembly code                       comment
    ------------------------------------------------------------------------------------------
    FF 34 24                        push    dword ptr[esp]              ;再次壓入返回地址
    C7 44 24 04 ?? ?? ?? ??         mov     dword ptr[esp+4],this       ;傳入this指針
    E9 ?? ?? ?? ??                  jmp     (relative target)           ;轉到成員函數
    ****************************************************************************************/

    m_pthis->m_stdcall.push[0] = 0xFF;
    m_pthis->m_stdcall.push[1] = 0x34;
    m_pthis->m_stdcall.push[2] = 0x24;

    m_pthis->m_stdcall.mov = 0x042444C7;
    m_pthis->m_stdcall.pthis = (byte4)pThis;

    m_pthis->m_stdcall.jmp = 0xE9;
    m_pthis->m_stdcall.addr = (byte4)mfn-((byte4)&m_pthis->m_stdcall.jmp+5);

    FlushInstructionCache(GetCurrentProcess(),&m_pthis->m_stdcall,sizeof(m_pthis->m_stdcall));

    return &m_pthis->m_stdcall;
}

void* AThunk::fnCdeclcall(void* pThis,void* mfn)
{
    /****************************************************************************************
    machine code                    assembly code                       comment
    ------------------------------------------------------------------------------------------
    3E 8F 05 ?? ?? ?? ??            pop     dword ptr ds:[?? ?? ?? ??]  ;彈出並保存返回地址
    68 ?? ?? ?? ??                  push    this                        ;壓入this指針
    68 ?? ?? ?? ??                  push    my_ret                      ;壓入我的返回地址
    9E ?? ?? ?? ??                  jmp     (relative target)           ;跳轉到成員函數
    83 C4 04                        add     esp,4                       ;清除this棧
    3E FF 25 ?? ?? ?? ??            jmp     dword ptr ds:[?? ?? ?? ??]  ;轉到原返回地址
    ****************************************************************************************/
    m_pthis->m_cdecl.pop_ret[0] = 0x3E;
    m_pthis->m_cdecl.pop_ret[1] = 0x8F;
    m_pthis->m_cdecl.pop_ret[2] = 0x05;
    *(byte4*)&m_pthis->m_cdecl.pop_ret[3] = (byte4)&m_pthis->m_cdecl.ret_addr;

    
    m_pthis->m_cdecl.push_this[0] = 0x68;
    *(byte4*)&m_pthis->m_cdecl.push_this[1] = (byte4)pThis;
    
    m_pthis->m_cdecl.push_my_ret[0] = 0x68;     
    *(byte4*)&m_pthis->m_cdecl.push_my_ret[1] = (byte4)&m_pthis->m_cdecl.add_esp[0];
    
    m_pthis->m_cdecl.jmp_mfn[0] = 0xE9;
    *(byte4*)&m_pthis->m_cdecl.jmp_mfn[1] = (byte4)mfn-((byte4)&m_pthis->m_cdecl.jmp_mfn+5);
    
    m_pthis->m_cdecl.add_esp[0] = 0x83;
    m_pthis->m_cdecl.add_esp[1] = 0xC4;
    m_pthis->m_cdecl.add_esp[2] = 0x04;

    m_pthis->m_cdecl.jmp_ret[0] = 0x3E;
    m_pthis->m_cdecl.jmp_ret[1] = 0xFF;
    m_pthis->m_cdecl.jmp_ret[2] = 0x25;
    *(byte4*)&m_pthis->m_cdecl.jmp_ret[3] = (byte4)&m_pthis->m_cdecl.ret_addr;
    
    FlushInstructionCache(GetCurrentProcess(),&m_pthis->m_cdecl,sizeof(m_pthis->m_cdecl));
    
    return &m_pthis->m_cdecl;   
}

 


  下面再貼出一篇使用示例程序, 我已經列出了我見過的常見的回調函數的使用形式:

//main.cpp
#include <iostream>
#include <Windows.h>
#include <process.h>
#include "Thunk.h"
#include "resource.h"
using namespace std;

/////////////////////////////////////////////////////////
//第一個:__cdecl 回調類型
/////////////////////////////////////////////////////////

typedef int (__cdecl* CB)(int n);

void output(CB cb)
{
    for(int i=0; i<3; i++){
        cb(i);
    }
}

class ACDCEL
{
public:
    ACDCEL()
    {
        void* pthunk = m_Thunk.Cdeclcall(this,&ACDCEL::callback);
        ::output(CB(pthunk));
    }

private:
    int __cdecl callback(int n)
    {
        cout<<"n:"<<n<<endl;
        return n;
    }

private:
    AThunk m_Thunk;
};

/////////////////////////////////////////////////////////
//第二個:__stdcall 回調類型:封裝窗口類
/////////////////////////////////////////////////////////
class ASTDCALL
{
public:
    ASTDCALL()
    {
        void* pthunk = m_Thunk.Stdcall(this,&ASTDCALL::DialogProc);
        DialogBoxParam(GetModuleHandle(NULL),MAKEINTRESOURCE(IDD_DIALOG1),NULL,(DLGPROC)pthunk,0);
    }
    
private:
    INT_PTR CALLBACK DialogProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
    {
        switch(uMsg)
        {
        case WM_CLOSE:
            EndDialog(hWnd,0);
            return 0;
        }
        return 0;
    }
private:
    AThunk m_Thunk;
};

/////////////////////////////////////////////////////////
//第三個:__stdcall 回調類型:內部線程
/////////////////////////////////////////////////////////
class AThread
{
public:
    AThread()
    {
        void* pthunk = m_Thunk.Stdcall(this,&AThread::ThreadProc);
        HANDLE handle = (HANDLE)_beginthreadex(NULL,0,(unsigned int (__stdcall*)(void*))pthunk,(void*)5,0,NULL);
        WaitForSingleObject(handle,INFINITE);
        CloseHandle(handle);
    }
    
private:
    unsigned int __stdcall ThreadProc(void* pv)
    {
        int i = (int)pv;
        while(i--){
            cout<<"i="<<i<<endl;
        }
        return 0;
    }
private:
    AThunk m_Thunk;
};

int main(void)
{
    ASTDCALL as;
    ACDCEL ac;
    cout<<endl;
    AThread at;
    return 0;
}

 


哎呀, 不想寫了, 先去吃個宵夜, 有啥問題Q我吧~~~~

全部源代碼及測試下載(VC6):http://share.weiyun.com/7c5cf2f76fc119c06485222a2b6909d5

女孩不哭 @ 2013-09-11 22:32:25 @ http://www.cnblogs.com/nbsofer
-------------------------------


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM