构造一个通用的回调Thunk.(把回调函数指向对象的方法的办法)
2008-01-29 14:25
357 查看
构造一个通用的回调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.
最近又看到了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.
相关文章推荐
- 构造一个通用的回调Thunk.(把回调函数指向对象的方法的办法)
- 根据指定方法的参数去构造一个新的对象的拷贝并将他返回
- 【Java】【TIJ】实现一个类:不允许用构造方法来直接创建类实例(对象)
- 获取一个XMLHttpRequest 对象的通用方法
- 封装回调函数——为对象方法(Object Method,参数中带this指针的函数) 构造 普通函数(参数中无this指针的函数)形式 的入口
- Lua中让回调函数支持回调对象方法的解决方法
- 为什么Java反射对象必须有一个无参数的构造方法?
- 封装回调函数——为对象方法(Object Method,参数中带this指针的函数) 构造 普通函数(参数中无this指针的函数)形式 的入口
- 针对网上很多抱怨的言论,写了一个EF中update对象时,通用的遍历赋值方法,以供参考
- java创建一个子类对象是会调用父类的构造方法会不会创建父类
- php实例化一个对象时通过构造方法传参
- 继承与合成基本概念 继承:可以基于已经存在的类构造一个新类。继承已经存在的类就可以复用这些类的方法和域。在此基础上,可以添加新的方法和域,从而扩充了类的功能。 合成:在新类里创建原有的对象称为合成。
- 尝试创建一个父类和子类,分别创建一个构造方法,然后向父类和子类添加成员变量和方法,并总结构造子类对象时的顺序。
- Java类 属性 构造方法 方法 对象。+一个模拟银行存取款^_^
- 回调函数到对象方法Thunk技术(转载自http://blog.csdn.net/wr960204/archive/2008/01/29/2071480.aspx)
- (转)逐步为对象集合构建一个通用的按指定属性排序的方法
- new 一个没有定义构造方法的子类对象,会调用父类的默认构造方法
- 创建一个教师类(Teacher)属性有 姓名(name) 年龄(age) 性别(sex) 教师编号(tno) 2.提供本类的无参和全参构造方法 3.编写教师上课的办法teach() 编写静态方
- JavaSE8基础 File 通过构造方法可以指向一个不存在的文件
- 常量,字段,构造方法 调试 ms 源代码 一个C#二维码图片识别的Demo 近期ASP.NET问题汇总及对应的解决办法 c# chart控件柱状图,改变柱子宽度 使用C#创建Windows服务 C#服务端判断客户端socket是否已断开的方法 线程 线程池 Task .NET 单元测试的利剑——模拟框架Moq