原文: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(,,);
    a2.foo(,,);

  
    这是我们的书写方式, 编译器在编译时将生成如下调用(只考虑__cdecl和__stdcall,没有哪一个全局函数需要__thiscall的回调):

    foo(&a1,,,);
    foo(&a2,,,);

    我在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 push dword ptr[esp] ;再次压入返回地址
C7 ?? ?? ?? ?? mov dword ptr[esp+],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  ?? ?? ?? ??            pop     dword ptr ds:[?? ?? ?? ??]  ;弹出并保存返回地址(我的变量)
?? ?? ?? ?? push this ;压入this指针
?? ?? ?? ?? push my_ret ;压入我的返回地址
9E ?? ?? ?? ?? jmp (relative target) ;跳转到成员函数
C4 add esp, ;清除this栈
3E FF ?? ?? ?? ?? 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[];
byte4 mov;
byte4 pthis;
byte1 jmp;
byte4 addr;
}; struct MCODE_CDECL{
byte1 pop_ret[];
byte1 push_this[];
byte1 push_my_ret[];
byte1 jmp_mfn[];
byte1 add_esp[];
byte1 jmp_ret[];
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,,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[] = 0xFF;
m_pthis->m_stdcall.push[] = 0x34;
m_pthis->m_stdcall.push[] = 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+); 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[] = 0x3E;
m_pthis->m_cdecl.pop_ret[] = 0x8F;
m_pthis->m_cdecl.pop_ret[] = 0x05;
*(byte4*)&m_pthis->m_cdecl.pop_ret[] = (byte4)&m_pthis->m_cdecl.ret_addr; m_pthis->m_cdecl.push_this[] = 0x68;
*(byte4*)&m_pthis->m_cdecl.push_this[] = (byte4)pThis; m_pthis->m_cdecl.push_my_ret[] = 0x68;
*(byte4*)&m_pthis->m_cdecl.push_my_ret[] = (byte4)&m_pthis->m_cdecl.add_esp[]; m_pthis->m_cdecl.jmp_mfn[] = 0xE9;
*(byte4*)&m_pthis->m_cdecl.jmp_mfn[] = (byte4)mfn-((byte4)&m_pthis->m_cdecl.jmp_mfn+); m_pthis->m_cdecl.add_esp[] = 0x83;
m_pthis->m_cdecl.add_esp[] = 0xC4;
m_pthis->m_cdecl.add_esp[] = 0x04; m_pthis->m_cdecl.jmp_ret[] = 0x3E;
m_pthis->m_cdecl.jmp_ret[] = 0xFF;
m_pthis->m_cdecl.jmp_ret[] = 0x25;
*(byte4*)&m_pthis->m_cdecl.jmp_ret[] = (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=; i<; 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,);
} private:
INT_PTR CALLBACK DialogProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
switch(uMsg)
{
case WM_CLOSE:
EndDialog(hWnd,);
return ;
}
return ;
}
private:
AThunk m_Thunk;
}; /////////////////////////////////////////////////////////
//第三个:__stdcall 回调类型:内部线程
/////////////////////////////////////////////////////////
class AThread
{
public:
AThread()
{
void* pthunk = m_Thunk.Stdcall(this,&AThread::ThreadProc);
HANDLE handle = (HANDLE)_beginthreadex(NULL,,(unsigned int (__stdcall*)(void*))pthunk,(void*),,NULL);
WaitForSingleObject(handle,INFINITE);
CloseHandle(handle);
} private:
unsigned int __stdcall ThreadProc(void* pv)
{
int i = (int)pv;
while(i--){
cout<<"i="<<i<<endl;
}
return ;
}
private:
AThunk m_Thunk;
}; int main(void)
{
ASTDCALL as;
ACDCEL ac;
cout<<endl;
AThread at;
return ;
}

哎呀, 不想写了, 先去吃个宵夜, 有啥问题Q我吧~~~~

全部源代码及测试下载(VC6):http://share.weiyun.com/7c5cf2f76fc119c06485222a2b6909d5

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

C++中的Thunk技术 / 非静态类成员函数作为回调函数 的实现方法的更多相关文章

  1. 关于C++中的非静态类成员函数指针

    昨天发现了一个问题,就是使用对类中的非静态成员函数使用std::bind时,不能像普通函数一样直接传递函数名,而是必须显式地调用&(取地址),于是引申出我们今天的问题:非静态类成员函数指针和普 ...

  2. C++中 线程函数为静态函数 及 类成员函数作为回调函数

    线程函数为静态函数: 线程控制函数和是不是静态函数没关系,静态函数是在构造中分配的地址空间,只有在析构时才释放也就是全局的东西,不管线程是否运行,静态函数的地址是不变的,并不在线程堆栈中static只 ...

  3. C++中类成员函数作为回调函数

    注:与tr1::function对象结合使用,能获得更好的效果,详情见http://blog.csdn.net/this_capslock/article/details/38564719 回调函数是 ...

  4. 使用匿名函数在回调函数中正确访问JS循环变量

    有时候, 需要以不同的参数调用某个URL,并且在回调函数中仍然可以访问正在使用的参数, 这时候, 需要使用闭包保存当前参数, 否则, 当回调函数执行时, 之前的参数很可能早已被修改为最后一个参数了. ...

  5. MATLAB中为控件(uicontrol)绑定Callback函数(回调函数)

    笔者走了许多弯路,终于找到这个方法,分享给大家. 'callback',@(~,~)colormapeditor(h) 如果版本老不支持“~”这种写法,那就改成: 'callback',@(x,y)c ...

  6. js中匿名函数和回调函数

    匿名函数: 通过这种方式定义的函数:(没有名字的函数) 作用:当它不被赋值给变量单独使用的时候 1.将匿名函数作为参数传递给其他函数 2.定义某个匿名函数来执行某些一次性任务 var f = func ...

  7. JS中的匿名函数、回调函数、匿名回调函数

    工欲善其事必先利其器 在学习JavaScript设计模式一书时,遇到了“匿名回调函数”这个概念,有点疑惑,查找了些资料重新看了下函数的相关知识点之后,对这个概念有了认识.九层之台,起于垒土.在熟悉这一 ...

  8. 2016-12-14:通过static关键字,使用类成员函数作为回调函数

    #include <iostream> using namespace std; class Callee { public: void PrintInfo(int i) { cout & ...

  9. delphi 中的函数指针 回调函数(传递函数指针,以及它需要的函数参数)

    以下代码仅仅是测试代码:delphi XE7 UP1 interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.V ...

随机推荐

  1. JS调试必备的5个debug技巧_javascript技巧

    JS调试必备的debug调试javascript技巧 1. debugger; 我以前也说过,你可以在JavaScript代码中加入一句debugger;来手工造成一个断点效果.需要带有条件的断点吗? ...

  2. eclipse启动错误:java.lang.NoClassDefFoundError: org/eclipse/core/resources/IContainer

    转自:http://blog.csdn.net/niu_hao/article/details/9332521 eclipse启动时报错如下:java.lang.NoClassDefFoundErro ...

  3. redis-dev

    redis install by centos   -------------------------------------------------------------------------- ...

  4. Timer 与 TimerTask 示例

    , 1000);// 1秒后执行 然后每隔1秒 执行一次 ); ); timer.cancel();//停止任务(程序停止) } } /** * 启动刷新滚动数据的定时器 */public void ...

  5. Office 超级录屏如何旋转视频90度之后保存

    打开视频转换专家   添加视频后点击编辑,然后在旋转的地方设置旋转,应用   输出可以正常播放  

  6. Getting in Line UVA 216

     Getting in Line  Computer networking requires that the computers in the network be linked. This pro ...

  7. tornado code

    # get the requtest URL self.request.uri

  8. python xlwt写excel格式控制 颜色、模式、编码、背景色

    关于写excel的格式控制,比如颜色等等 import xlwt from datetime import datetime font0 = xlwt.Font() font0.name = 'Tim ...

  9. MySql8.0数据库链接报错The driver has not received any packets from the server

    1.我使用MySql数据库8.0版本,然后驱动改成了 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306 ...

  10. 演示一下:rm -rf /