Thunk技术,一般认为是在程序中直接构造出可执行代码的技术(在正常情况下,这是编译器的任务)。《深度探索C++对象模型》中对这个词的来源有过考证(在中文版的162页),说thunk是knuth的倒拼字。knuth就是大名鼎鼎的计算机经典名著《The Art of Computer Programming》的作者,该书被程序员们称为“编程圣经”,与牛顿的“自然哲学的数学原理”等一起,被评为“世界历史上最伟大的十种科学著作”之一(也不知是谁评的,我没查到,不过反正这本书很牛就是了)。
一般情况下,使用thunk技术都是事先查好指令的机器码,然后将数组或结构体赋值为这些机器码的二进制值,最后再跳转到数组或结构体的首地址。比如在参考文献[1]中的代码:
void foo(int a)
{ printf ("In foo, a = %d\n", a); }
unsigned char code[9];
* ((DWORD *) &code[0]) = 0x042444FF; /* inc dword ptr [esp+4] */
code[4] = 0xe9; /* JMP */
* ((DWORD *) &code[5]) = (DWORD) &foo - (DWORD) &code[0] - 9; /* 跳转偏移量 */
void (*pf)(int/* a*/) = (void (*)(int)) &code[0];
pf (6);
这是一段典型的thunk代码,其执行结果是“In foo, a = 7”。
可以看到,它定义了一个数组code[9],然后将事先查好的各汇编指令的机器码直接赋值给数组。然后定义一个函数指针等于数组的首地址,最后通过该函数指针调用thunk代码。这里使用了函数指针完成调用,好处是代码比较清晰易读。也可以使用汇编代码jmp或call来完成,这样就不必额外定义一个函数指针。
网络上的thunk代码,基本上都是这个思路。如果你实际写一段这样的代码,一定会发现很麻烦。对着教科书查找每一个汇编指令的机器码,相信不会是一件愉快的事情。其实我们回过头来想想,这件事计算机来做不是最合适吗,编译器不就是做这个事情的吗?
以上面的代码为例,让我们重新考虑一下整个过程。我们的目的是在调用函数foo之前将参数增加1。一般而言,这样做肯定是没有foo函数的源代码或者不允许修改源代码,否则直接改foo函数的代码就好了,何必这么麻烦。为了调用时候的简单化,定义一个函数指针是比较合适的,否则每次调用都写汇编代码jmp或call太麻烦。这样一来,函数指针必须指向一个代码段的地址。但是这个代码段必须用机器码来构造吗,直接写汇编代码也同样可以做到。
当然,这里有一个问题。我们写汇编指令的时候,必须是一条指令一条指令的写,不能说指令写一半,然后让汇编程序去处理。上面的代码中,第一条指令inc直接写汇编语句当然没问题。但下面的jmp语句,就不能直接写。因为我们写汇编语句的时候,jmp跳转偏移量是未知的,必须编译后才知道。并且我们不能只写jmp而不写偏移量,那是通不过编译的。
这个问题可以这样解决,写jmp语句的时候,我们写一个占位的DWORD,其值设为一个特殊的值,比如0xffff(原理是这样,实际处理还要迂回一下,后面有说明)。只要在这段thunk代码中不出现这个值就好。然后执行的时候,在第一次调用之前,在thunk代码中查找该值,将其替换为计算出来的动态值。经过这样的处理,就可以彻底在thunk代码中消除机器码的直接操作。
更一般化,为了生成正确的机器码,我们用两个函数。一个用于生成机器码的模板,另一个函数用于在机器码的模板中填入需要动态计算产生的值。下面是一个例子:
void ThunkTemplate(DWORD& addr1,DWORD& addr2)//生成机器码
{
int flag = 0;
DWORD x1,x2;
if(flag)
{
//注意,这个括号中的代码无法直接执行,因为其中可能含有无意义的占位数。
__asm
{
thunk_begin:
;//这里写thunk代码的汇编语句.
...
thunk_end: ;
}
}
__asm
{
mov x1,offset thunk_begin; //取 Thunk代码段 的地址范围.
mov x2,offset thunk_end;
}
addr1 = x1;
addr2 = x2;
}
上面的函数用于生成thunk的机器码模板,之所以称为模板,是因为其中包含了无意义的占位数,必须将这些占位数替换为有意义的值之后,才可以执行这些代码。因此,在函数中thunk代码模板放在一个if(0)语句中,就是避免调用该函数的时候执行thunk代码。另外,为了能方便的得到thunk代码模板的地址,这里采用一个函数传出thunk代码的首尾地址。
至于替换占位数的功能是很简单的,直接替换就好。
void ReplaceCodeBuf(BYTE *code,int len, DWORD old,DWORD x)//完成动态值的替换.
{
int i=0;
for(i=0;i<len-4;++i)
{
if(*((DWORD *)&code[i])==old)
{
*((DWORD *)&code[i]) = x;
return ;
}
}
}
这样使用两个函数:
DWORD addr1,addr2;
ThunkTemplate(addr1,addr2);
memset(m_thunk,0,100);//m_thunk是一个数组: char m_thunk[100];
memcpy(m_thunk,(void*)addr1,addr2-addr1);//将代码拷贝到m_thunk中。
ReplaceCodeBuf(m_thunk,addr2-addr1,-1,(DWORD)((void*)this));//将m_thunk中的-1替换为this指针的值。
原理部分到此为止。下面举一个完整的,有实际意义的例子。在windows中,回调函数的使用是很常见的。比如窗口过程,又比如定时器回调函数。这些函数,你写好代码,但是却从不直接调用。相反,你把函数地址传递给系统,当系统检测到某些事件发生的时候,系统来调用这些函数。这样当然很好,不过如果你想做一个封装,将所有相关部分写成一个类,那问题就来了。
问题是,这些回调函数的形式事先已经定义好了,你无法让一个类的成员函数成为一个回调函数,因为类型不可能匹配。这不能怪微软,微软不可能将回调函数定义为一个类成员函数(该定义为什么类?),而只能将回调函数定义为一个全局的函数。并且微软其实很多时候也提供了补救措施,在回调函数中增加了一个void *的参数。这个参数一般都用来传递类的this指针。这样一来,可以这样解决:给系统提供一个全局函数作为回调函数,在该函数中通过额外的那个void *参数访问到类的对象,从而直接调用到类成员函数。如此,你的封装一样可以完成,不过多了一次函数调用而已。
但是,不是所有的回调函数都这么幸运,微软都给它们提供了一个额外的参数。比如,定时器的回调函数就没有。
VOID CALLBACK TimerProc(
HWND hwnd, // handle to window
UINT uMsg, // WM_TIMER message
UINT_PTR idEvent, // timer identifier
DWORD dwTime // current system time
);
四个参数,个个都有用途。没有地方可以让你传递那个this指针。当然了,你实在要传也可以做到,比如将hwnd设置为一个结构体的指针,其中包含原来的hwnd和一个this指针。在定时器回调函数中取出hwnd后强制转化为结构体指针,取出原来的hwnd,取出this指针。现在就可以通过this指针自由的调用类成员函数了。不过这种方法不是我想要的,我要的是一个通用,统一的解决方法。通过在参数里面加塞夹带的方法,一般也是没有问题的,不过如果碰到一个回调函数没有参数怎么办?另外,本来是封装为一个类的,结果还是要带着一个全局函数,你难道不觉得有些不爽吗?
这正是thunk技术大显身手的地方了。我们知道,所谓类成员函数,和对应的全局函数,其实就差一个this指针。如果我们在系统调用函数之前正确处理好this指针,那系统就可以正确的调用类成员函数。
具体的思路是这样的:当系统需要一个回调函数地址的时候,我们传递一个thunk代码段的地址。这个代码段做两件事:
1、准备好this指针
2、调用成员函数
关键的代码如下(完整的工程在附件中):
void ThunkTemplate(DWORD& addr1,DWORD& addr2,int calltype=0)
{
int flag = 0;
DWORD x1,x2;
if(flag)
{
__asm //__thiscall
{
thiscall_1: mov ecx,-1; //-1占位符,运行时将被替换为this指针.
mov eax,-2; //-2占位符,运行时将被替换为CTimer::CallBcak的地址.
jmp eax;
thiscall_2: ;
}
__asm //__stdcall
{
stdcall_1: push dword ptr [esp] ; //保存(复制)返回地址到当前栈中
mov dword ptr [esp+4], -1 ; //将this指针送入栈中,即原来的返回地址处
mov eax, -2;
jmp eax ; //跳转至目标消息处理函数(类成员函数)
stdcall_2: ;
}
}
if(calltype==0)//this_call
{
__asm
{
mov x1,offset thiscall_1; //取 Thunk代码段 的地址范围.
mov x2,offset thiscall_2 ;
}
}
else
{
__asm
{
mov x1,offset stdcall_1;
mov x2,offset stdcall_2 ;
}
}
addr1 = x1;
addr2 = x2;
}
上面的函数有几个地方需要说明:
1、为了能适应两种不同的成员函数调用约定,这里写了两份代码。通过参数calltype决定拷贝哪一份代码到缓冲区。
2、本来一条jmp xxxx;指令这里分解为两条指令:
mov eax,-2;
jmp eax;
这是由汇编语言的特点决定的。直接写jmp -2是通不过的(根据地址的不同,jmp汇编后可能出现好几种形式。这里必须出现一个真实的地址以便汇编器决定jmp类型)。
3、如果对this指针的知识不清楚,请参考我在vc知识库的另外一篇文章《直接调用类成员函数地址》。
设置thunk代码的完整代码如下:
DWORD FuncAddr;
GetMemberFuncAddr_VC6(FuncAddr,&CTimer::CallBcak);
DWORD addr1,addr2;
ThunkTemplate(addr1,addr2,0);
memset(m_thunk,0,100);
memcpy(m_thunk,(void*)addr1,addr2-addr1);
ReplaceCodeBuf(m_thunk,addr2-addr1,-1,(DWORD)((void*)this)); //将-1替换为this指针.
ReplaceCodeBuf(m_thunk,addr2-addr1,-2,FuncAddr); //将-2替换为成员函数的指针.
如果你还想和以前一样直接在数组中赋值机器码(毕竟这样看起来很酷,我完全理解)。那也可以这样,调用ThunkTemplate生成m_thunk后,打印出该数组的值,而后在程序中直接给m_thunk数组赋值,就象网上大部分thunk代码那样,当然在调用前要多一个步骤就是替换掉占位数。不过无论如何,调用这两个函数生成机器码应该比手工查找方便多了,如果你也这样认为,那就算我这篇文章没白写。
通用thunk下载1 通用thunk下载2
分享到:
相关推荐
thunk技术实现窗口类的封装.rtf
为了解决这个问题,我们可以利用Thunk技术将窗口类的回调函数改写为窗口对象的成员函数,从而更好地封装和管理代码。 首先,让我们理解什么是Thunk。在Windows API中,Thunk是一种用于不同地址空间或不同代码段之间...
“基于thunk技术”是指该框架可能利用了thunk(也称为转换例程)的概念。在Windows编程中,thunk主要用于处理不同调用约定(如stdcall和cdecl)之间的转换,或者是处理32位和64位代码间的交互。Thunks允许不同地址...
Thunk v0.4 by intret 是一个C++实现的Thunk技术实例,它提供了一个简单的类,并包含使用示例,帮助开发者理解和应用Thunk技术。 首先,我们要理解什么是Thunk。在Windows编程中,Thunk通常被用来解决32位和64位...
Thunk技术源于低级编程,尤其是在Windows操作系统中,它常用于解决不同代码段之间调用的问题,尤其是涉及到32位和64位API调用兼容性时。Thunk可以被认为是一种“中间人”或者“代理”,它在不同的地址空间或不同的...
例如,KWinGUI 1.0可能使用了thunk技术来适应Windows、Linux(KDE桌面环境)和其他操作系统上的差异,确保其代码能够在各种平台上一致地运行。这样,开发者无需关注底层系统的细节,就可以专注于应用的逻辑和设计。 ...
《纯净SDK与thunk技术在win32窗口编程中的应用》 在计算机编程领域,尤其是Windows平台下,Win32 API(Application Programming Interface)扮演着至关重要的角色,它提供了操作系统级别的接口,允许开发者构建功能...
《通用Thunk技术在C语言中的应用——以"toy63r Vc"为例》 Thunk技术,源于汇编语言的概念,通常用于不同编程环境或体系结构间的接口转换。在C语言中,由于其不支持成员函数的this指针,使得在处理对象方法时面临...
本教程将详细讲解如何在Cocos2d中利用thunk技术进行文件下载,同时也会涉及curl库和C++编程语言。 一、Cocos2d框架 Cocos2d为开发者提供了丰富的图形渲染、动画、物理引擎、粒子系统等功能,使得游戏开发变得更加...
这个程序采用了Thunk技术 来自看雪的一个牛人 这个技术用到标准的SDK创建窗口中 就可以在 回调函数中调用 类内 消息响应函数了 回调函数演示: LRESULT CALLBACK CSDK::WndProc HWND hWnd UINT message WPARAM ...
而在Windows 95/98下,由于32位API调用的拦截无法直接修改代码段,可能需要借助Thunk技术和VXD技术来实现。而在后续的Windows版本中,操作系统可能加入了更多的保护措施,进一步限制了对API的直接修改,使得拦截技术...
其中,Thunk技术是一种机制,用于在不同的地址空间或不同的代码段之间转换调用,例如在32位和64位代码之间进行桥接,或者在用户模式和内核模式之间通信。在Windows线程封装类中,Thunk技术可能被用来实现线程间通信...
用 Win API 实现自绘按钮类 用过MFC自绘按钮的人都知道,是通过重载了父窗口WM_DRAWITEM的响应消息实现的。同时也要子类化按钮来得到按钮的其他有用...在我实现的按钮子类化类里,我用到Thunk技术或SetProp函数来实现的
用Delphi搞的一个商业软件,是电信97工程软件的一部分,电信局用户可以操作触摸屏自助查询话单和话费详细信息,可以查半年以内的。...杂项临时程序:如Delphi中32位程序调用16位动态链接库的Thunk技术等
标题中的“来自ATL的另一个新的thunk副本”暗示了我们即将探讨的是关于Microsoft的Active Template Library (ATL)中的thunk技术。ATL是一种C++库,它为开发COM(Component Object Model)对象提供了轻量级且高效的...
此外,为了在类中处理非静态成员函数,可以使用Thunk技术或`SetProp`函数将非静态函数转换为静态窗口过程。 自绘按钮的关键在于正确地响应`WM_PAINT`消息。在这个消息处理函数中,开发者可以绘制按钮的各个状态,...
VCL使用了一种称为Thunk技术的方法,这种方法利用函数调用过程中的堆栈机制,巧妙地将对象指针传递给窗口过程。虽然这种方式速度快,但实现复杂,不易维护。 ##### 3. SWT 相比之下,SWT利用JNI的优势,实现了更加...
### Redux-Thunk原理及源码解析 #### 一、引言 在现代前端开发中,尤其是在使用Redux进行状态管理时,经常会遇到需要处理异步操作的情况。这些操作可能包括发送网络请求、设置定时器等。然而,Redux的核心设计原则...