




已阅读5页,还剩23页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
一种实现Win32消息处理处理函数的新方法 基于一种实现Win32窗口过程函数(Window Procedure)的新方法基于Thunk实现的类成员消息处理函数JERKII.SHANG(JERKIIHOTMAIL.COM)MAR.10th-31st,2006 Windows是一个消息驱动的操作系统,在系统中发生的所有消息均需要通过消息处理过程(或叫窗口过程)进行处理。由于C+给我们在程序设计中带来更多的灵活性(如继承、重载、多态等),所以我们都希望能够使用C+的类来封装Windows中的窗口过程函数,但是Windows规定了窗口过程函数必须定义为一个全局函数,也就是说需要使用面向过程的方法来实现,为了使用面向对象的技术来实现消息处理,我们必须另辟它径。目前我们在网络上见得比较多的方式是使用Thunk将即将传递给窗口过程的第一个参数(HWND hWnd)的值使用类对象的内存地址(即this指针)进行替换(ATL使用的也是这种方法)。这样,在相应的窗口过程中通过将hWnd强制转换成类对象的指针,这样就可以通过该指针调用给类中的成员函数了。但是该方法仍然需要将该消息处理函数定义成一个静态成员函数或者全局函数。本文将介绍一种完全使用(非静态)类成员函数实现Win32的窗口过程函数和窗口过程子类化的新方法。虽然也是基于Thunk,但是实现方法完全不同于之前所说的那种,我所采用的是方法是-通过对Thunk的调用,将类对象的this指针直接传递给在类中定义的窗口处理函数(通过ECX或栈进行传递),这样就能够使Windows直接成功地调用我们窗口过程函数了。另外,本文介绍一种使用C+模板进行消息处理函数的重载,这种方法直接避免了虚函数的使用,因此所有基类及其派生类中均无虚函数表指针以及相应的虚函数表(在虚函数较多的情况下,该数组的大小可是相当可观的)。从而为每个类的实例节省了不少内存空间(相对于使用传统的函数重载机制)。关键字:C+模板,调用约定,Thunk,机器指令(编码),内嵌汇编环境:VC7,VC8,32位Windows也许你是一位使用MFC或ATL进行编程的高手,并且能在很短的时间内写出功能齐全的程序。但是,你是否曾经花时间去想过MFC或ATL是通过什么样的途径来调用我们的消息处理函数的呢?他们是怎样将Windows产生的消息事件传递给我们的呢?在MFC中定义一个从CWnd继承而来的类,相应的消息事件就会发送到我们定义的类中来,你不觉得这背后所隐藏的一切很奇怪吗?如果你的感觉是这样,那么本文将使用一种简单并且高效的方法来揭开这个神秘的面纱以看个究竟,同时我将非常详细地介绍需要使用到的各种知识,以便能让更多初学者更容易掌握这些知识。在Windows中,所有的消息均通过窗口过程函数进行处理,窗口过程函数是我们和Windows操作系统建立联系的唯一途径,窗口过程函数的声明均为:LRESULT _stdcall WndProc(HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam);MSDN有对该函数中的每个参数的详细描述,如果你现在仍然对该函数存在疑问,那么请先参照MSDN中的相关内容后,再继续阅读本文。通常,Windows均要求我们将消息处理函数定义为一个全局函数,或者是一个类中的静态成员函数。并且该函数必须采用_stdcall的调用约定(Calling Convention),因为_stdcall的函数在返回前会自己修正ESP的值(由于参数传递所导致的ESP的改变),从而使ESP的值恢复到函数调用之前的状态。全局函数或静态成员函数的使用使得我们很难发挥C+的优势(除非你一直想使用C进行Windows程序开发,如果是这样,那么你也就没有必要再继续阅读本文了),因为在这种结构下(即面向过程),我们不能很方便地在消息处理函数中使用我们的C+对象,因为在这样的消息处理函数中,我们很难得到我们对象的指针,从而导致我们不能很方便的操作C+对象中的属性。为了解决对Win32的封装,Microsoft先后推出了MFC和ATL,可以说两者都是非常优秀的解决方案,并且一直为多数用户所使用,为他们在Windows下的程序开发提供了很大的便利。但是,MFC在我们大家的眼里都是一种比较笨重的方法,使用MFC开发出来的程序都必须要在MFC相关动态库的支持下才能运行,并且这些动态库的大小可不一般(VS2005中的mfc80.dll就有1.04M),更为甚者,CWnd中包含大量的虚函数,所以每个从他继承下来的子类都有一个数量相当可观的虚函数表(虽然通过在存在虚函数的类上使用sizeof得到的结果是该类对象的大小只增长了4个字节,即虚函数表指针,但是该指针所指向的虚函数数组同样需要内存空间来存储。一个虚函数在虚函数表中需占用4个字节,如果在我们的程序中用到较多的CWnd的话,定会消耗不少内存(sizeof(CWnd)=84,sizeof(CDialog)=116)。ATL与MFC完全不同,ATL采用模板来对Win32中的所有内容进行封装,使用ATL开发出来的程序不需要任何其他动态库的支持(当然,除基本的Windows库外),ATL使用Thunk将C+对象的指针通过消息处理函数的第一个参数(即hWnd)传入,这样,在消息处理函数中,我们就可以很方便地通过该指针来访问C+对象中的属性和成员函数了,但这种方法也必须要借助几个静态成员函数来实现。本文将采用这几种技术实现一种完全由C+类成员函数(非静态)实现的Windows消息处理机制,并最终开发一个封装Windows消息处理的工具包(KWIN)。首先让我们来看看怎样使用KWIN来开发的一个简单程序:创建一个简单的窗口程序:#includekwin.hclass MyKWinApp:public KWindowImpl MyKWinApp public:MyKWinApp():KWindowImpl MyKWinApp(MyKWinAppClassName)/*Overriede the window procdure*/LRESULT KCALLBACK KWndProc(HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam)/*Do anthing you want here*/return _super:KWndProc(hWnd,msg,wParam,lParam);BOOL OnDestroy()PostQuitMessage(0);return TRUE;/*Override other message handler*/;INT _stdcall WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPTSTR lpCmdLine,int nCmdShow)MyKWinApp kapp;kapp.CreateOverlappedWindow(This is my first KWinApp);MSG msg;while(GetMessage(&msg,0,0,0)TranslateMessage(&msg);DispatchMessage(&msg);return msg.wParam;创建一个简单的对话框程序:#includekwin.hclass MyKDlgApp:public KDialogImpl MyKDlgApp public:enumIDD=IDD_DLG_MYFIRSTDIALOG;BOOL OnCommand(WORD wNotifyCode,WORD wId,HWND hWndCtrl)if(wId=IDOK)EndDialog(m_hWnd,wId);return TRUE;INT _stdcall WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPTSTR lpCmdLine,int nCmdShow)MyKDlgApp kapp;kapp.DoModal();return 0;怎么样?使用KWIN开发包后,你的程序结构是不是变得更加清晰了呢?你可以在你的类中(如MyKWinApp或MyKDlgApp)重载更多的消息处理函数,你甚至可以重载窗口过程函数(如MyKWinApp)。这里的重载跟通常意义上的重载可不是一个意思,这里的重载是使用C+模板机制实现的,而传统意义上的重载需要通过虚函数(更准确地说应该是虚函数表)来实现。好,你现在是不是很想知道,这个KWIN的内部到底是怎样实现的了吧?好,让我来一步一步带你步入KWIN的内部中去吧,现在你唯一需要的就是耐心,因为这篇文章写得似乎太长了些_.通常,如果我们要重载父类中的方法,那么我们必须在父类中将该方法声明为虚函数,这样每个拥有虚函数的类对象将会拥有一个自己的虚函数表(指针),也就是说,该虚函数表(数组)就成了该类对象的静态成员变量(之所以说是静态的,是因为该虚函数数组及其指向该数组的指针在该类的所有实例中均为同一份数据)了,C+(更确切地说应该是编译器)就是通过虚函数表来实现函数的重载机制的,因为有了虚函数表后,对虚函数的调用就是通过该虚函数表来完成的,因为编译器在生成代码的时候会根据每个虚函数在类中的位置而对其进行编号,并且通过该序号来对虚函数进行调用,所以你通常会在反汇编中看到如下代码:mov edx,this pointer;Load EAX with the value ofthis pointer(or vptr)mov edx,dword ptredx+4;Get the address of the second virtual function in vtable push.;Pass argument 1push.;Pass other arguments here.call edx;Call virtual function当子类从有虚函数的父类中派生时,他将拥有一份独立的虚函数表指针以及虚函数数组,并且开始时该数组中的内容和基类一模一样。但是,当编译器在编译时检测到子类重载了父类中的虚函数时,编译器就会修改子类的虚函数数组表,将该表中被重载的虚函数的地址改为子类中的函数地址,对于那些没有被重载的虚函数,该表中的函数地址和父类的虚函数表中的地址一样!当一个子类从多个带有虚函数的父类中继承时,该子类就拥有多个虚函数表指针了(也就是将拥有多个虚函数指针数组),当我们在这种情况下进行指针的转换的时(通常为将子类指针转换成父类指针),所进行最终操作的就是虚函数数组指针的转换。如:class A:public VBase1,public VBase2,public VBase3/*VBase1,VBase2,VBase3均为存在虚函数的父类*/.;A a;VBase1*p1;VBase2*p2;VBase3*p3;p1=&a;p2=&a;p3=&a;/假定这里a的地址为0x0012f564,那么(按字节为单位)p1=&a+0=0x0012f564,p2=&a+4=0x0012f568,p3=&a+8=0x0012f56C因为在类对象的内存布局中,编译器总是将虚函数数组指针放在偏移为0的地方。好了,似乎我们已经跑题了,关于这方面的知识在网上也可以找到很多,如果你有兴趣,可以参见我的另一篇文章:略谈虚函数的调用机制,至此,我相信你已经对C+中的重载机制有一定的认识了吧,现在再让我们来看看怎样在C+中使用模板来实现函数的重载。通过我们对周围事物(或数据)的抽象,我们得到类。如果我们再对类进行抽象,得到就是一个模板。模板是一种完全基于代码级的重用机制,同时也为我们编写一些结构良好,灵活的程序提供了手段,所有的这一切都得归功于编译器的帮助,因为编译器最终会将我们所有的这些使用这些高级技术编写的代码转换成汇编代码(或者应该说是机器码),好,废话少说为好!_。通常我们会使用下面的方式来实现函数的重载,这里似乎没有很复杂的理论要阐述,我就简单的把大致实现过程描述一下吧:template class Tclass TBase public:void foo1()printf(This is foo1 in TBasen);void foo2()printf(This is foo2 in TBasen);void callback()/*如果子类重载了TBase中的函数,通过pThis将会直接调用子类中的函数*/T*pThis=static_cast T*(this);pThis-foo1();pThis-foo2();class TDerive:public TBase TDerive public:void foo1()printf(This is foo1 in TDeriven);/*重载父类中的foo1*/;TDerive d;d.callback();输出结果为:This is foo1 in TDerive This is foo2 in TBase虽然上面的代码看起来很奇怪,因为子类将自己作为参数传递给父类。并且子类中定义的重载函数只能通过回调的方式才能被调用,但这对Windows中的消息处理函数来说无疑是一大福音,因为Windows中的消息函数均是回调函数,似乎这样的重载就是为Windows中的消息处理函数而定制的。虽然有些奇怪,但是他真的很管用,尤其是在实现我们的消息处理函数的时候。不是吗?我们通常会将一些相关的属性以及操作这些属性的方法封装到一个类中,从而实现所谓的信息(属性)隐藏,各类对象(或类的实例)之间的交互都得通过成员函数来进行,这些属性的值在另一方面也表明了该类对象在特定时刻的状态。由于每个类对象都拥有各自的属性(或状态),所以系统需要为每个类对象分配相应的属性(内存)存储空间。但是类中的成员函数却不是这样,他们不占用类对象的任何内存空间。这就是为什么使用sizeof(your class)总是返回该类中的成员变量的字节大小(如果该类中存在虚函数,则还会多出4个字节的虚函数数组指针来)。因为成员函数所要操作的只是类对象中的属性,他们所关心的不是这些属性的值。那么,这些类成员函数又是怎么知道他要去操作哪个类对象中的属性呢?答案就是通过this指针。this指针说白了就是一个指向该类对象在创建之后位于内存中的内存地址。当我们调用类中的成员函数时,编译器会悄悄地将相应类对象的内存地址(也就是this指针)传给我们的类成员函数,有了这个指针,我们就可以在类成员函数中对该类对象中的属性进行访问了。this指针被传入成员函数的方式主要取决于你的类成员函数在声明时所使用的调用约定,如果使用_thiscall调用约定,那么this指针将通过寄存器ECX进行传递,该方式通常是编译器(VC)在缺省情况下使用的调用约定。如果是_stdcall或_cdecl调用约定,this指针将通过栈进行传递,并且this指针将是最后一个被压入栈的参数,虽然我们在声明成员函数时,并没有声明有这个参数。简单说来,调用约定(Calling Convention)主要是用来指定相应的函数在被调用时的参数传递顺序,以及在调用完成后由谁(调用者还是被调用者)来修正ESP寄存器的值(因为调用者向被调用者通过栈来传递参数时,ESP的值会被修改,系统必须要能够保证被调用函数返回后,ESP的值要能够恢复到调用之前的值,这样调用者才能正确的运行下去,对于其他寄存器,编译器通常会自动为我们生成相应的保存与恢复代码,通常是在函数一开始将相关寄存器的值PUSH到栈中,函数返回之前再依次pop出来)。通常对ESP的修正有两种方式,一种是直接使用ADD ESP,4*n,一种是RET 4*n(其中n为调用者向被调用者所传递的参数个数,乘4是因为在栈中的每个参数需要占用4个字节,因为编译器为了提高寻址效率,会将所有参数转换成32位,即即使你传递一个字节,那么同样会导致ESP的值减少4)。通常我们使用的调用约定主要有_stdcall,_cdecl,_thiscall,_fastcall。有关这些调用约定的详细说明,请参见MSDN(节)。这里只简略地描述他们的用途:几乎所有的Windows API均使用_stdcall调用约定,ESP由被调用者函数自行修正,通常在被调用函数返回之前使用RET 4*n的形式。_cdecl调用约定就不一样,它是由调用者来对ESP进行修正,即在被调用函数返回后,调用者采用ADD ESP,4*n的形式进行栈清除,通常你会看到这样的代码:push argument1 push argument2 call _cdecl_function add esp,8另外一个_cdecl不得不说的功能是,使用_cdecl调用约定的函数可以接受可变数量的参数,我们见得最多的_cdecl函数恐怕要算printf了(int _cdecl printf(char*format,.),瞧!是不是很cool啊。因为_cdecl是由调用者来完成栈的清除操作,并且他自己知道自己向被调用函数传递了多少参数,此次他自己也知道该怎样去修正ESP。跟_stdcall比起来,唯一的缺点恐怕就是他生成的可执行代码要比_stdcall稍长些(即在每个CALL之后,都需要添加一条ADD ESP,X的指令)。但跟他提供给我们的灵活性来说,这点缺点又算什么呢?_thiscall主要用于类成员函数的调用约定,在VC中它是成员函数的缺省调用约定。他跟_stdcall一样,由被调用者清除调用栈。唯一不同的恐怕就是他们对于this指针的传递方式了吧!在_stdcall和_cdecl中,this指针通过栈传到类成员函数中,并且被最后一个压入栈。而在_thiscall中,this指针通过ECX进行传递,直接通过寄存器进行参数传递当然会得到更好的运行效率。另外一种,_fastcall,之所以叫fast,是因为使用这种调用约定的函数,调用者会尽可能的将参数通过寄存器的方式进行传递。另外,编译器将为每种调用约定的函数产生不同的命名修饰(即Name-decoration convention),当然这些只是编译器所关心的东西,我们就不要再深入了吧!。对于给定的一个类:class MemberCallDemo public:void _stdcall foo(int a)printf(In MemberCallDemo:foo,a=%dn,a);通常我们会通过下面的方式进行调用该类中的成员方法foo:MemberCallDemo mcd;mcd.foo(9);或者通过函数指针的方式:void(_stdcall MemberCallDemo:*foo_ptr)(int)=&MemberCallDemo:foo;(mcd.*foo_ptr)(9);我总是认为这中使用成员函数指针的调用方式(或是语法)感到很奇怪,不过它毕竟是标准的并且能够为C+编译器认可的调用方式。几乎在所有编译器都不允许将成员函数的地址直接赋给其他类型的变量(如DWORD等,即使使用reinterpret_cast也无济于事)例如:而只能将其赋给给与该成员函数类型声明(包括所使用的调用约定,返回值,函数参数)完全相同的变量。因为成员函数的声明中都有一个隐藏的this指针,该指针会通过ECX或栈的方式传递到成员函数中,为了成员函数被安全调用,编译器禁止此类型的转换也在情理当中。但有时我们为了实现特殊的目的需要将成员函数的地址直接赋给一个DWORD变量(或其他任何能够保存一个指针值的变量,通常只要是一个32位的变量即可),我们可以通过下面两种方法来实现:下面的这几种试图将一个成员函数的地址保存到一个DWORD中都将被编译器认为是语法错误:我总是认为这中使用成员函数指针的调用方式(或是语法)感到很奇怪,不过它毕竟是标准的并且能够为C+编译器认可的调用方式。几乎在所有编译器都不允许将成员函数的地址直接赋给其他类型的变量(如DWORD等,即使使用reinterpret_cast也无济于事)。下面的这几种试图将一个成员函数的地址保存到一个DWORD中都将被编译器认为是语法错误:DWORD dwFooAddrPtr=0;dwFooAddrPtr=(DWORD)&MemberCallDemo:foo;/*Error C2440*/dwFooAddrPtr=reinterpret_cast DWORD(&MemberCallDemo:foo);/*Error C2440*/因为成员函数的声明中都有一个隐藏的this指针,该指针会通过ECX或栈的方式传递到成员函数中,为了成员函数被安全调用,编译器禁止此类型的转换也在情理当中。但有时我们为了实现特殊的目的需要将成员函数的地址直接赋给一个DWORD变量(或其他任何能够保存一个指针值的变量,通常只要是一个32位的变量即可)。我们只能将成员函数的地址赋给给与该成员函数类型声明(包括所使用的调用约定,返回值,函数参数)完全相同的变量(如前面的void(_stdcall MemberCallDemo:*foo_ptr)(int)=&MemberCallDemo:foo)。因为成员函数的声明中都有一个隐藏的this指针,该指针会通过ECX或栈的方式传递到成员函数中,为了成员函数被安全调用,编译器禁止此类型的转换也在情理当中。但有时我们为了实现特殊的目的需要将成员函数的地址直接赋给一个DWORD变量(或其他任何能够保存一个指针值的变量,通常只要是一个32位的变量即可),就像我们即将介绍的情况。通过前面几节的分析我们知道,成员函数的调用和一个普通的非成员函数(如全局函数,或静态成员函数等)唯一不同的是,编译器会在背后悄悄地将类对象的内存地址(即this指针)传到类成员函数中,具体的传递方式以该类成员函数所采用的调用约定而定。所以,是不是只要我们能够手动地将这个this指针传递给一个成员函数(这是应该是一个函数地址),是不是就可以使该成员函数被正确调用呢?答案是肯定的,但是我们迫在眉睫需要解决的是怎样才能得到这个成员函数的地址呢?通常,我们有两种方法可以达到此目的:1。使用内嵌汇编(在VC6及以前的版本中将不能编译通过)DWORD dwFooAddrPtr=0;_asm/*得到MemberCallDemo:foo偏移地址,事实上就是该成员函数的内存地址(起始地址)*/MOV EAX,OFFSET MemberCallDemo:foo MOV DWORD PTRdwFooAddrPtr,EAX这种方法虽然看起来甚是奇怪,但是他却能够解决我们所面临的问题。虽然在目前的应用程序开发中,很少甚至几乎没有人使用汇编语言去开发,但是,往往有时一段小小的汇编代码居然能够解决我们使用其他方法不能解决的问题,这在上面的例子和下面即将介绍的Thunk中大家就会看到他那强有力的问题解决能力。所以说我们不能将汇编仍在一边,我们需要了解她,并且能够在适当的时候使用她。毕竟她始终是一个最漂亮,最具征服力的编程语言。_2。通过使用union来欺骗编译器或使用一种更为巧妙的方法,通过使用一个union数据结构进行转换(Stroustrup在其The C+Programming Language中讲到类似方法),由于在union数据结构中,所有的数据成员均享用同一内存区域,只是我们为这一块内存区域赋予了不同的类型及名称,并且我们修改该结构中的任何变量都会导致其他所有变量的值均被修改。所以我们可以使用这种方法来欺骗编译器,从而让他认为我们所进行的转换是合法的。template class ToType,class FromType ToType union_cast(FromType f)unionFromType _f;ToType _t;ut;ut._f=f;return ut._t;DWORD dwAddrPtr=union_cast DWORD(&YourClass:MemberFunction);怎么样,这样的类型转换是不是很酷啊?就像使用reinterpret_cast和static_cast等之类的转换操作符一样。通过巧妙地使用union的特点轻松逃过编译器的类型安全检查这一关,从而达到我们的数据转换目的。当然,我们通常不会这样做,因为这样毕竟是类型不安全的转换,他只适用于特定的非常规的(函数调用)场合。好,我们现在已经得到了该成员函数的内存地址了,下买面我们通过一个更为奇怪的方式来调用成员函数MemberCallDemo:foo(使用这种方式,该成员函数foo将不能使用缺省的_thiscall调用约定,而必须使用_stdcall或_cdecl):void(_stdcall*fnFooPtr)(void*/*pThis*/,int/*a*/)=(void(_stdcall*)(void*,int)dwFooAddrPtr;fnFooPtr(&mcd,9);执行上面的调用后,屏幕上依然会输出In MemberCallDemo:foo,a=9。这说明我们成功地调用了成员函数foo。当然,使用这种方式使我们完全冲破了C+的封装原则,打坏了正常的调用规则,即使你将foo声明为private函数,我们的调用同样能够成功,因为我们是通过函数地址来进行调用的。你不禁会这样问,在实际开发中谁会这样做呢?没错,我估计也没有人会这样来调用成员函数,除非在一些特定的应用下,比如我们接下来所要做的事情。Thunk!Thunk?这是什么意思?翻开Oxford Advanced Learners Dictionary,Sixth-Edition.查不到!再使用Kingsofts PowerWord 2006,其曰铮,铛,锵?显然是个象声词。顿晕,这跟我们所要描述的简直不挨边啊!不知道人们为什么要把这种技术称之为Thunk。不管了,暂时放置一边吧!通常,我们所编写的程序最终将被编译器的转换成计算机能够识别的指令码(即机器码),比如:C/C+代码等价的汇编代码编译器产生的机器指令=int k1,k2,k;k1=1;mov dword ptrk1,1 C7 45 E0 01 00 00 00 k2=2;mov dword ptrk2,2 C7 45 D4 02 00 00 00 k=k1+k2;mov eax,dword ptrk18B 45 E0 add eax,dword ptrk203 45 D4 mov dword ptrk,eax 89 45 C8最终,CPU执行完指令序列C7 45 E0 01 00 00 00 C7 45 D4 02 00 00 00 8B 45 E0 03 45 D4 89 45 C8后,就完成了上面的简单加法操作。从这里我们似乎能够得到启发,既然CPU只能够认识机器码,那么我们可以直接将机器码送给CPU去执行吗?答案是:当然可以,而且还非常高效!那么,怎么做到这一点呢?-定义一个机器码数组,然后跳转到该数组的起始地址处开始执行:unsigned char machine_code=0xC7,0x45,0xE0,0x01,0x00,0x00,0x00,0xC7,0x45,0xD4,0x02,0x00,0x00,0x00,0x8B,0x45,0xE0,0x03,0x45,0xD4,0x89,0x45,0xC8;void*paddr=machine_code;使用内嵌汇编调用该机器码:_asmMOV EAX,dword ptrpaddr;or mov eax,dword ptr paddr;or mov eax,paddr CALL EAX如果使用C调用该机器码,则为:void(*fn_ptr)(void)=(void(*)(void)paddr;fn_ptr();怎么样?当上面的CALL EAX执行完后,变量k的值同样等于3。但是,当machine_code中的指令执行完后,CPU将无法再回到CALL指令的下一条指令了!为什么啊?是的,因为machine_code中没有返回指令的机器码!要让CPU能够返回到正确的位置,我们必须将返回指令RET的机器码(0xC3)追加到machine_code的末尾,即:unsigned char machine_code=0xC7,0x45,.,0x89,0x45,0xC8,0xC3;。这就是Thunk!一种能让CPU直接执行我们的机器码的技术,也有人称其为自修改代码(Self-Modifying Code)。但这有什么用呢?同样,在通常的开发中,我们不可能通过这么复杂的代码来完成上面的简单加法操作!谁这样做了,那他/她一定是个疯子!_。目前所了解的最有用的也是用得最多的就是使用Thunk来更改栈中的参数,甚至可以是栈中的返回地址,或者向栈中压入额外的参数(就像我们的KWIN那样),从而达到一些特殊目的。当然,在你熟知Thunk的原理后,你可能会想出更多的用途来,当然,如果你想使用Thunk来随意破坏当前线程的栈数据,从而直接导致程序或系统崩溃,那也不是不可能的,只要你喜欢,谁又在乎呢?只要你不把这个程序拿给别人运行就行!通常我们会使用Thunk来截获对指定函数的调用,并在真正调用该函数之前修改调用者传递给他的参数或其他任何你想要做的事情,当我们做完我们想要做的时候,我们再跳转到真正需要被调用的函数中去。既然要跳转,就势必要用到JMP指令。由于在Thunk中的代码必须为机器指令,所以我们必须按照编译器的工作方式将我们所需要Thunk完成的代码转换成机器指令,因此我们需要知道我们在Thunk所用到的指令的机器指令的编码规则(通常,我们在Thunk中不可能做太多事情,了解所需的指令的编码规则也不是件难事)。大家都知道,JMP为无条件转移指令,并且有short、near、far转移,通常编译器会根据目标地址距当前JMP指令的下一条指令之间的距离来决定跳转类型。在生成机器码时,并且编译器会优先考虑short转移(如果目标地址距当前JMP指令的下一条指令的距离在-128和127之间),此时,JMP对应的机器码为0xEB。如果超出这个范围,JMP对应的机器码通常为0xE9。当然,JMP还存在其他类型的跳转,如绝对地址跳转等,相应地也有其他形式的机器码,如0xFF,0xEA。我们常用到的只有0xEB和0xE9两种形式。另外,需要注意的是,在机器码中,JMP指令后紧跟的是一个目标地址到该条JMP指令的下一条指令之间的距离(当然,以字节为单位),所以,如果我们在Thunk中需要用到JMP指令,我们就必须手动计算该距离(这也是编译器所需要做的一件事)。如果你已经很了解JMP指令的细节,那么你应该知道了下面Thunk的将是什么样的结果了吧:unsigned char machine_code=0xEB,0xFE;啊,没错,这将是一个死循环。这一定很有趣吧!事实上他跟JMP$是等价的。由于这里的机器码是0xEB,它告诉CPU这是一个short跳转,并且,0xFE的最高位为1(即负数),所以CPU直到它是一个向后跳转的JMP指令。由于向后跳转的,所以此时该JMP所能跳转到的范围为-128至-1(即0x80至0xFF),但是由于这时的JMP指令为2个字节,所以向后跳转(从该条指令的下一条指令开始)2个字节后,就又回到了该条JMP指令的开始位置。当发生Short JMP指令时,其所能跳转的范围如下:偏移量机器码=(-128)0x80?(-127)0x81?(-3)0xFD?(-2)0xFE EB-Short JMP指令(-1)0xFF XX-XX为跳转偏移量,其取值范围可为0x80-0x7F(0)0x00?-JMP下一条指令的开始位置(+1)0x01?(+2)0x02?(+125)0x7D?(+126)0x7E?(+127)0x7F?好,让我们在来看一个例子,来说明Thunk到底是怎样修改栈中的参数的:void foo(int a)printf(In foo,a=%dn,a);unsigned char code9;*(DWORD*)&code0)=0x 042444FF;/*inc dword ptresp+4*/code4=0xe9;/*JMP*/*(DWORD*)&code5)=(DWORD)&foo-(DWORD)&code0-9;/*跳转偏移量*/void(*pf)(int/*a*/)=(void(*)(int)&code0;pf(6);当执行完pf(6)调用后,就会得到下面的输出:In foo,a=7(明明传入的是6,为什么到了foo中就变成了7了呢?)。怎么样?我们在Thunk中通过强制CPU执行机器码0xFF,0x44,0x24,0x04来将栈中的传入参数a(位于ESP+4处)增加1,从而修改了调用者传递过来的参数。在执行完INC DWORD PTRESP+4后,再通过一个跳转指令跳转到真正的函数入口处。当然,我们同样不可能在实际的开发中使用这种方法进行函数调用,之所以这样做是为了能够更加容易的弄清楚Thunk到底是怎么工作的!好了,写了这么久,似乎我们到这里才真正进入我们的正题。上面几节所描述的都是这一节所需的基本知识,有了以上知识,我们就能够很容易的实现我们的最终目的-让Windows来直接调用我们的类消息处理成员函数,在这里无须使用任何静态成员函数或全局函数,所有的事情都将由我们定义的类成员函数来完成。由于Windows中所有的消息处理均为回调函数,即它们是由操作系统在特定的消息发生时被系统调用的函数,我们需要做的仅仅是定义该消息函数,并将该消息函数的函数地址告诉Windows。既然我们能够使用在通过其他途径调用类中的成员函数中所描述的方法得到类成员函数(消息处理函数)的地址,那么,我们能够直接将该成员函数地址作为一个回调函数的地址传给操作系统吗?很显然,这是不可能的。但是为什么呢?我想聪明的你已经猜到,因为我们的成员函数需要类对象的this指针去访问类对象中的属性,但是Windows是无法将相应的类对象的this指针传给我们的成员函数的!这就是我们所面临的问题的关键所在!如果我们能够解决这个类对象的this指针传递问题,即将类对象的this指针手动传递到我们的类成员函数中,那么我们的问题岂不是就解决了吗?没错!Thunk可以为们解决这个难题!这里Thunk需要解决的是将消息处理函数所在的类的实例的this指针传递到消息处理函数中,从前面的描述我们已经知道,this指针的传递有两种方式,一种是通过ECX寄存器进行传递,一种是使用栈进行传递。这是一种最简单的方式,它只需我们简单地在Thunk中执行下面的指令即可:LEA ECX,this pointer JMP member function-based message handler使用这种方式传递this指针时,类中的消息处理函数必须使用_thiscall调用约定!在关于调用约定与this指针的传递中我们对调用约定有较为详细的讨论。这是一种稍复杂的方式,使用栈传递this指针时必须确保类中的消息处理函数使用_stdcall调用约定,这跟通常的消息处理函数(静态成员函数或全局函数)使用的是同一种条用约定,唯一不同的是现在我们使用的是类成员函数(非静态)。之所以说他稍复杂,是因为我们要在Thunk中要做稍多的工作。前面我们已经说过,我们已经将我们定义的Thunk的地址作为消息处理回调函数地址传给了Windows,那么,当有消息需要处理时,Windows就会调用我们的消息处理函数,不过这时它调用的是Thunk中的代码,并不是真正的我们在类中定义的消息处理函数。这时,要将this指针送入当前栈中可不是件容易的事情。让我们来看看Windows在调用我们的Thunk代码时的栈的参数内容:this指针被压入栈之前this指针被压入栈之后|-|-|LPARAM|LPARAM|-|-|WPARAM|WPARAM|-|-|UINT(msg)|UINT(msg)|-|-|HWND|HWND|-|-|(Return Addr)|-ESP|this pointer|-New item inserted by this thunk code|-|-|:.:|(Return Addr)|-ESP|-|图1图2从图1可以看出,为了将this指针送入栈中,我们可不能简单地使用PUSHthis pointer的方式将this指针压入栈中!但是为什么呢?想想看,如果直接将this指针压入栈中,那么原来的返回地址将不能再起效。也就是说我们将不能在我们的消息处理函数执行结束后返回到正确的地方,这势必会导致系统的崩溃。另一方面,我们的成员函数要求this指针必须是最后一个被送入栈的参数,所以,我们必须将this指针插入到HWND参数和返回地址(Return Addr)之间。如图2所示。所以,在这种情况下,我们须在Thunk中完成以下工作:PUSH DWORD PTRESP;保存(复制)返回地址到当前栈中MOV DWORD PTRESP+4,pThis;将this指针送入栈中,即原来的返回地址处JMP member function-based message handler;跳转至目标消息处理函数(类成员函数)好了,有了以上知识后,现在就只剩下我们的KWIN包的开发了,当然,如果你掌握以上知识后,你可以运用这些知识,甚至找出新的方法来实现你自己的消息处理包。我想,那一定时间非常令人激动的事情!如果你想到更好的方法,千万别忘了告诉我一声哦。首先来看看我们在KWIN中需要使用到的一个比较重要的宏:#define _DO_DEFAULT(LRESULT)-2#define _USE_THISCALL_CALLINGCONVENTION#ifdef _USE_THISCALL_CALLINGCONVENTION#define THUNK_CODE_LENGTH 10/*For _thiscall calling convention ONLY*/#define KCALLBACK _thiscall#else#define THUNK_CODE_LENGTH 16/*For _stdcall or _cdecl calling convention ONLY*/#define KCALLBACK _stdcall#endif在KWIN中同时实现了_thiscall和_stdcall两种调用约定,如果定义了_USE_THISCALL_CALLINGCONVENTION宏,那么就使用_thiscall调用约定,否则将使用_stdcall调用约定。宏KCALLBACK在定义了_USE_THISCALL_CALLINGCONVENTION宏后将被替换成_thiscall,否则为_stdcall。THUNK_CODE_LENGTH定义了在不同的调用约定下所需的机器指令码的长度,如果使用_thiscall,我们只需要10个字节的机器指令码,而在_stdcall下,我们需要使用16字节的机器指令码。我们将实现对话框和一般窗口程序的消息处理函数进行封装的包(KWIN),我们力求使用KWIN能为我们的程序带来更好的灵活性和结构良好性,就像我们在本文开始时向大家展示的一小部分代码那样。首先我们将定义一个对话框和窗口程序都需要的数据结构_K_THUNKED_DATA,该结构封装了所有所需的Thunk代码(机器指令码)。整个KWIN的结构大致如下:图3在_K_THUNKED_DATA中有一个非常重要的函数-Init,它的原型如下:void Init(DWORD_PTR pThis,/*消息处理类对象的内存地址(this指针)*/DWORD_PTR dwProcPtr/*消息处理函数(类成员函数)的地址*/DWORD dwDistance=(DWORD)dwProcPtr-(DWORD)&pThunkCode0-THUNK_CODE_LENGTH;#ifdef _USE_THISCALL_CALLINGCONVENTION/*Encoded machine instruction Equivalent assembly languate notation-B9?mov ecx,pThis;Load ecx with this pointer E9?jmp dwProcPtr;Jump to target message handler*/pThunkCode0=0xB9;pThunkCode5=0xE9;*(DWORD*)&pThunkCode1)=(DWORD)pThis;*(DWORD*)&pThunkCode6)=dwDistance;#else/*Encoded machine instruction Equivalent assembly languate notation-FF 34 24 push dword ptresp;Save(or duplicate)the Return Addr into stack C7 44 24 04?mov dword ptresp+4,pThis;Overwite the old Return Addr withthis pointerE9?jmp dwProcPtr;Jump to target message
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2026届浙江省温州市名校英语九年级第一学期期末监测试题含解析
- 2026届江苏省淮安市清江浦区江浦中学化学九年级第一学期期中质量跟踪监视模拟试题含解析
- 湖北省武汉市武昌区省水二中学2026届九年级英语第一学期期末检测模拟试题含解析
- 水力学考试题及答案
- 中职英语b级考试题目及答案
- 2025年设计史考试题目及答案
- 中医推拿考试题目及答案
- 成都高新区社区卫生服务中心2025年公开招聘工作人员(94人)考试参考题库及答案解析
- 无锡核酸采样培训课件
- 2026中国银行审计部广东分部秋季校园招聘11人考试参考题库及答案解析
- 高职院校教师职业发展规划指南
- 2025重庆市专业应急救援总队应急救援人员招聘28人考试参考题库及答案解析
- 黑龙江省龙东地区2025届中考数学试卷(含解析)
- 2025-2026学年人教版(2024)小学美术二年级上册(全册)教学设计(附目录P144)
- 2025高考地理试题分类汇编:地球上的水含解析
- 2025年机器人面试题及答案解析
- 2026届高考作文写作素材:《感动中国》2024年度十大人物素材及其运用
- 2025年重庆八中宏帆中学小升初自主招生数学试题(含答案详解)
- 2025年度江苏省档案管理及资料员基础试题库和答案
- 口腔咨询顾问入门知识培训课件
- 公司金融学 课件 第三章:货币的时间价值
评论
0/150
提交评论