本文基本只讨论原理,具体实现请参见后续文章《C++ 通过Thunk在WNDPROC中访问this指针实现细节

当注册窗口类时,WNDCLASSEX结构的lpfnWndProc成员应设置为窗口过程函数的地址,这是一个C风格的函数指针,所以我们只能使用全局或静态函数的地址,这在我们将窗口封装为C++类时会很麻烦,因为我们无法在一个全局或静态的WindowProc函数中直接访问类实例,这就需要一些手段了(MS的API设计着实不怎么样)

  第一种方案,建立一个HWND到C++类实例的映射表,在WindowProc中通过这个映射表从HWND得到C++类实例,由于可能有多线程安全问题,在访问这个映射表时可能涉及到线程同步,再加上可能应用程序要处理的消息频率十分高,从而带来性能问题(一般情况还是可以接受的)。
  另一种方案是通过SetWindowLongPtr/GWLP_USERDATA将类实例指针存放在窗口的用户数据字段中,这样就可以在WindowProc中通过调用GetWindowLongPtr/GWLP_USERDATA来获取类实例指针了,缺点就是当别人使用你的C++类时可能会不留神把你存放的this指针覆盖,从而导致不可预知的后果;此外每一条window消息都要调用GetWindowLongPtr这个系统API也是一点点额外开销。
  第三种也就是本文要讨论的方案,是传说中的Thunk方案。这也是MFC/ATL所使用的方现。Thunk在这里是指一小段代码,这段代码无法用C/C++来表示(因为是动态代码),只能用机器码写(汇编都不好使),这也就造成本方案在跨平台时有点小麻烦,好在Windows本身也支持不了几种CPU。这里仅以x86体系来讨论。

  首先说下x86下__stdcall调用约定。 Windows API要求窗口过程必须使用__stdcall调用约定。 该约定通过栈来传递参数,通过eax寄存器返回值。参数压栈顺序为从右到左。 那么对于窗口过程的定义

LRESULT (CALLBACK *WNDPROC)(HWND,UINT,WPARAM,LPARAM);

来看,当系统调用我们指定的窗口过程时,从右向左依次将LPARAM, WPARAM, UINT, HWND压入栈中,然后使用call指令进入窗口过程。 THUNK的目标就是在这个时候将栈上的HWND参数替换为C++类实例指针。看下此时的栈结构先

栈底
......
......
栈顶 + 7
栈顶 + 6
栈顶 + 5
栈顶 + 4    4-7 原本存放着HWND参数,在执行完Thunk后,其值为类实例地址
-------------------------------------------
栈顶 + 3    0-3 存放着窗口过程的返回地址,
栈顶 + 2    WindowProc里在return之后会返回到该地址继续运行
栈顶 + 1
栈顶 + 0

因为THUNK代码在运行时生成,此时C++类实例的地址已经确定,那么对于THUNK代码来说,类实例指针就是个立即数(常数)。 那么基本指令应该是

mov mov dword ptr [esp+0x4], $class_instance
jmp $real_window_proc

其中 $class_instance 是我们要填入的C++类实际的指针, 而$real_window_proc是我们真正的windowproc的地址,但该windowproc第一个参数不是HWND,而是C++类指针,也就是该函数应该类似于:

LRESULT CALLBACK cpp_window_proc(cpp_window_class* thiz, UINT msg, WPARAM wParam, LPARAM lParam) {
thiz->window_proc(msg, wParam, lParam);
/* 实际上thiscall正好是第一个(隐形)参数为this指针,
* 所以这里也可以直接把 cpp_window_class::window_proc的地址作为$real_window_proc的值
* 但那样对于使用虚函数的情况有些复杂, 所以最好还是用静态或全局函数转一下。
*/
}

以下是一个实现这个机制的伪代码片段

struct wndproc_thunk;
struct window
{
HWND _handle;
wndproc_thunk* _thunk;
HRESULT WINAPI static static_window_proc(HWND, UINT, WPARAM, LPARAM);
HRESULT WINAPI window_proc(UINT, WPARAM, LPARAM);
}; #pragma pack(push,1)
struct wndproc_thunk
{
DWORD mov; // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)
DWORD thiz; //
BYTE jmp; // jmp WndProc
DWORD relproc; // relative jmp
};
#pragma pack(pop) window win;
win._thunk = alloc_wndproc_thunk();
win._thunk->mov = 0x042444C7; // mov dword ptr [esp+0x4],
win._thunk->thiz = &win; // thiz
win._thunk->jmp = 0xe9; // jmp
win._thunk->relproc = window::static_window_proc - (win._thunk + ); // relproc
FlushInstructionCache(GetCurrentProcess(), win._thunk, sizeof(wndproc_thunk));
SetWindowLongPtr(win._handle, GWLP_WNDPROC, (LONG_PTR) win._thunk);

最后补充一句,因为新版Windows及最新的Server Packs都加入了数据执行保护(DEP)功能,因此如果直接在堆或栈上分配空间构造thunk的话,因为堆和栈所在内存都被默认标记为不可执行,从而导致系统异常。这里就需要VirtualAlloc方法动态为thunk分配内在,并使用PAGE_EXECUTE_READWRITE标志,记得最后使用VirtualFree释放该内存。

C++ 通过Thunk在WNDPROC中访问this指针的更多相关文章

  1. C++ 通过Thunk在WNDPROC中访问this指针实现细节

    本文代码使用了一些C++11特性,需要编译器支持.本文仅讨论x86_64平台的相关实现,x86平台理论上只需修改 thunk 相关机器码即可. THUNK的原理参见之前的一篇博文<C++ 通过T ...

  2. Hadoop3 在eclipse中访问hadoop并运行WordCount实例

    前言:       毕业两年了,之前的工作一直没有接触过大数据的东西,对hadoop等比较陌生,所以最近开始学习了.对于我这样第一次学的人,过程还是充满了很多疑惑和不解的,不过我采取的策略是还是先让环 ...

  3. 错误: 从内部类中访问本 地变量vvv; 需要被声明为最终类型

    从github 下载了源码, 进行编译, 出现了下面的错误 E:\downloads\ff\elasticsearch-master\elasticsearch-master>GRADLE :b ...

  4. phpmyadmin中访问时出现2002 无法登录 MySQL 服务器

    phpmyadmin中访问时出现2002 无法登录 MySQL 服务器! 解决方法如下: 修改phpmyadmin目录中libraries文件夹下的config.default.php文件 $cfg[ ...

  5. nginx日志中访问最多的100个ip及访问次数

    nginx日志中访问最多的100个ip及访问次数 awk '{print $1}' /opt/software/nginx/logs/access.log| sort | uniq -c | sort ...

  6. Fastreport使用经验(转)在Delphi程序中访问报表对象

    Fastreport使用经验(转) 在Delphi程序中访问报表对象 最基本的方法就是frxReport1.FindObject. 然后把返回的对象强制转换成它的类型,当然,在报表中必须真的有这么个东 ...

  7. 如何在外网中访问自己在另一个局域网中的某个机器(SSH为例)

    UBUNTU 14.04 LTS 为例 如何在外网中访问自己在另一个局域网中的某个机器(SSH为例) 2013-05-01 16:02 2693人阅读 评论(0) 收藏 举报 情景描述: 计算机C1放 ...

  8. 在Asp.net MVC中访问静态页面

    有时候由于一些特殊的需要,我们需要在MVC中访问HTML页面,假如您将这个页面放在Views中的话,去访问将会收到一个404,但是放在Views外面的目录则不受此限制. 那么我们就来解决View里面的 ...

  9. 九、在动作类中访问ServletAPI

    九.在动作类中访问ServletAPI .方式一:(简单,推荐使用)ServletActionContext public String execute() throws Exception {    ...

随机推荐

  1. navicat导入mysql数据库sql时报错或数据不完全问题

    错误详情:[Err] [Imp] 2006 - MySQL server has gone away 或无提示错误,但是导入数据明显缺少字段和数据 找到服务器上的MYSQL安装目录下的my.ini文件 ...

  2. python小练习,打出1-100之间的所有偶数,设计一个函数,在桌面上创建10个文件,并以数字命名,复利计算函数

    练习一:打出1-100之间的所有偶数 def even_print(): for i in range(1,101): if i % 2 == 0: print (i) even_print() #列 ...

  3. FileWriter类的flush方法的作用

    FileWriter类的flush方法的作用 每次io都会影响性能,将需要写入的内容,放入缓冲区中,然后调用flush方法,将缓冲区内容写入文件中.

  4. Wpf ListBox数据绑定实例1--绑定字典集合

    1.使用ListBox绑定Dictionary字典数据 ListBox常用事件SelectionChanged private void bindListBox() { Dictionary<s ...

  5. JavaScript typeof, null, 和 undefined

    typeof 操作符 你可以使用 typeof 操作符来检测变量的数据类型. 实例 typeof "John"                // 返回 string typeof ...

  6. C# .net 如何根据访问者IP获取所在地区

    第一步:在根目录添加新项(类),新建一个类文件,把以下文件粘贴到该类文件下:   using System; using System.Collections.Generic; using Syste ...

  7. sql server 利用首字母拼音排序和笔画排序的语句

    --按笔画排序 select * from Student order by Sname COLLATE Chinese_PRC_Stroke_CS_AS_KS_WS --按字母拼音排序 select ...

  8. 封装兼容性添加、删除事件的函数 addEventListener与removeEventListener

    var Event = { addHandler: function (oElement, sEvent, fnHandler) { oElement.addEventListener ? oElem ...

  9. Linux命令——创建删除文件

    创建文件夹 mkdir filename 进入目录文件 cd filename 返回上一级目录 cd ..返回多级目录   cd ../../.. (../表示一级) 创建文件 touch filen ...

  10. 浅谈intval()函数用法

    <? } } 总结:intval()函数功能1.参数一定是数字否则会报错,2.如果是数字那一定是整数,如果有小点,那会省略掉,3,强调参数可以有“-”值.4.参数第一位不应为0开头,不然会自动转 ...