您的位置:首页 > 其它

学习下 WTL 的 thunk

2014-12-14 21:32 204 查看
原文:http://www.cppblog.com/Streamlet/archive/2010/10/24/131064.html

由于 C++ 成员函数的调用机制问题,对C语言回调函数的 C++ 封装是件比较棘手的事。为了保持C++对象的独立性,理想情况是将回调函数设置到成员函数,而一般的回调函数格式通常是普通的C函数,尤其是 Windows API 中的。好在有些回调函数中留出了一个额外参数,这样便可以由这个通道将 this 指针传入。比如线程函数的定义为:

typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(

LPVOID lpThreadParameter

);

typedef PTHREAD_START_ROUTINE LPTHREAD_START_ROUTINE;

这样,当我们实现线程类的时候,就可以:

class Thread

{

private:

HANDLE m_hThread;

public:

BOOL Create()

{

m_hThread = CreateThread(NULL, 0, StaticThreadProc, (LPVOID)this, 0, NULL);

return m_hThread != NULL;

}

private:

DWORD WINAPI ThreadProc()

{

// TODO

return 0;

}

private:

static DWORD WINAPI StaticThreadProc(LPVOID lpThreadParameter)

{

((Thread *)lpThreadParameter)->ThreadProc();

}

};

不过,这样,成员函数 ThreadProc() 便丧失了一个参数,这通常无伤大雅,任何原本需要从参数传入的信息都可以作为成员变量让 ThreadProc 来读写。如果一定有些什么是非从参数传入不可的,那也可以,一种做法,创建线程的时候传入一个包含 this 指针信息的结构。第二种做法,对该 class 作单例限制——如果现实情况允许的话。

所以,有额外参数的回调函数都好处理。不幸的是,Windows 的窗口回调函数没有这样一个额外参数:

typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);

这使得对窗口的 C++ 封装变得困难。为了解决这个问题,一个很自然的想法是,维护一份全局的窗口句柄到窗口类的对应关系,如:

#include <map>

class Window

{

public:

Window();

~Window();

public:

BOOL Create();

protected:

LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam);

protected:

HWND m_hWnd;

protected:

static LRESULT CALLBACK StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

static std::map<HWND, Window *> m_sWindows;

};

在 Create 的时候,指定 StaticWndProc 为窗口回调函数,并将 hWnd 与 this 存入 m_sWindows:

BOOL Window::Create()

{

LPCTSTR lpszClassName = _T("ClassName");

HINSTANCE hInstance = GetModuleHandle(NULL);

WNDCLASSEX wcex = { sizeof(WNDCLASSEX) };

wcex.lpfnWndProc = StaticWndProc;

wcex.hInstance = hInstance;

wcex.lpszClassName = lpszClassName;

RegisterClassEx(&wcex);

m_hWnd = CreateWindow(lpszClassName, NULL, WS_OVERLAPPEDWINDOW,

CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

if (m_hWnd == NULL)

{

return FALSE;

}

m_sWindows.insert(std::make_pair(m_hWnd, this));

ShowWindow(m_hWnd, SW_SHOW);

UpdateWindow(m_hWnd);

return TRUE;

}

在 StaticWindowProc 中,由 hWnd 找到 this,然后转发给成员函数:

LRESULT CALLBACK Window::StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

{

std::map<HWND, Window *>::iterator it = m_sWindows.find(hWnd);

assert(it != m_sWindows.end() && it->second != NULL);

return it->second->WndProc(message, wParam, lParam);

}

(m_sWindows 的多线程保护略过,下同)

据说 MFC 采用的就是类似的做法。缺点是,每次 StaticWndProc 都要从 m_sWindows 中去找 this。由于窗口类一般会保存窗口句柄,回调函数里的 hWnd 就没多大作用了,如果这个 hWnd 能够被用来存 this 指针就好了,那么就能写成这样:

LRESULT CALLBACK Window::StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

{

return ((Window *)hWnd)->WndProc(message, wParam, lParam);

}

这样看上去就爽多了。传说中 WTL 所采取的 thunk 技术就是这么干的。之前,只是听过这遥远的传说,今天,终于有机会走进这个传说去看一看。参考资料是一篇不知原始出处的文章《深入剖析WTL—WTL框架窗口分析》,以及部分 WTL 8.0 代码,还有其他乱七八糟的文章。

WTL 的思路是,每次在系统调用 WndProc 的时候,让它鬼使神差地先走到我们的另一处代码,让我们有机会修改堆栈中的 hWnd。这处代码可能是类似这样的:

__asm

{

mov dword ptr [esp+4], pThis ;调用 WndProc 时,堆栈结构为:RetAddr, hWnd, message, wParam, lParam, ... 故 [esp+4]

jmp WndProc

}

由于 pThis 和 WndProc 需要被事先修改(但又无法在编译前定好),所以我们需要运行的时候去修改这部分代码。先弄一个小程序探测下这两行语句的机器码:

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

{

return 0;

}

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)

{

MessageBox(NULL, NULL, NULL, MB_OK);

__asm

{

mov dword ptr [esp+4], 1

jmp WndProc

}

return 0;

}

最前面的 MessageBox 是为了等下调试的时候容易找到进入点。

然后使用 OllyDbg,在 MessageBoxW 上设置断点,执行到该函数返回:





这里我们看到,mov dword ptr [esp+4] 的机器码为 C7 44 24 04,后面紧接着的一个 DWORD 是 mov 的第二个操作数。jmp 的机器码是 e9,后面紧接着的一个 DWORD 是跳转的相对地址。其中 00061000h - 0006102Bh = FFFFFFD5h。

于是定义这样一个结构:

#pragma pack(push,1)

typedef struct _StdCallThunk

{

DWORD m_mov; // = 0x042444C7

DWORD m_this; // = this

BYTE m_jmp; // = 0xe9

DWORD m_relproc; // = relative distance

} StdCallThunk;

#pragma pack(pop)

这个结构可以作为窗口类的成员变量存在。我们的窗口类现在变成了这样子:

class Window

{

public:

Window();

~Window();

public:

BOOL Create();

protected:

LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam);

protected:

HWND m_hWnd;

StdCallThunk m_thunk;

protected:

static LRESULT CALLBACK StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

};

似乎少了点什么……创建窗口的时候,我们是不能直接把回调函数设到 StaticWndPorc 中去的,因为这个函数是希望被写成这样子的:

LRESULT CALLBACK Window::StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

{

return ((Window *)hWnd)->WndProc(message, wParam, lParam);

}

那么至少需要一个临时的回调函数,在这个函数里去设置新的回调函数(设到 m_thunk 上),再由 m_thunk 来调用 StaticWndProc,StaticWndProc 再去调用 WndProc,这样整个过程就通了。

但是,临时回调函数还是需要知道从 hWnd 到 this 的对应关系。可是现在我们不能照搬用刚才的 m_sWindows 了。因为窗口在创建过程中就会调用到回调函数,需要使用到 m_sWindows 里的 this,而窗口被成功创建之前,我们没法提前拿到 HWND 存入 m_sWindows。现在,换个方法,存当前线程 ID 与 this 的对应关系。这样,这个类变成了:

#include <map>

class Window

{

public:

Window();

~Window();

public:

BOOL Create();

protected:

LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam);

protected:

HWND m_hWnd;

StdCallThunk m_thunk;

protected:

static LRESULT CALLBACK TempWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);


static LRESULT CALLBACK StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

static std::map<DWORD, Window *> m_sWindows;

};

然后实现 Create 和 TempWndProc:

BOOL Window::Create()

{

LPCTSTR lpszClassName = _T("ClassName");

HINSTANCE hInstance = GetModuleHandle(NULL);

WNDCLASSEX wcex = { sizeof(WNDCLASSEX) };

wcex.lpfnWndProc = TempWndProc;

wcex.hInstance = hInstance;

wcex.lpszClassName = lpszClassName;

RegisterClassEx(&wcex);

DWORD dwThreadId = GetCurrentThreadId();

m_sWindows.insert(std::make_pair(dwThreadId, this));

m_thunk.m_mov = 0x042444c7;

m_thunk.m_jmp = 0xe9;


m_hWnd = CreateWindow(lpszClassName, NULL, WS_OVERLAPPEDWINDOW,

CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

if (m_hWnd == NULL)

{

return FALSE;

}

ShowWindow(m_hWnd, SW_SHOW);

UpdateWindow(m_hWnd);

return TRUE;

}

LRESULT CALLBACK Window::TempWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

{

std::map<DWORD, Window *>::iterator it = m_sWindows.find(GetCurrentThreadId());

assert(it != m_sWindows.end() && it->second != NULL);

Window *pThis = it->second;

m_sWindows.erase(it);

WNDPROC pWndProc = (WNDPROC)&pThis->m_thunk;

pThis->m_thunk.m_this = (DWORD)pThis;

pThis->m_thunk.m_relproc = (DWORD)&Window::StaticWndProc - ((DWORD)&pThis->m_thunk + sizeof(StdCallThunk));


m_hWnd = hWnd;

SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pWndProc);

return pWndProc(hWnd, message, wParam, lParam);

}

差不多可以了,调试一下。结果,在 thunk 的第一行出错了。我原以为地址算错了神马的,尝试把 thunk.m_mov 改为 0x90909090,再运行,还是出错。于是傻掉了……过了好一会儿才意识到,可能是因为 thunk 在数据段,无法被执行。可是,很久很久以前偶滴一个敬爱的老师在 TC 中鼓捣程序运行时改变自身代码时,貌似无此问题啊。。。然后查呀查,原来是 Windows 在的数据执行保护搞的鬼。于是,需要用 VirtualAlloc 来申请一段有执行权限的内存。WTL 里面也是这么做的,不过它似乎维护了一块较大的可执行内存区作为
thunk 内存池,我们这里从简。最后,整个流程终于跑通了。最终代码清单如下:
#include <Windows.h>

#include <assert.h>

#include <map>

#include <tchar.h>

#pragma pack(push,1)

typedef struct _StdCallThunk

{

DWORD m_mov;

DWORD m_this;

BYTE m_jmp;

DWORD m_relproc;

} StdCallThunk;

#pragma pack(pop)

class Window

{

public:

Window();

~Window();

public:

BOOL Create();

protected:

LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam);

protected:

HWND m_hWnd;

StdCallThunk *m_pThunk;

protected:

static LRESULT CALLBACK TempWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

static LRESULT CALLBACK StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

static std::map<DWORD, Window *> m_sWindows;

};

std::map<DWORD, Window *> Window::m_sWindows;

Window::Window()

{

}

Window::~Window()

{

VirtualFree(m_pThunk, sizeof(StdCallThunk), MEM_RELEASE);

}

BOOL Window::Create()

{

LPCTSTR lpszClassName = _T("ClassName");

HINSTANCE hInstance = GetModuleHandle(NULL);

WNDCLASSEX wcex = { sizeof(WNDCLASSEX) };

wcex.lpfnWndProc = TempWndProc;

wcex.hInstance = hInstance;

wcex.lpszClassName = lpszClassName;

wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);

RegisterClassEx(&wcex);

DWORD dwThreadId = GetCurrentThreadId();

m_sWindows.insert(std::make_pair(dwThreadId, this));

m_pThunk = (StdCallThunk *)VirtualAlloc(NULL, sizeof(StdCallThunk), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

m_pThunk->m_mov = 0x042444c7;

m_pThunk->m_jmp = 0xe9;

m_hWnd = CreateWindow(lpszClassName, NULL, WS_OVERLAPPEDWINDOW,

CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

if (m_hWnd == NULL)

{

return FALSE;

}

ShowWindow(m_hWnd, SW_SHOW);

UpdateWindow(m_hWnd);

return TRUE;

}

LRESULT Window::WndProc(UINT message, WPARAM wParam, LPARAM lParam)

{

switch (message)

{

case WM_LBUTTONUP:

MessageBox(m_hWnd, _T("LButtonUp"), _T("Message"), MB_OK | MB_ICONINFORMATION);

break;

case WM_RBUTTONUP:

MessageBox(m_hWnd, _T("RButtonUp"), _T("Message"), MB_OK | MB_ICONINFORMATION);

break;

case WM_DESTROY:

PostQuitMessage(0);

break;

default:

break;

}

return DefWindowProc(m_hWnd, message, wParam, lParam);

}

LRESULT CALLBACK Window::TempWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

{

std::map<DWORD, Window *>::iterator it = m_sWindows.find(GetCurrentThreadId());

assert(it != m_sWindows.end() && it->second != NULL);

Window *pThis = it->second;

m_sWindows.erase(it);

WNDPROC pWndProc = (WNDPROC)pThis->m_pThunk;

pThis->m_pThunk->m_this = (DWORD)pThis;

pThis->m_pThunk->m_relproc = (DWORD)&Window::StaticWndProc - ((DWORD)pThis->m_pThunk + sizeof(StdCallThunk));

pThis->m_hWnd = hWnd;

SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pWndProc);

return pWndProc(hWnd, message, wParam, lParam);

}

LRESULT CALLBACK Window::StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

{

return ((Window *)hWnd)->WndProc(message, wParam, lParam);

}

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)

{

Window wnd;

wnd.Create();

MSG msg;

while (GetMessage(&msg, NULL, 0, 0))

{

TranslateMessage(&msg);

DispatchMessage(&msg);

}

return (int)msg.wParam;

}

刚才有一处,存 this 指针的时候,我很武断地把它与当前线程 ID 关联起来了,其实这正是 WTL 本身的做法。它用 CAtlWinModule::AddCreateWndData 存的 this,最终会把当前线程 ID 和 this 作关联。我是这么理解的吧,同一线程不可能同时有两处在调用 CreateWindow,所以这样取回来的 this 是可靠的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: