• 构造一个通用的回调Thunk.(把回调函数指向对象的方法的办法)


    构造一个通用的回调Thunk.(把回调函数指向对象的方法)
    最近又看到了VCL代码中的MakeObjectInstance函数,实际上是一段WndProc的Thunk代码.再一次感叹VCL设计之精巧,效率之高.
    不喜欢MFC的消息映射方式,MFC的消息映射虽然好理解,但是是采用查表方式效率实在是太低了.VCL的MakeObjectInstance可以
    说是VCL Windows系统的灵魂所在,效率极高.
    不禁想可不可以实现一个通用的回调函数Thunk呢,可以把所有回调函数都变成对象的方法.
    但是MakeObjectInstance实际上是为WndProc特化的.
    分析一下回调函数
    1.回调函数不过是一个函数指针.
    2.尽管回调函数可以是任何调用约定,但绝大多数Win32API的回调函数都是stdcall.(VC中WINAPI,PASCAL,CALLBACK不过是stdcall的宏).
      我们完全可以不考虑其他的调用约定,只考虑stdcall的.
    想一下,如果我们对象的方法也是一个stdcall调用约定的方法,那么和回调函数还差什么呢?
    只差一个参数,第一个参数对象实例的指针,在Delphi,Pascal,Ada中叫Self,C++,java,C#中叫this.VB中叫ME.
    那么我们只要塞给它这个对象的地址不就行了吗.好在stdcall约定参数是由右向左传递的,也就是说第一个参数是最后传递的,又由于stdccall约定
    参数全部是由栈传递的.所以我们只要把对象指针直接压入栈中就行了.
    但别忽略了一点,
    call指令相当于
         Push 返回地址
         Jmp  函数
    ret指令相当于
         pop  返回地址
         Jmp  返回地址      
    也就是说实际上在调用函数的时候栈顶保留的是返回地址,如果我们直接压入实例指针的话原来,当跳到函数体中,函数会把返回地址当Self,而Self则
    会被当成返回地址,具体会有什么样的后果大家自己去想像一下
    所以我们做的事情就是弹出返回地址,压入实例地址,压入返回地址,跳到对象方法去执行.
    实际上我们就是要构造这样一段代码当回调用,这段代码插入对象实例参数到第一个参数,然后跳到对象方法:
         pop    eax            //弹出返回地址到eax
         push   对象实例       //压入对象实例
         push   eax            //压入返回地址
         jmp    对应的对象方法 //跳转到相应的对象方法
    具体实现如下     
         
    //构造出一段Thunk代码
    //构造出一段Thunk代码
    Function CreateThunk(Obj : TObject; CallBackProc: Pointer):Pointer;
    const
      PageSize = 4096;
      SizeOfJmpCode = 5;
    type
      TCode = packed record
        Int3: Byte;          //想调试的的时候填Int 3($CC),不想调试的时候填nop($90)
        PopEAX : Byte;       //把返回地址从栈中弹出
        Push: Byte;          //压栈指令
        AddrOfSelf: TObject; //压入Self地址,把Self作为第一个参数
        PushEAX : Byte;      //重新压入返回地址
        Jmp: Byte;           //相对跳转指令
        AddrOfJmp: Cardinal; //要跳转到的地址,
      end;
    var
      LCode : ^TCode;
    begin
      //分配一段可以执行,可读写的内存
      Result := VirtualAlloc(nil, PageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
      LCode := Result;
      LCode^.Int3 := $90; //nop
      //LCode^.Int3 := $CC; //Int 3
      LCode^.PopEAX := $58;
      LCode^.Push := $68;
      LCode^.AddrOfSelf := Obj;
      LCode^.PushEAX := $50;
      LCode^.Jmp := $E9;
      LCode^.AddrOfJmp := DWORD(CallBackProc) - (DWORD(@LCode^.Jmp) + SizeOfJmpCode);//计算相对地址
    end;

    //销毁thunk代码
    procedure ReleaseThunk(Thunk: Pointer);
    begin
      VirtualFree(Thunk, 0, MEM_RELEASE);
    end;  

    任何Stdcall调用约定的回调都可以用这个Thunk,只要你构造出一个参数一样的对象方法.
    具体举个例子:如SetTimer这个API最后一个参数就是一个回调函数.我们可以拿他试试.
    unit Unit1;

    interface

    uses
      Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
      Dialogs, StdCtrls;

    type
      TForm1 = class(TForm)
        procedure FormCreate(Sender: TObject);
        procedure FormDestroy(Sender: TObject);
      private
        FThunk : Pointer; //Thunk代码的指针
        FTimerId : Cardinal;
      public
        //构造一个和SetTimer回调参数一样的方法,就等着被调用吧
        procedure TimeProc(hwnd: HWND; uMsg: UINT; var idEvent: UINT; dwTime: DWORD); stdcall;
      end;

    var
      Form1             : TForm1;

    implementation

    {$R *.dfm}

    //构造出一段Thunk代码
    Function CreateThunk(Obj : TObject; CallBackProc: Pointer):Pointer;
    const
      PageSize = 4096;
      SizeOfJmpCode = 5;
    type
      TCode = packed record
        Int3: Byte;          //想调试的的时候填Int 3($CC),不想调试的时候填nop($90)
        PopEAX : Byte;       //把返回地址从栈中弹出
        Push: Byte;          //压栈指令
        AddrOfSelf: TObject; //压入Self地址,把Self作为第一个参数
        PushEAX : Byte;      //重新压入返回地址
        Jmp: Byte;           //相对跳转指令
        AddrOfJmp: Cardinal; //要跳转到的地址,
      end;
    var
      LCode : ^TCode;
    begin
      //分配一段可以执行,可读写的内存
      Result := VirtualAlloc(nil, PageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
      LCode := Result;
      LCode^.Int3 := $90; //nop
      //LCode^.Int3 := $CC; //Int 3
      LCode^.PopEAX := $58;
      LCode^.Push := $68;
      LCode^.AddrOfSelf := Obj;
      LCode^.PushEAX := $50;
      LCode^.Jmp := $E9;
      LCode^.AddrOfJmp := DWORD(CallBackProc) - (DWORD(@LCode^.Jmp) + SizeOfJmpCode);//计算相对地址
    end;

    //销毁thunk代码
    procedure ReleaseThunk(Thunk: Pointer);
    begin
      VirtualFree(Thunk, 0, MEM_RELEASE);
    end;


    procedure TForm1.FormCreate(Sender: TObject);
    begin
      //构造Thunk
      FThunk := CreateThunk(Self, @TForm1.TimeProc);
      //把Thunk当作回调函数传递给SetTimer,1000毫秒(1秒)被调用一次
      FTimerId := SetTimer(0, 0, 1000, FThunk);
    end;

    procedure TForm1.FormDestroy(Sender: TObject);
    begin
      //停止Timer
      KillTimer(0, FTimerId);
      //释放Thunk
      ReleaseThunk(FThunk);
    end;

    procedure TForm1.TimeProc(hwnd: HWND; uMsg: UINT; var idEvent: UINT;
      dwTime: DWORD);
    begin
      Caption := Format('我被调用了,GetTickCount=%d',[dwTime]);
    end;

    end.    

  • 相关阅读:
    MVC之Ajax异步操作
    MVCHtmlHelper使用
    Xamarin.Forms初始
    .NET CORE2.0后台管理系统(一)配置API
    DDD领域驱动之干货(四)补充篇!
    基于官方驱动封装mongodb
    webApi签名验证
    在.Net下使用redis基于StackExchange.Redis
    DDD领域驱动之干货(三)完结篇!
    DDD领域驱动之干货(二)
  • 原文地址:https://www.cnblogs.com/ljl_falcon/p/2420914.html
Copyright © 2020-2023  润新知