C++ 通过Thunk在WNDPROC中访问this指针
本文基本只讨论原理,具体实现请参见后续文章《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指针的更多相关文章
- C++ 通过Thunk在WNDPROC中访问this指针实现细节
本文代码使用了一些C++11特性,需要编译器支持.本文仅讨论x86_64平台的相关实现,x86平台理论上只需修改 thunk 相关机器码即可. THUNK的原理参见之前的一篇博文<C++ 通过T ...
- Hadoop3 在eclipse中访问hadoop并运行WordCount实例
前言: 毕业两年了,之前的工作一直没有接触过大数据的东西,对hadoop等比较陌生,所以最近开始学习了.对于我这样第一次学的人,过程还是充满了很多疑惑和不解的,不过我采取的策略是还是先让环 ...
- 错误: 从内部类中访问本 地变量vvv; 需要被声明为最终类型
从github 下载了源码, 进行编译, 出现了下面的错误 E:\downloads\ff\elasticsearch-master\elasticsearch-master>GRADLE :b ...
- phpmyadmin中访问时出现2002 无法登录 MySQL 服务器
phpmyadmin中访问时出现2002 无法登录 MySQL 服务器! 解决方法如下: 修改phpmyadmin目录中libraries文件夹下的config.default.php文件 $cfg[ ...
- nginx日志中访问最多的100个ip及访问次数
nginx日志中访问最多的100个ip及访问次数 awk '{print $1}' /opt/software/nginx/logs/access.log| sort | uniq -c | sort ...
- Fastreport使用经验(转)在Delphi程序中访问报表对象
Fastreport使用经验(转) 在Delphi程序中访问报表对象 最基本的方法就是frxReport1.FindObject. 然后把返回的对象强制转换成它的类型,当然,在报表中必须真的有这么个东 ...
- 如何在外网中访问自己在另一个局域网中的某个机器(SSH为例)
UBUNTU 14.04 LTS 为例 如何在外网中访问自己在另一个局域网中的某个机器(SSH为例) 2013-05-01 16:02 2693人阅读 评论(0) 收藏 举报 情景描述: 计算机C1放 ...
- 在Asp.net MVC中访问静态页面
有时候由于一些特殊的需要,我们需要在MVC中访问HTML页面,假如您将这个页面放在Views中的话,去访问将会收到一个404,但是放在Views外面的目录则不受此限制. 那么我们就来解决View里面的 ...
- 九、在动作类中访问ServletAPI
九.在动作类中访问ServletAPI .方式一:(简单,推荐使用)ServletActionContext public String execute() throws Exception { ...
随机推荐
- 9.14noip模拟试题
中文题目名称 祖孙询问 比赛 数字 英文题目名称 tree mat num 可执行文件名 tree mat num 输入文件名 tree.in mat.in num.in 输出文件名 tree.out ...
- Datables wrning(table id='example'):Cannot reinitialise DataTable.
出现的问题如下所示: Datables wrning(table id='example' Datables object for this table,please pass eithser no ...
- 关于PHP导入项目的时候导入不了的情况
导入的时候,会发现明明是一个手动创建的一个项目, 才能导入, 有时候会发现这样导入不了的情况 那是因为,可能这个项目是手动创建的,如果通过IDE可能看不出来 不过如果你进入项目的根目录的时候就会知道 ...
- 一、初识T4引擎
对于代码生成器我们并不陌生,在日常编码中这也是用的比较多的工具之一.一般代码生成器主要功能是生成公共或基础代码来减少编码人员的工作量,而一款优秀的代码生成器除了生产代码以外,同时兼具生成项目架构和基础 ...
- 总结Linux下查看流量工具
Linux服务器要查看带宽情况,可以使用nethogs.dstat.nload.iftop.ifstat工具. 而每个工具都有自己的特色,这里简单总结一下使用方法. 一.nethogs 查看这台设备上 ...
- OC - 16.大文件下载
大文件下载注意事项 若不对下载的文件进行转存,会造成内存消耗急剧升高,甚至耗尽内存资源,造成程序终止. 在文件下载过程中通常会出现中途停止的状况,若不做处理,就要重新开始下载,浪费流量. 大文件下载的 ...
- 玩转iOS 9的UIDynamics(转)
转自 http://www.cocoachina.com/ios/20150716/12613.html 本文由CocoaChina翻译小组成员AGSpider(微博)翻译自fancypixel的博客 ...
- Swift进阶
概述 访问控制 Swift命名空间 Swift和ObjC互相调用 Swift和ObjC映射关系 Swift调用ObjC ObjC调用Swift 扩展—Swift调用C 反射 扩展—KVO 内存管理 循 ...
- Swift - 22 - 循环结构
//: Playground - noun: a place where people can play import UIKit // for-in for i in -99...99 { i * ...
- 【USACO 2.4.3】牛的旅行
[描述] 农民 John的农场里有很多牧区.有的路径连接一些特定的牧区.一片所有连通的牧区称为一个牧场.但是就目前而言,你能看到至少有两个牧区通过任何路径都不连通.这样,Farmer John就有多个 ...