本文基本只讨论原理,具体实现请参见后续文章《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寄存器返回值。参数压栈顺序为从右到左。 那么对于窗口过程的定义

  1. 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代码来说,类实例指针就是个立即数(常数)。 那么基本指令应该是

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

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

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

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

  1. struct wndproc_thunk;
  2. struct window
  3. {
  4. HWND _handle;
  5. wndproc_thunk* _thunk;
  6. HRESULT WINAPI static static_window_proc(HWND, UINT, WPARAM, LPARAM);
  7. HRESULT WINAPI window_proc(UINT, WPARAM, LPARAM);
  8. };
  9.  
  10. #pragma pack(push,1)
  11. struct wndproc_thunk
  12. {
  13. DWORD mov; // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)
  14. DWORD thiz; //
  15. BYTE jmp; // jmp WndProc
  16. DWORD relproc; // relative jmp
  17. };
  18. #pragma pack(pop)
  19.  
  20. window win;
  21. win._thunk = alloc_wndproc_thunk();
  22. win._thunk->mov = 0x042444C7; // mov dword ptr [esp+0x4],
  23. win._thunk->thiz = &win; // thiz
  24. win._thunk->jmp = 0xe9; // jmp
  25. win._thunk->relproc = window::static_window_proc - (win._thunk + ); // relproc
  26. FlushInstructionCache(GetCurrentProcess(), win._thunk, sizeof(wndproc_thunk));
  27. 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. lvs+heartbeat搭建负载均衡高可用集群

    [172.25.48.1]vm1.example.com [172.25.48.4]vm4.example.com 集群依赖软件:

  2. NPOI从数据库中导出数据到Excel

    首先要添加NPOI.dll程序集 https://yunpan.cn/cMeSTELJSXmJJ  访问密码 8d83 把里面的程序集都添加到引用里 下面的代码是从数据库导出到Excel { //pa ...

  3. 2015 Multi-University Training Contest 5

    #include <iostream> //1002 #include<set> #include<stdio.h> using namespace std; co ...

  4. Python 文件的IO

    对文件的操作 #coding=utf-8 #!user/bin/python import os #基本操作和写入文件 fo = open("test2.py",'wb') pri ...

  5. 【javascript模式】Chapter2: 基本 技巧

    1 尽量少用全局变量,最好一个应用程式只有一个全局变量  隐含全局变量(不使用var声明)与明确定义的全局变量区别:  (1)使用var创建的全局变量(在函数外部声明)不能用delete删除  (2) ...

  6. JavaScript 目标装配式编程(Target Assemble Programming)

    TAP概述 脚本中一切皆对象,若还以传统模式思考编程模式,那简直是对不起脚本解释器的强大支持:我们应该以最接近人类操作方式的来表达人的意图. 更接近工作实践的方式,比如游戏中,一个人物一个角色,人物的 ...

  7. linux c下几种定时器实现

    1.alarm n秒后触发一次,不是循环的2.setitimer 可以发出3种信号给自己,3.timerfd 这个接口基于文件描述符,通过文件描述符类似epoll那种的可读事件进行超时通知,能够被用于 ...

  8. sql -实验二

    8. 统计各部门下工资大于2000的雇员的平均工资. select avg(sal)from empwhere sal>2000;

  9. 破解Inode客户端使用笔记本共享WIFI

    由于住在学校的公寓里面,所以使用的是校园网,但是校园网限制了无线的使用,强制所有网络用户使用INode网络客户端,这个客户端不但很丑很难看,而且每天联网十分费劲,费了半天的力气终于联上网了,可是一眨眼 ...

  10. 深入了解使用egret.WebSocket

    概念 本教程不讲解TCP/IP协议,Socket属于哪层,消息包体怎么设计等,主讲 egret.WebSocket 使用示例 与 protobuf 使用示例. 在使用egret.WebSocket之前 ...