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(,,);
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技术 / 非静态类成员函数作为回调函数 的实现方法的更多相关文章
- 关于C++中的非静态类成员函数指针
昨天发现了一个问题,就是使用对类中的非静态成员函数使用std::bind时,不能像普通函数一样直接传递函数名,而是必须显式地调用&(取地址),于是引申出我们今天的问题:非静态类成员函数指针和普 ...
- C++中 线程函数为静态函数 及 类成员函数作为回调函数
线程函数为静态函数: 线程控制函数和是不是静态函数没关系,静态函数是在构造中分配的地址空间,只有在析构时才释放也就是全局的东西,不管线程是否运行,静态函数的地址是不变的,并不在线程堆栈中static只 ...
- C++中类成员函数作为回调函数
注:与tr1::function对象结合使用,能获得更好的效果,详情见http://blog.csdn.net/this_capslock/article/details/38564719 回调函数是 ...
- 使用匿名函数在回调函数中正确访问JS循环变量
有时候, 需要以不同的参数调用某个URL,并且在回调函数中仍然可以访问正在使用的参数, 这时候, 需要使用闭包保存当前参数, 否则, 当回调函数执行时, 之前的参数很可能早已被修改为最后一个参数了. ...
- MATLAB中为控件(uicontrol)绑定Callback函数(回调函数)
笔者走了许多弯路,终于找到这个方法,分享给大家. 'callback',@(~,~)colormapeditor(h) 如果版本老不支持“~”这种写法,那就改成: 'callback',@(x,y)c ...
- js中匿名函数和回调函数
匿名函数: 通过这种方式定义的函数:(没有名字的函数) 作用:当它不被赋值给变量单独使用的时候 1.将匿名函数作为参数传递给其他函数 2.定义某个匿名函数来执行某些一次性任务 var f = func ...
- JS中的匿名函数、回调函数、匿名回调函数
工欲善其事必先利其器 在学习JavaScript设计模式一书时,遇到了“匿名回调函数”这个概念,有点疑惑,查找了些资料重新看了下函数的相关知识点之后,对这个概念有了认识.九层之台,起于垒土.在熟悉这一 ...
- 2016-12-14:通过static关键字,使用类成员函数作为回调函数
#include <iostream> using namespace std; class Callee { public: void PrintInfo(int i) { cout & ...
- delphi 中的函数指针 回调函数(传递函数指针,以及它需要的函数参数)
以下代码仅仅是测试代码:delphi XE7 UP1 interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.V ...
随机推荐
- 怎样编写YARN应用程序
(注意:本文的分析基于Hadoop trunk上的"Revision 1452188"版本号,详细可參考:http://svn.apache.org/repos/asf/hadoo ...
- Python标准库:内置函数type(object)
type(object) type(name, bases, dict) 本函数是返回对象的类型对象.仅仅有一个參数object时,直接返回对象的类型对象.假设仅仅是想推断一个对象是否属于某一个类的对 ...
- Android 关于 ActionBarSherlock 的使用
原文地址 本文内容 使用 主题化 ActionBarSherlock 演示项目 本文 ActionBarSherlock 简单演示 最近一个星期被 actionsherlock 搞得很不爽(光去足疗店 ...
- ArcGIS读取dem格式数据
DEM是GIS常用的一种数据,用来做各种分析.展示等,十分有用!它实质上就是一个栅格,只不过这个栅格值表示高程,常用的格式是tif,grid等.今天听到了另外一种说法:*.dem是最常见到的DEM的格 ...
- 牛客网-《剑指offer》-跳台阶
题目:http://www.nowcoder.com/practice/8c82a5b80378478f9484d87d1c5f12a4 C++ class Solution { public: in ...
- windows 2012授权模型
转自:http://www.aidanfinn.com/?p=13090 Remember that Microsoft licenses its servers at the physical le ...
- JAVA设计模式——第 1 章 策略模式【Strategy Pattern】(转)
刘备要到江东娶老婆了,走之前诸葛亮给赵云(伴郎)三个锦囊妙计,说是按天机拆开解决棘手问题,嘿,还别说,真是解决了大问题,搞到最后是周瑜陪了夫人又折兵呀,那咱们先看看这个场景是什么样子的. 先说这个场景 ...
- ajax 与springmvc交互返回数据
1.controller将数据封装成json格式返回页面 @RequestMapping("/dataList") public void datalist(CsoftCunsto ...
- ajax请求,返回值为304 Not Modified 错误原因与解决办法
先说原因吧,这是因为http请求的缓存问题引起的 前后调用了两个相同的请求,服务器懒得给你重新发一个请求,所以就304咯 那怎么办呢? 解决方法也很简单,加一个时间戳就行了 比如: 原请求为: $.g ...
- system.DateTime ToDateTime(System.String)”,因此该方法无法转换为存储表达式-解决方法
LINQ to Entities的lambda表达式中如果需要转换时间及各种时间格式请使用System.Data.Entity的类DbFunctions的各种方法 例如: IsOverdue = db ...