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

C++ WinAPI 包装器对象,使用 thunk(x32 和 x64)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (31投票s)

2016 年 8 月 31 日

CPOL

14分钟阅读

viewsIcon

65637

使用“thunk”技术将 this 指针作为第五个参数添加到 WndProc 调用中,适用于 x32 和 x64

引言

本文介绍了一种称为“thunking”的技术,作为在 C++ 对象中实例化 WinAPI 的一种方式。虽然有多种实现方法,但本文描述了一种拦截 WndProc 调用并将 this 指针附加到函数调用第五个参数的方法。它使用一个 thunk 和一次函数调用,并且同时支持 x32 和 x64。

背景

WinAPI 的实现是在 C++ 和 OOP 流行之前引入的。ATL 等尝试过使其更面向类和对象。主要障碍是消息处理过程(通常称为 WndProc)不是由应用程序调用的,而是由 Windows 本身从外部调用的。这要求该函数是全局的,并且如果是 C++ 成员函数,则必须声明为 static。结果是,当应用程序进入 WndProc 时,它没有指向特定对象实例的指针来调用任何其他处理函数。

这意味着任何 C++ OOP 方法都必须解决如何从 static 成员函数确定消息处理应该传递给哪个对象方法的问题。

一些选项包括:

  1. 仅针对单个窗口实例进行设计。消息处理器可以是全局的或命名空间范围的。
  2. 可以使用 cbClsExtracbWndExtra 提供的额外内存位来存储指向正确对象的指针。
  3. 向窗口添加一个指向对象的属性,并使用 GetProp 来检索它。
  4. 维护一个引用对象指针的查找表。
  5. 使用一种称为“thunk”的方法。

每种方法都有优缺点。

  1. 您仅限于单个窗口,代码也无法重用。这对于简单的应用程序来说可能没问题,但如果您花费精力将其封装在对象中,那么您最好放弃它,坚持使用标准模板。
  2. 该方法“速度慢”,需要开销才能每次在消息传入时从额外内存位获取指针。此外,它降低了代码的可重用性,因为它依赖于这些值在窗口生命周期内不被覆盖或用于其他目的。另一方面,它实现起来简单明了。
  3. 比第 2 种方法慢,并且引入了类似的开销,但您可以消除数据被覆盖的可能性(尽管您需要确保属性名称是唯一的,以免与其他添加的属性冲突)。
  4. 在这里,我们遇到了性能和开销问题,因为查找表不断增大,而且每次调用消息处理器函数时都需要进行此查找。它确实允许函数成为 private static 成员。
  5. 这有点棘手,但提供了低开销、比其他方法更好的性能,并允许增强的灵活性,适用于任何 OOP 设计风格。

事实上,很多应用程序并不需要任何花哨的东西,可以使用更传统的方法。但是,如果您想构建一个低开销的可扩展框架,那么第 5 种方法是最佳选择,本文将概述如何实际实现这种设计。

Using the Code

Thunk 是一段位于内存中的可执行代码。它有可能在执行时更改执行的代码。其思想是将一小段代码放入内存,然后让它执行并修改其他地方的运行代码。就我们而言,我们希望捕获消息处理成员函数的可执行地址,并将其替换为最初注册的函数,并将对象的地址与函数一起编码,以便它能够正确地调用消息处理队列中的下一个正确非静态成员函数。

首先,让我们为这个项目创建我们的模板。我们需要一个包含 wWinMain 函数的主文件。

// appmain.cpp : Defines the entry point for the application.
//
 
#include "stdafx.h"
#include "AppWin.h"
 
 
int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, 
_In_ LPWSTR    lpCmdLine, _In_ int nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);
}

现在我们的 AppWin.hAppWin.cpp 文件,并创建一个空的类结构。

// AppWin.h : header file for the AppWinClass
//
 
#pragma once
 
#include "resource.h"
 
class AppWinClass {
public:
    AppWinClass(){}
    ~AppWinClass(){}
 
private:
};

// AppWin.cpp : implementation of AppWinClass
//
 
#include "stdafx.h"
#include "AppWin.h"
 
AppWinClass::AppWinClass() {}
AppWinClass::~AppWinClass() {}

我们需要设置我们的对象,包含创建窗口所需的所有必要元素。第一个元素是注册 WNDCLASSEX 结构。WNDCLASSEX 中的某些元素应该允许创建对象的代码进行更改,但我们希望保留某些字段由对象控制。

这里的一个选项是定义一个“struct”,其中包含我们允许用户自行定义的元素,然后将其传递给一个函数以复制到将要注册的 WNDCLASSEX 结构中,或者直接将元素作为函数调用的一部分传递。如果我们使用“struct”,数据元素可能会在其他地方重用。当然,struct 会占用内存,如果我们只使用这些元素一次,那效率不高。您可以简单地将元素作为函数调用的一部分传递,将范围缩小到该函数,从而提高效率。但我们需要传递至少 20 个参数,然后对每个参数的值进行检查。

在这里,我们将在创建函数中声明默认值,然后在类外部声明一个“struct”,如果用户想调整默认值,他们可以,并且他们可以管理该结构的生命周期。用户只需向函数指示他们是传递 struct 并更新默认值,还是仅使用默认值。所以,我们声明以下函数:

int AppWinClass::Create(HINSTANCE hInstance, int nCmdShow, AppWinStruct* varStruct)

hInstance 在整个创建过程中使用,nCmdShow 作为 ShowWindow 调用的一部分传递。

所以我们开始函数,检查是否收到了 AppWinStruct,如果没有,我们就用默认值加载 WNCLASSEX 结构,否则我们接受 AppWinStruct 提供的。

int AppWinClass::Create(HINSTANCE hInstance, int nCmdShow = NULL, AppWinStruct* varStruct = nullptr)
{  
    WNDCLASSEX wcex; //initialize our WNDCLASSEX
    wcex.cbSize      = sizeof(WNDCLASSEX);
    wcex.hInstance      = hInstance;
    wcex.lpfnWndProc = ;
    if (!varStruct) //default values
    {
        varStruct = new AppWinStruct;
        wcex.style            = CS_HREDRAW | CS_VREDRAW;
        wcex.cbClsExtra    = 0;
        wcex.cbWndExtra    = 0;
        wcex.hIcon            = LoadIcon(nullptr, IDI_APPLICATION);
        wcex.hCursor        = LoadCursor(nullptr, IDC_ARROW);
        wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
        wcex.lpszMenuName  = nullptr;
        wcex.lpszClassName = L"Window";
        wcex.hIconSm        = NULL;        
    }
    else
    { //user defined
        wcex.style            = varStruct->style;
        wcex.cbClsExtra    = varStruct->cbClsExtra;
        wcex.cbWndExtra    = varStruct->cbWndExtra;
        wcex.hIcon            = varStruct->hIcon;
        wcex.hCursor        = varStruct->hCursor;
        wcex.hbrBackground = varStruct->hbrBackground;
        wcex.lpszMenuName  = varStruct->lpszMenuName;
        wcex.lpszClassName = varStruct->lpszClassName;
        wcex.hIconSm        = varStruct->hIconSm;
    }

请注意,我们缺少 wcex.lpfnWndProc 的声明。此变量将注册我们的消息处理函数。由于设置的原因,此函数必须是 static,因此无法调用对象的特定函数来处理特定消息的消息处理。典型的 WNDPROC 函数头如下所示:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

最终,我们将使用 thunk 来有效地重载函数调用,并添加一个第 5 个参数,它将是我们对象 this 的指针。在此之前,我们将声明我们的 WndProc 函数。这只是一个标准的 WndProc 函数,提供 PAINTDESTROY 消息的处理——足以启动一个窗口。

// AppWin.h
class AppWinClass {
.
.
.
private:

    static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, 
                    WPARAM wParam, LPARAM lParam, DWORD_PTR pThis);

//AppWin.cpp
LRESULT CALLBACK AppWinClass::WndProc(HWND hWnd, UINT message, 
                    WPARAM wParam, LPARAM lParam, DWORD_PTR pThis)
{
    switch (message)
    {
    case WM_PAINT:
    {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hWnd, &ps);
        // TODO: Add any drawing code that uses hdc here...
        EndPaint(hWnd, &ps);
    }
    break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

在这里,我们声明了一个第五个参数,它将包含我们的 this 指针。Windows 将使用传递的四个标准参数调用它。所以我们需要中断函数调用,并在调用堆栈上放置一个第五个参数,它将是我们类对象的指针。这就是 thunk 的作用。

同样,thunk 是堆上的一段可执行代码。与直接调用窗口消息过程相比,我们将调用 thunk,就像它是一个函数一样。在调用 thunk 之前,函数变量会被压入堆栈,thunk 所要做的就是再添加一个变量到堆栈,然后跳转到原始目标函数。

几点说明。由于 DEP(数据执行保护),我们必须分配一些标记为可执行的堆内存。否则,DEP 将阻止代码执行并引发异常。我们使用 HeapCreate 并设置 HEAP_CREATE_ENABLE_EXECUTE 位。HeapCreate 至少会保留一个 4k 的内存页面,而我们的 thunk 非常小。由于我们不想为每个对象的每个新 thunk 实例创建新页面,我们将声明一个变量来保存堆句柄,以便可以重用该堆。

// AppWin.h
class AppWinClass {
.
.
.
private:
    static HANDLE eheapaddr;
    static int objInstances;

我们将使用我们的类构造函数来创建堆。

//AppWin.cpp
HANDLE AppWinClass::eheapaddr = NULL;
int    AppWinClass::objInstances = 0;

AppWinClass::AppWinClass()
{
    objInstances = ++objInstances;
    if (!eheapaddr)
    {
        try {
            eheapaddr = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE | HEAP_GENERATE_EXCEPTIONS, 0, 0);
        }
        catch (...) {
            throw;
        }
    }
    try {
        thunk = new(eheapaddr) winThunk;
    }
    catch (...) {
        throw;
    }
}

我们初始化 static eheapaddr(可执行堆地址)和 objInstances(我们的计数器,用于计算我们对象实例的数量)为 0。在构造函数中,我们首先增加 objInstances。我们不希望在所有其他对象实例消失之前销毁堆。现在,我们检查 eheapaddr 是否已初始化,如果没有,则将其设置为 HeapCreate 返回的句柄值。我们调用 HeapCreate 并指定我们希望在该堆上启用代码执行,并且我们希望在分配失败时生成异常。然后我们将其包装在 try catch 语句中,该语句将重新抛出 HeapCreate 抛出的异常,并允许对象调用者进行处理。

我们还将把我们的 thunk 分配到堆上。我们还将重载我们 thunk 类的 new 运算符,以便它可以在我们的堆上分配,并且我们可以传递来自 HeapCreate 的句柄。我们也将这包装在 try catch 语句中,以防分配失败(因为我们为 HeapCreate 设置了 HEAP_GENERATE_EXCEPTIONSHeapAlloc 也会生成异常)。

我们将在对象被删除时销毁堆,因此我们将更新析构函数如下:

//AppWin.cpp
AppWinClass::~AppWinClass() {
    if (objInstances == 1)
    {
        HeapDestroy(eheapaddr);
        eheapaddr = NULL;
        objInstances = 0;
    }
    else
    {
        objInstances = --objInstances;
        delete thunk;
    }
}

只需检查我们是否是最后一个对象实例,如果是,则销毁堆并将 eheapaddr 重置为 NULL。否则,递减 objInstances注意eheapaddrobInstances 不需要设置为零,因为我们的整个对象即将消失。我们需要调用我们 thunkdelete 运算符,以确保它从我们的堆中释放自己。

这里有一点要注意:InterlockedInstances() 可以用于提供更好的多线程方法,而不是增加和减少 static 计数器。

现在我们可以声明我们的 thunk 类了。因为 x32 和 x64 在处理堆栈和函数调用方面有所不同,所以我们需要将声明包装在 #if 定义的语句中。我们使用 _M_IX86 表示 x32 位应用程序,使用 _M_AMD64 表示 x64 位应用程序。

其思想是创建一个结构并将变量按特定顺序放在顶部。当我们调用这个“函数”时,我们实际上是调用结构内存的顶部,并将开始执行存储在顶部变量中的代码。

我们使用 #pragma pack(push,#) 声明来正确对齐字节以供执行,否则编译器可能会填充变量(并且在 x64 设置下仍然会这样做)。

对于 x32,我们需要 7 个变量。然后我们将它们赋为我们 x86 汇编代码的十六进制等效值。汇编如下:

push dword ptr [esp] ;push return address 
mov dword ptr [esp+0x4], pThis ;esp+0x4 is the location of the first element in the function header
                               ;and pThis is the value of the pointer to our object’s “this”
jmp WndProc ;where WndProc is the message processing function

由于我们在程序运行前不知道 pThisWndProc 的值,因此我们需要在运行时收集这些值。所以我们在结构中创建一个函数来初始化这些变量,我们将同时传递消息处理函数和 pThis 的位置。

我们还需要刷新指令缓存,以确保我们的新代码可用,并且指令缓存不会尝试执行旧代码。如果刷新成功(返回 0),则返回 true,否则返回 false,让程序知道我们遇到了问题。

关于我们 32 位代码中发生的事情的一些说明。遵循调用约定,我们需要为调用函数保留我们的堆栈帧(请记住,它正在调用一个它认为有 4 个变量的函数)。调用函数的返回地址位于堆栈底部。所以我们解引用 esp(它指向我们的返回地址)并推入(push [esp])并递减 esp,添加一个包含返回地址的新“层”,从而为我们的第五个变量腾出空间。现在,我们将对象指针值加上 4 个字节推入堆栈(覆盖了原始返回地址的位置),在那里它将成为我们函数调用的第一个值(概念上我们是向右推送了函数参数)。在 Init m_mov 中,给出了 mov dword ptr [esp+0x4] 的十六进制等效值。然后我们将 pThis 的值赋给 m_this 以完成 mov 指令。m_jmp 获取 jmp 操作码的十六进制等效值。现在我们进行一些计算来找到我们需要跳转到的地址,并将其赋给 m_relproc(相对于我们过程的偏移量)。

我们还需要重载我们 struct 的 new 和 delete 运算符,以便将对象正确地分配到我们的可执行堆上。

另请注意,Intel 使用“小端序”格式,因此指令字节必须反转(高位字节在前)[x64 也适用]。

// AppWin.h
#if defined(_M_IX86)
#pragma pack(push,1)
struct winThunk
{
    unsigned short m_push1;    //push dword ptr [esp] ;push return address
    unsigned short m_push2;
    unsigned short m_mov1;     //mov dword ptr [esp+0x4], pThis ;set our new parameter by replacing old return address
    unsigned char  m_mov2;     //(esp+0x4 is first parameter)
    unsigned long  m_this;     //ptr to our object
    unsigned char  m_jmp;      //jmp WndProc
    unsigned long  m_relproc;  //relative jmp
    static HANDLE  eheapaddr;  //heap address this thunk will be initialized to
    bool Init(void* pThis, DWORD_PTR proc)
    {
        m_push1 = 0x34ff; //ff 34 24 push DWORD PTR [esp]
        m_push2 = 0xc724;
        m_mov1  = 0x2444; // c7 44 24 04 mov dword ptr [esp+0x4],
        m_mov2  = 0x04;
        m_this  = PtrToUlong(pThis);
        m_jmp   = 0xe9;  //jmp
        //calculate relative address of proc to jump to
        m_relproc = unsigned long((INT_PTR)proc - ((INT_PTR)this + sizeof(winThunk)));
        // write block from data cache and flush from instruction cache
        if (FlushInstructionCache(GetCurrentProcess(), this, sizeof(winThunk)))
        { //succeeded
            return true;
        }
        else
        {//error
            return false;
        }
    }
    //some thunks will dynamically allocate the memory for the code
    WNDPROC GetThunkAddress()
    {
        return (WNDPROC)this;
    }
    void* operator new(size_t, HANDLE heapaddr)
    {
        eheapaddr = heapaddr; //since we can't pass a value with delete operator, we need to store
                              //our heap address so we can use it later when we need to free this thunk
        return HeapAlloc(heapaddr, 0, sizeof(winThunk));
    }
    void operator delete(void* pThunk)
    {
        HeapFree(eheapaddr, 0, pThunk);
    }
};
#pragma pack(pop)

x64 版本遵循相同的原理,但我们需要考虑 x64 如何处理堆栈的一些差异,并补偿一些对齐问题。Windows x64 ABI 在为函数调用推送变量时使用以下范例(请注意,它不使用 pushpop——它类似于 fastcall)。第一个参数移入 rcx。第二个参数移入 rdx。第三个参数移入 r8。第四个参数移入 r9。接下来的参数被推入堆栈,但这里有个技巧。ABI 在堆栈上为存储这 4 个参数保留空间(称为 shadow space)。因此,在堆栈顶部有四个 8 字节空间。堆栈顶部还有一个返回地址。所以第五个参数放在堆栈的 rsp+28 位置。

--- Bottom of stack ---    RSP + size     (higher addresses)
arg N
arg N - 1
arg N - 2
...
arg 6
arg 5                      [rsp+28h]
(shadow space for arg 4)   [rsp+20h]
(shadow space for arg 3)   [rsp+18h]
(shadow space for arg 2)   [rsp+10h]
(shadow space for arg 1)   [rsp+8h]
(return address)           [rsp]
---- Top of stack -----    RSP            (lower addresses)

对于非静态函数调用,它对前 5 个参数执行以下操作。它将 this 推入 rcx,然后移入 edx(第 1 个参数),然后移入 r8(第 2 个参数),然后移入 r9(第 3 个参数),然后移入 rsp+0x28(第 4 个参数),然后移入 rsp+0x30(第 5 个参数)。对于非静态第 1 个参数到 rcx,然后到 rdx(第 2 个参数),然后到 r8(第 3 个参数),然后到 r9(第 4 个参数),然后到 rsp+0x28(第 5 个参数)。所以我们需要将我们的值放在 rsp+0x28

我们遇到了一个问题,即指令集之一(mov [esp+28], rax)是 5 字节指令,编译器会尝试将所有内容对齐到 1、2、4、8、16 字节边界。所以我们需要进行一些手动对齐。这需要添加一个空操作(nop)[90] 命令。否则,原理是相同的。请注意,由于 pThisproc 的地址是 64 位变量,我们需要使用 movabs 操作数,它使用 rax

#elif defined(_M_AMD64)
#pragma pack(push,2)
struct winThunk
{
    unsigned short     RaxMov;  //movabs rax, pThis
    unsigned long long RaxImm;
    unsigned long      RspMov;  //mov [rsp+28], rax
    unsigned short     RspMov1;
    unsigned short     Rax2Mov; //movabs rax, proc
    unsigned long long ProcImm;
    unsigned short     RaxJmp;  //jmp rax
    static HANDLE      eheapaddr; //heap address this thunk will be initialized too
    bool Init(void *pThis, DWORD_PTR proc)
    {
          RaxMov  = 0xb848;                    //movabs rax (48 B8), pThis
          RaxImm  = (unsigned long long)pThis; //
          RspMov  = 0x24448948;                //mov qword ptr [rsp+28h], rax (48 89 44 24 28)
          RspMov1 = 0x9028;                    //to properly byte align the instruction we add a nop (no operation) (90)
          Rax2Mov = 0xb848;                    //movabs rax (48 B8), proc
          ProcImm = (unsigned long long)proc;
          RaxJmp = 0xe0ff;                     //jmp rax (FF EO)
        if (FlushInstructionCache(GetCurrentProcess(), this, sizeof(winThunk)))
        { //error
            return FALSE;
        }
        else
        {//succeeded
            return TRUE;
        }
    }
    //some thunks will dynamically allocate the memory for the code
    WNDPROC GetThunkAddress()
    {
        return (WNDPROC)this;
    }
    void* operator new(size_t, HANDLE heapaddr)
    {
        eheapaddr = heapaddr; //since we can't pass a value with delete operator we need to store
                              //our heap address so we can use it later when we need to free this thunk
        return HeapAlloc(heapaddr, 0, sizeof(winThunk));
    }
    void operator delete(void* pThunk)
    {
        HeapFree(eheapaddr, 0, pThunk);
    }
};
#pragma pack(pop)
#endif

现在我们有了消息处理程序和我们的 thunk。我们可以将值赋给 lpfnWndProc

警告 - 我们使用了两个不同的调用参数,一个用于 32 位,一个用于 64 位。在我们的 32 位代码中,指针是第一个参数。在我们的 64 位代码中,它是第五个参数。我们需要通过将代码包装在一些编译器指令中来解决这个问题。

AppWin.h
#if defined(_M_IX86)
    static LRESULT CALLBACK WndProc(DWORD_PTR, HWND, UINT, WPARAM, LPARAM);
#elif defined(_M_AMD64)
    static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM, DWORD_PTR);
#endif
AppWin.cpp
#if defined(_M_IX86)
LRESULT CALLBACK AppWinClass::WndProc(DWORD_PTR This, HWND hWnd, 
                                      UINT message, WPARAM wParam, LPARAM lParam)
#elif defined(_M_AMD64)
LRESULT CALLBACK AppWinClass::WndProc(HWND hWnd, UINT message, 
                                      WPARAM wParam, LPARAM lParam, DWORD_PTR This)
#endif

lpfnWndProc 将指向我们的 thunk 而不是我们的消息处理函数。所以我们用正确的值初始化我们的 thunk

//AppWin.cpp
int AppWinClass::Create(HINSTANCE hInstance, int nCmdShow, AppWinStruct* varStruct)
{
    thunk->Init(this, (DWORD_PTR)WndProc); //init our thunk

一些 thunks 可能会动态分配内存,所以我们使用 GetThunkAddress 函数,它只是返回 thunk 的实际 this 指针。我们将调用强制转换为 WNDPROC,因为这是我们的窗口类所期望的。

现在我们注册我们的 WNDCLASSEX 结构。我们将声明一个 public 变量 classatom 来保存 RegisterClassEx 的返回值,以便将来使用。然后我们调用 RegisterClassEx

现在我们调用 CreateWindowEx 并传递变量。如果设置了 WS_VISIBLE 位,则无需调用 ShowWindow,因此我们对此进行检查。我们执行 UpdateWindow,然后进入消息循环。完成。

*一个额外的说明。我在 WndProc 声明中使用了 DWORD_PTR This。在我看来,这有助于更好地演示原理。然而,为了避免无用的转换,请将其声明为 AppWinClass This

AppWin.h
// AppWin.h : header file for the AppWinClass
//

#pragma once

#include "resource.h"

#if defined(_M_IX86)
#pragma pack(push,1)
struct winThunk
{
    unsigned short m_push1;    //push dword ptr [esp] ;push return address
    unsigned short m_push2;
    unsigned short m_mov1;     //mov dword ptr [esp+0x4], pThis ;set our new parameter by replacing old return address
    unsigned char  m_mov2;     //(esp+0x4 is first parameter)
    unsigned long  m_this;     //ptr to our object
    unsigned char  m_jmp;      //jmp WndProc
    unsigned long  m_relproc;  //relative jmp
    static HANDLE  eheapaddr;  //heap address this thunk will be initialized to
    bool Init(void* pThis, DWORD_PTR proc)
    {
        m_push1 = 0x34ff; //ff 34 24 push DWORD PTR [esp]
        m_push2 = 0xc724;
        m_mov1  = 0x2444; // c7 44 24 04 mov dword ptr [esp+0x4],
        m_mov2  = 0x04;
        m_this  = PtrToUlong(pThis);
        m_jmp   = 0xe9;  //jmp
        //calculate relative address of proc to jump to
        m_relproc = unsigned long((INT_PTR)proc - ((INT_PTR)this + sizeof(winThunk)));
        // write block from data cache and flush from instruction cache
        if (FlushInstructionCache(GetCurrentProcess(), this, sizeof(winThunk)))
        { //succeeded
            return TRUE; 
        }
        else { //error
             return FALSE;
        }
     }
     //some thunks will dynamically allocate the memory for the code
     WNDPROC GetThunkAddress() 
     { 
        return (WNDPROC)this;
     }
     void* operator new(size_t, HANDLE heapaddr)
     {
        eheapaddr = heapaddr;
        //since we can't pass a value with delete operator we need to store
        //our heap address so we can use it later when we need to free this thunk
        return HeapAlloc(heapaddr, 0, sizeof(winThunk));
      }
      void operator delete(void* pThunk)
      {
        HeapFree(eheapaddr, 0, pThunk);
      }
 };
#pragma pack(pop)
#elif defined(_M_AMD64)
#pragma pack(push,2)
struct winThunk
{
    unsigned short     RaxMov;  //movabs rax, pThis
    unsigned long long RaxImm;
    unsigned long      RspMov;  //mov [rsp+28], rax
    unsigned short     RspMov1;
    unsigned short     Rax2Mov; //movabs rax, proc
    unsigned long long ProcImm;
    unsigned short     RaxJmp;  //jmp rax
    static HANDLE      eheapaddr; //heap address this thunk will be initialized too
    bool Init(void *pThis, DWORD_PTR proc)
    {
          RaxMov  = 0xb848;                    //movabs rax (48 B8), pThis
          RaxImm  = (unsigned long long)pThis; //
          RspMov  = 0x24448948;                //mov qword ptr [rsp+28h], rax (48 89 44 24 28)
          RspMov1 = 0x9028;                    //to properly byte align the instruction we add a nop (no operation) (90)
          Rax2Mov = 0xb848;                    //movabs rax (48 B8), proc
          ProcImm = (unsigned long long)proc;
          RaxJmp = 0xe0ff;                     //jmp rax (FF EO)
            if (FlushInstructionCache(GetCurrentProcess(), this, sizeof(winThunk)))
            { //error
               return FALSE;
            }
            else
            {//succeeded
               return TRUE;
        }
    }
    //some thunks will dynamically allocate the memory for the code
    WNDPROC GetThunkAddress()
    {
        return (WNDPROC)this;
    }
    void* operator new(size_t, HANDLE heapaddr)
    {
        return HeapAlloc(heapaddr, 0, sizeof(winThunk));
    }
    void operator delete(void* pThunk, HANDLE heapaddr)
    {
        HeapFree(heapaddr, 0, pThunk);
    }
};
#pragma pack(pop)
#endif

struct AppWinStruct {
    //structure to hold variables used to instantiate the window
    LPCTSTR lpszClassName = L"Window";
    LPCTSTR lpClassName   = L"Window";
    LPCTSTR lpWindowName  = L"Window";
    DWORD     dwExStyle       = WS_EX_OVERLAPPEDWINDOW;
    DWORD    dwStyle       = WS_OVERLAPPEDWINDOW | WS_VISIBLE;
    UINT     style           = CS_HREDRAW | CS_VREDRAW;
    int     cbClsExtra       = 0;
    int     cbWndExtra       = 0;
    HICON     hIcon           = LoadIcon(nullptr, IDI_APPLICATION);
    HCURSOR hCursor       = LoadCursor(nullptr, IDC_ARROW);
    HBRUSH     hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    LPCTSTR lpszMenuName  = nullptr;
    HICON     hIconSm       = NULL;
    int     xpos           = CW_USEDEFAULT;
    int     ypos           = CW_USEDEFAULT;
    int     nWidth           = CW_USEDEFAULT;
    int     nHeight       = CW_USEDEFAULT;
    HWND     hWndParent       = NULL;
    HMENU     hMenu           = NULL;
    LPVOID     lpParam       = NULL;
};

class AppWinClass {
public:
    
    ATOM classatom = NULL;

    AppWinClass(); //constructor
    ~AppWinClass(); //descructor

    int Create(HINSTANCE, int, AppWinStruct*);
    int GetMsg(HINSTANCE);

private:
    static HANDLE eheapaddr;
    static int objInstances;
    winThunk* thunk;
#if defined(_M_IX86)
    static LRESULT CALLBACK WndProc(DWORD_PTR, HWND, UINT, WPARAM, LPARAM);
#elif defined(_M_AMD64)
    static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM, DWORD_PTR);
#endif
};
AppWin.cpp
// AppWin.cpp : implementation of AppWinClass
//

#include "stdafx.h"
#include "AppWin.h"

HANDLE AppWinClass::eheapaddr = NULL;
int    AppWinClass::objInstances = 0;

AppWinClass::AppWinClass()
{
    objInstances = ++objInstances;
    if (!eheapaddr)
    {        
        try {
            eheapaddr = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE | HEAP_GENERATE_EXCEPTIONS, 0, 0);
        }
        catch (...) {
            throw;
        }
    }
    try {
        thunk = new(eheapaddr) winThunk;
    }
    catch (...) {
        throw;
    }
}

AppWinClass::~AppWinClass() {
    if (objInstances == 1)
    {
        HeapDestroy(eheapaddr);
        eheapaddr = NULL;
        objInstances = 0;
    }
    else
    {
        objInstances = --objInstances;
    }
}

int AppWinClass::Create(HINSTANCE hInstance, int nCmdShow, AppWinStruct* varStruct)
{
    HWND hWnd = NULL;
    DWORD showwin = NULL;
    thunk->Init(this, (DWORD_PTR)WndProc); //init our thunk
    WNDPROC pProc = thunk->GetThunkAddress(); //get our thunk's address 
                                                 //and assign it pProc (pointer to process)
    WNDCLASSEX wcex; //initialize our WNDCLASSEX
    wcex.cbSize      = sizeof(WNDCLASSEX);
    wcex.hInstance      = hInstance;
    wcex.lpfnWndProc = pProc; //our thunk
    if (!varStruct) //default values
    {
        varStruct = new AppWinStruct;
        wcex.style            = CS_HREDRAW | CS_VREDRAW;
        wcex.cbClsExtra    = 0;
        wcex.cbWndExtra    = 0;
        wcex.hIcon            = LoadIcon(nullptr, IDI_APPLICATION);
        wcex.hCursor        = LoadCursor(nullptr, IDC_ARROW);
        wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
        wcex.lpszMenuName  = nullptr;
        wcex.lpszClassName = L"Window";
        wcex.hIconSm        = NULL;
        //register wcex
        classatom = RegisterClassEx(&wcex);
        //create our window
        hWnd = CreateWindowEx(WS_EX_OVERLAPPEDWINDOW, L"Window", 
               L"Window", WS_OVERLAPPEDWINDOW | WS_VISIBLE,
               CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 
               nullptr, nullptr, hInstance, nullptr);
        showwin = WS_VISIBLE; //we set WS_VISIBLE so we do not need to call ShowWindow
    }
    else
    { //user defined
        wcex.style            = varStruct->style;
        wcex.cbClsExtra    = varStruct->cbClsExtra;
        wcex.cbWndExtra    = varStruct->cbWndExtra;
        wcex.hIcon            = varStruct->hIcon;
        wcex.hCursor        = varStruct->hCursor;
        wcex.hbrBackground = varStruct->hbrBackground;
        wcex.lpszMenuName  = varStruct->lpszMenuName;
        wcex.lpszClassName = varStruct->lpszClassName;
        wcex.hIconSm        = varStruct->hIconSm;
        //register wcex
        classatom = RegisterClassEx(&wcex);
        //create our window
        hWnd = CreateWindowEx(varStruct->dwExStyle, varStruct->lpClassName, 
               varStruct->lpWindowName, varStruct->dwStyle,
               varStruct->xpos, varStruct->ypos, varStruct->nWidth,  
               varStruct->nHeight, varStruct->hWndParent, varStruct->hMenu,
               hInstance, varStruct->lpParam);
        showwin = (varStruct->dwStyle & (WS_VISIBLE)); //check if the WS_VISIBLE bit was set
    }
    if (!hWnd)
    {
        return FALSE;
    }
    //check if the WS_VISIBLE style bit was set and if so we don't need to call ShowWindow
    if (showwin != WS_VISIBLE)
    {
        ShowWindow(hWnd, nCmdShow);
    }
    UpdateWindow(hWnd);
    return 0;

}
#if defined(_M_IX86)
LRESULT CALLBACK AppWinClass::WndProc
(DWORD_PTR This, HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
#elif defined(_M_AMD64)
LRESULT CALLBACK AppWinClass::WndProc
(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, DWORD_PTR This)
#endif
{
    AppWinClass* pThis = (AppWinClass*)This;
    switch (message)
    {
    case WM_PAINT:
    {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hWnd, &ps);
        // TODO: Add any drawing code that uses hdc here...
        EndPaint(hWnd, &ps);
    }
    break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

int AppWinClass::GetMsg(HINSTANCE hInstance)
{
    HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_APPWIN));

    MSG msg;

    // Main message loop:
    while (GetMessage(&msg, nullptr, 0, 0))
    {
        if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

    return (int)msg.wParam;
}

历史

  • 版本 1.1
    • 修正了最后一个对象实例上没有将 objInstances 设置回零的问题。添加了两个注释。
  • 版本 1.5
    • 根据 pVerer 在评论中的建议,更改了 thunk 的 32 位约定以正确保留堆栈——这导致需要将 WndProc 函数包装在一些 #if 定义的约定中,因为 32 位和 64 位代码现在以不同的方式调用此函数。
  • 1.6 版
    • 对 thunk 的 delete 运算符进行了一些小的修改。
  • 1.8 版
    • 更新了 x64 ABI 部分,以更正确、更清晰地讨论约定。其描述存在一个错误。还更新了 x64 thunk 代码,并使其更简单,使用了 movabs 操作数,它适用于 64 位立即数。
  • 版本 1.8.1
    • 更新了 thunk 部分,进行了一个小编辑以支持 VS 2017——将 Microsoft 类型定义 USHORT、ULONG 等更改为 C++ 的正确类型(即 USHORT 变为 unsigned short),否则代码在由 VS 2017 RC 编译后将无法正确执行。
© . All rights reserved.