Thunk
在C和C++中函数调用一般是通过压栈的方式实现参数传递。函数的调用有很多种约定,以__cdecl为例[^1], 编译器将按照如下的约定去解析这个函数声明.
- 函数将按照从右向左的顺序将参数压入到栈中;
- 函数调用返回之后,调用者负责清除栈中的压入的元素;
- 编译器将会在函数名前加一个"_"并导入到symbol表中;
int __cdecl sumExample (int a, int b);
总而言之, Function可以看为一段代码,当调用这段代码时会做一些事情,然后返回继续执行被调用者的代码; 而Thunk也是一段代码,这段代码也可以跟函数一样被调用,做一些事情,但是它会jmp到另外一个地址而非返回到被调用者。假设jmp到的地址是一个正常的函数,在执行完这个函数return的时候,会直接返回到Thunk的调用者。Thunk一般用汇编直接写成,可以用来实现一些非常有意思的功能,譬如.
- Protocol translation 譬如如当使用thiscall调用一段__stdcall的代码的时候,需要对参数的位置做一些调整,可以使用Thunk来完成;
- virtual function handling 在深入探索c++对象模型中已经描述的很清楚了,这也是虚拟继承实现的方法;
- Dynamic closures 在ATL中广泛使用这种方式支持将一个类的非静态方法做为callback传递给Win32的系统API(譬如窗口类的处理程序和Timer的callback)。其主要解决的问题是当我们把一个类中的非静态成员函数做为回调函数的时候,这些函数函数的地址可能是动态绑定的,而回调函数需要的是一个静态的地址, 而使用Thunk可以解决这个问题。
Sample
下面这两个gist 演示了Thunk的基本用法. 其实现分为三部分.
https://gist.github.com/miaoxingman/7dc69c409d7a77e160b6cf3cb4684748
https://gist.github.com/miaoxingman/056a9e4e4b32cf3028af864e416b1172
-
首先定义一个结构体,这个结构体将被存储在数据段但是添加可执行的属性。在Windows10上面需要特殊的API实现这种转化,也可以在编译的时候添加/NXCOMPACT将Data Execution Prevention关掉。
#pragma pack(push,1) struct _WndProcThunk { DWORD m_mov; // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd) DWORD m_this; BYTE m_jmp; // jmp WndProc DWORD m_relproc; // relative jmp }; #pragma pack(pop)
-
添加一个初始化函数,当调用这个函数的时候,会在Thunk中存储类相关的Context.
void Init(WNDPROC proc, void* pThis) { thunk.m_mov = 0x042444C7; //C7 44 24 04 thunk.m_this = (DWORD)pThis; thunk.m_jmp = 0xe9; thunk.m_relproc = (int)proc - ((int)this+sizeof(_WndProcThunk)); ::FlushInstructionCache(GetCurrentProcess(), &thunk, sizeof(thunk)); }
-
将windows的callback设置为一个静态的成员变量,在其中根据不同的对象调用上述初始化成员函数初始化对象中的Thunk,然后使用SetWindowLong(GWL_WNDPROC)将Thunk注册为instance的消息处理函数.
static LRESULT CALLBACK StartWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { ZWindow* pThis = g_pWnd; pThis->m_hWnd = hWnd; // initilize the thunk code pThis->m_thunk.Init(WindowProc, pThis); // get the address of thunk code WNDPROC pProc = (WNDPROC)&(pThis->m_thunk.thunk); ::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pProc); return pProc(hWnd, uMsg, wParam, lParam); }
Reference
-
关于__cdecl, __stdcall, __fastcall, WINAPI这些Calling Conventions的区别,推荐这篇code project的文章.
https://www.codeproject.com/Articles/1388/Calling-Conventions-Demystified
-
More about Data Execution Prevention
https://msdn.microsoft.com/en-us/library/windows/desktop/aa366553(v=vs.85).aspx