65.9K
CodeProject 正在变化。 阅读更多。
Home

ATL 的另一个新 thunk 复制

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.43/5 (5投票s)

2012 年 7 月 25 日

CPOL

6分钟阅读

viewsIcon

36022

downloadIcon

362

摆脱 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 维护,所以看起来我们无法摆脱它,必须找到一个好办法..微笑 | <img src= 

使用代码 

经过一夜的奋斗,我找到了关键点——旨在在 WindowProc 中传递类 This 指针的 'thunk' 数据结构,我在原始结构(你可以在 <atlstdthunk.h> 中找到它)的基础上做了一些修改,下面的新 'thunk' 数据结构就是我修改后的,你可以先记住,把它看作一种新的 thunk 技术!

#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_movInit 函数中缺失了。

是的,我已经删除了它,我稍后会解释为什么。

为了方便起见,我做了一个使用新 '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 应用程序时,它对提高性能绝对有益。微笑 | <img src= 

顺便说一句,新的 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。

© . All rights reserved.