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