ATL 的另一个新 thunk 复制






4.43/5 (5投票s)
摆脱 ATL,但自己实现类似灵活架构的实用技巧。
简介
摆脱 ATL 的实用技巧,同时也提供了一个类似于 ATL 的核心,你将能够快速地从自己编写的轻量级框架开始,如果你掌握了这种新的 thunk 技术,就不需要编写基于 Windows SDK 的过程式 C++ 代码。这使得你的代码更清晰,不只看起来像 C++。
背景
大多数 Windows 开发者都知道如何在 ATL 框架上工作,很少有人知道 ATL 使用 thunk 技术在 WindowProc 回调函数中传递类实例的 This 指针的核心机制,This 指针可以通过汇编代码替换的第一个参数 - hWnd 获取,这种技术不像 MFC 那样使用映射表来查找 This 指针,因此它比 MFC 具有更高的性能。
好吧,我不是 C++ 甚至 ATL 的专家,在业余时间,我喜欢开发一些小型 Windows 应用程序。并且希望它有高性能。所以我选择 ATL 框架作为 Windows 应用程序的基础,但这个基础仍然太大,换句话说,这个基础仍然通过 C++ 模板特性进行了复杂的封装,在我的情况下,对于小型 Windows 应用程序来说并不是很好,还有另一个选择 - 直接在 Windows SDK 架构上编码,但它无法有效地利用类特性来将许多方法封装为一个组件。
陷入两难境地,在我做了一些调查并深入研究了 ATL 框架的核心之后,现在,我知道 ATL 如何获取类实例的 This 指针,并在 WindowProc 中传递 This 指针,这就是我们之前提到的 thunk 技术。
原理是它将 pThis 和相关的线程 ID 推送到 _ATLModule 维护的全局列表中以创建窗口,然后在 WindowProc 中从全局列表中弹出 pThis,根据我的理解和在 Google 上搜索的一些观点,这个原理应该是可靠的,因为一个线程不能同时创建许多窗口,并且调用 WindowProc 具有 FIFO 特性,即,第一个创建的窗口将获取全局列表中的第一个 This 指针。
在我的情况下,我计划摆脱 ATL,但也想在我的代码中使用它的 thunk 技术,从而使代码更有效地封装在类中。问题是如果我不打算使用 ATL,我如何获取 thunk 机制?如你所知,'thunk' 的细节由 _ATLModule
维护,所以看起来我们无法摆脱它,必须找到一个好办法..
使用代码
经过一夜的奋斗,我找到了关键点——旨在在
中传递类 This 指针的 'thunk' 数据结构,我在原始结构(你可以在 <atlstdthunk.h> 中找到它)的基础上做了一些修改,下面的新 'thunk' 数据结构就是我修改后的,你可以先记住,把它看作一种新的 thunk 技术!WindowProc
#pragma pack(push,1)
struct _stdcallthunk
{
DWORD m_mov; //
DWORD m_this; //
BYTE m_jmp; // jmp WndProc
DWORD m_relproc; // relative jmp
BOOL Init(DWORD_PTR proc, void* pThis)
{
m_this = PtrToUlong(pThis);
m_jmp = 0xe9;
m_relproc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(_stdcallthunk)));
// write block from data cache and
// flush from instruction cache
FlushInstructionCache(GetCurrentProcess(), this, sizeof(_stdcallthunk));
return TRUE;
}
//some thunks will dynamically allocate the memory for the code
void* GetCodeAddress()
{
return this;
}
};
#pragma pack(pop)
如果你将修改后的代码与 <atlstdthunk.h> 中的原始代码进行比较,你会发现 m_mov
在 Init
函数中缺失了。
是的,我已经删除了它,我稍后会解释为什么。
为了方便起见,我做了一个使用新 'thunk' 技术的对话框演示项目,在 CTestDlg
类中,我放置了两个新 'thunk' 数据结构的实例。请参考以下代码:
注意,下面的部分将涉及一些汇编语言知识,但这不是必需的,你可以尝试阅读它,或者直接跳过它,然后直接查看对话框演示项目。
如果没有特殊声明,我们将只关注 X86 thunk,即 thunk 代码遵循 #if defined(_M_IX86)
指令。(备注 1.)
HANDLE CTestDlg::s_hPrivHeap = NULL;
CTestDlg::CTestDlg(void)
{
// Uses Heap to construct the thunk(s) for avoiding DEP (Data Execution Prevention)
if (!s_hPrivHeap)
{
// dwMaximumSize is zero that means it specifies that the private heap is growable.
// The heap's size is limited only by available memory.
s_hPrivHeap = ::HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0);
if (!s_hPrivHeap) throw "error: failed to create private heap!";
}
m_thunk = (_stdcallthunk*)::HeapAlloc(s_hPrivHeap, HEAP_ZERO_MEMORY, sizeof(_stdcallthunk));//(_stdcallthunk*)VirtualAlloc(NULL, sizeof(_stdcallthunk), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!m_thunk) throw "error: m_thunk cannot be allocated by HeapAlloc";
#if defined(_M_IX86)
// mov dword ptr [esp+0x14], pThis (esp + 0x14 is the custom 5th param- pThis)
m_thunk->m_mov = 0x142444C7;
#elif defined (_M_AMD64)
// mov r9, pThis (r9 is the 64 bit register to store the 4th parameter - lParam
m_thunk->m_mov = 0xb949; // mov r9, pThis
#endif
m_thunk2 = (_stdcallthunk*)::HeapAlloc(s_hPrivHeap, HEAP_ZERO_MEMORY, sizeof(_stdcallthunk));//(_stdcallthunk*)VirtualAlloc(NULL, sizeof(_stdcallthunk), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!m_thunk2) throw "error: m_thunk2 cannot be allocated by HeapAlloc";
#if defined(_M_IX86)
// mov dword ptr [esp+0x4], pThis (esp+0x04 is hWnd, pThis is assigned to hWnd)
m_thunk2->m_mov = 0x042444C7;
#elif defined (_M_AMD64)
// mov rcx, pThis (rcx is the 64 bit register to store the 1st parameter - hWnd
m_thunk2->m_mov = 0xb948;
#endif
}
在类 CTestDialog
的构造函数中,我使用 HeapCreate
为 _stdcallthunk
结构的两个实例分配内存,并将内存页面标记为 HEAP_CREATE_ENABLE_EXECUTE
(备注 2. ),这将避免 DEP (数据执行保护) 问题,即,如果 thunk 实例正常初始化,thunk 的内存页面不会被标记为可执行,一旦在系统高级设置中启用 DEP,thunk 将会崩溃!
花点时间理解 DEP 问题,让我们关注关键代码,我在其中为两个 thunk 实例分别放置了不同的汇编指令,用于 m_mov
。
对于第一个实例 - m_thunk
,我们摆脱了 ATL 中用于获取 This 指针的全局列表,而是通过 mov 指令在堆栈帧上添加一个额外的参数:
m_thunk.m_mov = 0x142444C7;
对应于 -
mov dword ptr [esp+0x14], pThis
现在,我们可以在 StartDialogProc
中检索 This
:
INT_PTR CALLBACK CTestDlg::StartDialogProc(HWND hwndDlg, // handle to dialog box
UINT uMsg, // message WPARAM wParam,
WPARAM wParam, // first message parameter
LPARAM lParam, // second message parameter
DWORD_PTR This
)
{
CTestDlg* pThis = (CTestDlg*)This;
pThis->m_hDlg = hwndDlg;
// Initalize next thunk ..
pThis->m_thunk2->Init((DWORD_PTR)pThis->DialogProc, pThis);
DLGPROC pProc = (DLGPROC)pThis->m_thunk2->GetCodeAddress();
DLGPROC pOldProc = (DLGPROC)::SetWindowLongPtr(hwndDlg, DWLP_DLGPROC, (LONG_PTR)pProc);
return pProc(hwndDlg, uMsg, wParam, lParam);
}
一旦我们检索到 This 指针,我们就会将当前的 HWND
句柄分配给 pThis->m_hDlg
,然后我们进一步初始化下一个 thunk - m_thunk2
,这将转发到真实的窗口过程 - DialogProc
,并且 DialogProc
中的第一个参数 hwndDlg
将被 This 指针替换。
INT_PTR CALLBACK CTestDlg::DialogProc(HWND hwndDlg, // handle to dialog box
UINT uMsg, // message WPARAM wParam,
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
)
{
CTestDlg* pThis = (CTestDlg*)hwndDlg; // We take out pThis from hwndDlg
...
以上所有处理都类似于原始的 ATL thunk 机制,不同之处在于我们使用了两个不同的 thunk(即不同的 mov 指令),独立于一个全局列表来获取 This 指针,因此我们完全摆脱了 ATL!
关注点
这种新的 thunk 技术并不是什么创意,我只是采用了一种新的方法来检索 This 指针,因此,你不仅摆脱了 ATL,还有机会保持代码的整洁,正如我一开始提到的,另一方面,当你只编写一个小型 Windows 应用程序时,它对提高性能绝对有益。
顺便说一句,新的 thunk 技术只在 X86 上工作,我很快会在 X64 上做一些研究。
备注
1. 演示项目也支持 X64 thunk,X64 thunk 的汇编代码有一些变化,它不像 X86 thunk,它在 StartDialogProc 的第四个参数 - lParam
中传递 This 指针,在 X64 上,它由调用者而不是被调用者管理堆栈帧,如果我们将 This 指针放在堆栈帧 [rsp+xxh]
上,那么我们仍然需要手动编写更多的汇编代码来恢复堆栈帧(thunk 代码是调用者),这对我来说太复杂了,所以我只将 This 指针传递给第四个参数 - lParam,基本上,当 StartDialogProc
最初被调用时,lParam 是空的,所以我选择这个参数来保存 This 指针。我不确定我是否理解正确。
2. 早些时候,我使用 VirtualAlloc 动态构造 thunk 实例,但这浪费了太多内存空间,我甚至在新的内存页(4k)中创建了每个 thunk,即使你想在同一个内存页中放置其他 thunk,你仍然需要编写额外的代码来创建一个列表,然后检测哪个 thunk 被释放或没有...,HeapCreate / HeapAlloc / HeapFree 将更容易地在堆后面自动管理内存,它已经实现了这种智能。
历史
2012-7-28:修复 DEP (数据执行保护) 问题。
2012-7-29:Thunk 可以通过进程中的私有堆自动管理。新的演示项目支持 X64 thunk。