本文代码使用了一些C++11特性,需要编译器支持。本文仅讨论x86_64平台的相关实现,x86平台理论上只需修改 thunk 相关机器码即可。

THUNK的原理参见之前的一篇博文《C++ 通过Thunk在WNDPROC中访问this指针

首先定义我们的window类,该类实现对一个Win32窗口句柄的封装。

该类将在构造函数中创建窗口,在析构时销毁窗口;

窗口的消息过程函数(WindowProc)将是一个用机器码在内存中动态构造的thunk,其作用是把收到的4个参数中的第一个也就是窗口句柄替换成window类的this指针,然后把调用传递给window类的静态函数static_procedure;

静态成员函数static_procedure的signature与WNDPROC相似,同为stdcall调用约定,四个参数中仅第一个参数由HWND类型改为了window类指针,该函数的目的省去在thunk中处理虚函数调用,因此它仅简单把调用传递给window类的非静态成员函数procedure;

非静态成员函数procedure是一个protected的虚函数,真正负责消息处理且可以override;

此外由于窗口消息过程函数是在注册Win32窗口类时提供而不是创建窗口时提供,此时window类实例可能尚未构造,因此这是thunk还无法构建,这就需要使用一个临时的WindowProc来进行过度,并负责在收到第一个消息时通过SetWindowLongPtr将窗口过程设置为thunk,这个函数就是first_message_procedure,一个stdcall的静态函数,符合WNDPROC的signature要求;

此外还需要一个单例对象来负责Win32窗口类的注册与消息,该对象的类型及实现稍后考虑,现在仅确定其提供一个name函数来返回Win32窗口类的名字。

class window {
public:
window();
virtual ~window() noexcept;
window(const window& other) = delete;
window(window&& other);
public:
HWND handle() {
return _handle;
}
protected:
virtual LRESULT procedure(UINT msg, WPARAM wParam, LPARAM lParam);
private:
static LRESULT CALLBACK static_procedure(window* thiz, UINT msg,
WPARAM wParam, LPARAM lParam);
static LRESULT CALLBACK first_message_procedure(HWND window, UINT msg,
WPARAM wParam, LPARAM lParam);
private:
HWND _handle;
void* _thunk;
private:
static class window_class _class;
};

首先来看看window类的构造函数的实现。首先我们要使用当前的this指针和static_procedure函数指针来构造一个thunk,然后我们调用Win32 API的CreateWindow/CreateWindowEx函数来创建窗口。在窗口创建过程中,注册Win32窗口类时指定的first_message_procedure将会至少收到1次消息(实际上WM_NCCREATE, WM_CREATE两个消息是一定会出现的,此外还有WM_GETMINMAXINFO),此时CreateWindow/CreateWindowEx尚未返回。 在first_message_procedure中,把收到的HWND句柄存入window类实例中,并调用SetWindowLongPtr来将当前窗口的WindowProc设置为前边构建的thunk的指针。最后当然是返回对procedure成员函数的调用了。也就是说包括第一个窗口消息在内,所有的窗口消息实质上都是由procedure函数处理的。

说到这里,有一个棘手的问题 -- 如何将window类实例指针或引用传递给静态函数 first_message_procedure ? 鉴于 win32 API 实在是残废,说好了WM_NCCREATE消息是第一个消息,但却很没节操的在前边插一个WM_GETMINMAXINFO消息,而WM_GETMINMAXINFO中又没有那个CREATESTRUCT结构。 当然可以选择忽略WM_NCCREATE消息之前的所有消息,但那一定是万般无奈之后的决定。而这里还有另一条更完美的小路可走:线程本地变量(thread local variable)。因为在 first_message_procedure 收到最初的几个消息并返回之前,CreateWindow/CreateWindowEx不会返回,也就是说我们基本可以断定对first_message_procedure 的调用永远发生在调用CreateWindow/CreateWindowEx的线程上,也就是说通过一个线程本地变量,可以方便的把任何数据传递给first_message_procedure。

thread_local window* _window_creatting = nullptr;

window::window() : _handle(), _thunk(nullptr) {
// g_thunk_manager 是一个全局变量,用来管理所有thunk
_thunk = g_thunk_manager.alloc_thunk(this, static_procedure); _window_creatting = this;
CreateWindowExW(WS_EX_OVERLAPPEDWINDOW, _class.name(), nullptr, WS_TILEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, , , get_executable_module(), nullptr);
_window_creatting = nullptr; ShowWindow(_handle, SW_SHOW);
} LRESULT CALLBACK window::first_message_procedure(HWND handle, UINT msg, WPARAM wParam, LPARAM lParam) { window* w = _window_creatting;
w->_handle = handle;
SetWindowLongPtrW(handle, GWLP_WNDPROC, (LONG_PTR) w->_thunk);
return w->procedure(msg, wParam, lParam);
return ;
} LRESULT CALLBACK window::procedure(UINT msg, WPARAM wParam, LPARAM lParam) {
return DefWindowProcW(_handle, msg, wParam, lParam);
}
LRESULT CALLBACK window::static_procedure(window* thiz, UINT msg, WPARAM wParam, LPARAM lParam) {
return thiz->procedure(msg, wParam, lParam);
}

首先说上边提到的window_class类比较简单,该类的构造函数接受两个参数,一个是win32窗口类的名字,一个是默认的窗口过程(WindowProc),(这里一切从简,理论上应该多传些参数进去,比如窗口图标、背景刷子等),在构造函数中通过Win32 API注册这个窗口类,并在析构时注消。该类的实现细节不作过多讨论,仅仅是调用RegisterClass/RegisterClassEx和UnRegisterClass函数而已。以下是类的原型

class window_class {
public:
window_class(const std::wstring class_name, WNDPROC wndproc);
~window_class();
LPCWSTR name() const {
return (LPCWSTR) (intptr_t) _atom;
}
private:
ATOM _atom;
};

下边就是重点了thunk构建了。 这里使用一个thunk_manager的类来负责管理thunk的构建与释放。由于DEP(数据执行保护)的问题,无法使用默认的栈内存或堆内存来构建thunk,这些内存是不可执行的。这里需要通过Win32 API中的VirtualAllocEx/VirtualAlloc和VirtualFree/VirtualFreeEx来向系统申请或返还可执行内存。为简单起见,我们预估一下程序需要同时使用的thunk的数量,一次性向系统申请足够的内存,免去内存管理的麻烦。比如我们一次性申请4M内存来(对于现在的机器,4M一般也不算什么大内存),每个thunk大概占用32个字节,也就是足够13万多个thunk,一般应用场合足矣。这里使用一个virtual_memory类来专门管理VirtualAllocEx/VirtualAlloc和VirtualFree/VirtualFreeEx,在构造时调用 VirtualAllocEx/VirtualAlloc 申请内存,在析构时调用VirtualFree/VirtualFreeEx释放内存。而 thunk_manager 专门负责在这些内存中为正在构造的窗口找到一块空闲之地,并把构造机器码填入这块内存;在窗口销毁后,把其使用过的thunk内存重新标记为空闲供后续窗口重复使用。需要留心的是thunk_manager需要线程安全。

struct thunk_code_type {
uint8_t mov_rax_1[]; // mov &window_instance to rax
uint8_t object[sizeof(window*)];
uint8_t mov_rax_to_rcx[]; // mov rax to rcx
uint8_t mov_rax_2[]; // mov &first_message_procedure to rax
uint8_t procedure[sizeof(window_procedure_type)];
uint8_t jump_rax[]; // jmp to [rax]
#ifdef _WIN64
thunk_code_type(const window*w, window_procedure_type proc) :
mov_rax_1 { 0x48, 0xb8 }, object { }, //
mov_rax_to_rcx { 0x48, 0x89, 0xc1 }, mov_rax_2 { 0x48, 0xb8 }, //
procedure { }, jump_rax { 0x48, 0xff, 0xe0 } {
*reinterpret_cast<const window**>(&object) = w;
*reinterpret_cast<window_procedure_type*>(procedure) = proc;
}
#else
#error Only x86_64 is supported now.
#endif
~thunk_code_type() {
}
}; struct thunk_type{
thunk_code_type code;
volatile long flag;
}; thunk_manager::thunk_manager(size_t max_count) :
_memory(sizeof(thunk_type) * max_count, true), _max_count(max_count) {
}
thunk_manager::~thunk_manager() {
}
void* thunk_manager::alloc_thunk(const window* w, window_procedure_type proc) {
thunk_type* memory = reinterpret_cast<thunk_type*>(_memory.get());
thunk_type* end = memory + _max_count;
for (thunk_type * p = memory; p < end; p++) {
auto ret = InterlockedBitTestAndSet(&p->flag, );
if (!ret) {
new (&p->code) thunk_code_type(w, proc);
return p;
}
}
throw std::bad_alloc();
}
void thunk_manager::free_thunk(void* thunk) {
thunk_type* p = reinterpret_cast<thunk_type*>(thunk);
InterlockedBitTestAndReset(&p->flag, );
}

文中所有代码兼容 gcc 4.8.2 ( mingw64) with posix threading model,启用 -std=c++11 选项;其它编译器未测试。

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

  1. C++ 通过Thunk在WNDPROC中访问this指针

    本文基本只讨论原理,具体实现请参见后续文章<C++ 通过Thunk在WNDPROC中访问this指针实现细节> 当注册窗口类时,WNDCLASSEX结构的lpfnWndProc成员应设置为 ...

  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. Android(java)学习笔记242:多媒体之设置全屏的方法

    在实际的应用程序开发中,我们有时需要把 Activity 设置成全屏显示,一般情况下,可以通过两种方式来设置全屏显示效果.其一,通过在代码中可以设置,其二,通过manifest配置文件来设置全屏. 其 ...

  2. Linq101-Ordering

    using System; using System.Collections.Generic; using System.Linq; namespace Linq101 { class Orderin ...

  3. ChesFrame框架介绍

    一直以来想写一个框架,想达到的目的: 1.对曾经做过项目的总结 2.节约构建系统的成本,不用每次都从零开始做起 3.写框架并在使用中不断的完善框架,这也是个积攒过程. 经历了一个多月的时间,一个基本的 ...

  4. asp.net 读取sql存储过程返回值

    关于Exec返回值的问题有很多,在这做个简要的总结. 读查询语句示例:    Declare @count int     select @Count 要点:                      ...

  5. MySQL 管理

    MySQL 管理 启动及关闭 MySQL 服务器 首先,我们需要通过以下命令来检查MySQL服务器是否启动: ps -ef | grep mysqld 如果MySql已经启动,以上命令将输出mysql ...

  6. [转载]delete指针之后应该赋值NULL

    首先,C++标准规定:delete空指针是合法的,没有副作用.但是,delete p后,只是释放了指针指向的内存空间.p并不会自动被置为NULL,而且指针还在,同时还指向了之前的地址. 问题来了,对一 ...

  7. [LeetCode OJ] Reorder List—Given a singly linked list L: L0→L1→…→Ln-1→Ln, reorder it to: L0→Ln→L1→Ln-1→L2→Ln-2→…

    For example,Given {1,2,3,4}, reorder it to {1,4,2,3}. /** * Definition for singly-linked list. * str ...

  8. C# Trim方法去除字符串两端的指定字符

    var str= ",2,3,4,6,7,"; var str2 = str.Trim(new char[] { ',' }); //去除字符串str两端的','字符. //则st ...

  9. 较详细的sqlserver数据库备份、恢复(转)

    C#实现SQL数据库备份与恢复 有两种方法,都是保存为.bak文件.一种是直接用Sql语句执行,另一种是通过引用SQL Server的SQLDMO组件来实现: .通过执行Sql语句来实现 注意,用Sq ...

  10. 001.为什么选择用AngularJs开发?

    网上关于AngularJs(https://angularjs.org/)的介绍与好处已经相当多了,大家可以自己去搜索看看,很多很多. 1.疯狂的原生的App开发 在这里我主要谈一下我自己为什么要使用 ...