zoukankan      html  css  js  c++  java
  • 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
    -------------------------------

  • 相关阅读:
    Azkaban 简介(一)
    大数据平台搭建(Ambari +HDP)
    大数据平台比较-CDH、HDP、CDP
    Kylin 操作使用(六)
    Kylin 安装部署(五)
    Kylin 核心概念(四)
    数据流图
    android:sharedUserId
    Android的uid与UserHandle
    C++ 多态
  • 原文地址:https://www.cnblogs.com/memset/p/thunk_in_cpp.html
Copyright © 2011-2022 走看看