本文基本只讨论原理,具体实现请参见后续文章《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. Unable to execute dex: java.nio.BufferOverflowException. Check the Eclipse log for stack trace.(转)

    1.程序运行后异常显示: 解决方案:在项目上点击右键->properties->Java Build Path, remove掉Android Dependences即可

  2. 图片预览(base64和blob:图片链接)和ajax上传、下载(带进度提示)

    直接上代码 html和js <!DOCTYPE html> <html> <head> <meta name="viewport" con ...

  3. dotnet与各种数据库类型对应关系

    Data Provider Parameter format sqlclient @parametername oledb ?    mark odbc ?   mark oracleclient : ...

  4. socket.io实现

    后台代码 index_server.js var app = require('http').createServer(handler)//创建服务器app , io = require('socke ...

  5. 在IE6/7/8下识别html5标签

    识别html5标签: html5添加了许多语义化的标签,比如<nav></nav>,<aside></aside>,<article>< ...

  6. java jdbc 连接mysql 数据库

    JDBC连接MySQL 加载及注册JDBC驱动程序 Class.forName("com.mysql.jdbc.Driver"); Class.forName("com. ...

  7. python函数参数的pack与unpack

    python函数参数的pack与unpack 上周在使用django做开发的时候用到了mixin(关于mixin我还要写一个博客专门讨论一下,现在请参见这里),其中又涉及到了一个关于函数参数打包(pa ...

  8. overload and overwrite in C++

    1. overload : don't using it in different scope. it will hidden the one in base or global scope. 2. ...

  9. hdu1405 第六周J题(质因数分解)

    J - 数论,质因数分解 Time Limit:1000MS     Memory Limit:32768KB     64bit IO Format:%I64d & %I64u   Desc ...

  10. Linux 下安装Python框架django建立与mysql的连接

    0.基本环境说明: a. Ubuntu 14.04 64bit b. python 2.7.6 c. django 1.8 d. django-registration e. django-widge ...