回調函數到對象方法Thunk技術(轉載自http://blog.csdn.net/wr960204/archive/2008/01/29/2071480.aspx)

構造一個通用的回調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.    

發佈了21 篇原創文章 · 獲贊 4 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章