今天由于工作需要,研究了下thunk技术,下面的内容算是个小小的总结吧。
我们知道,在Windows和x86下,函数调用主要有以下几种方式:
- __stdcall
这是Windows下默认的函数调用方式。调用者负责把函数参数从右到左压入堆栈,函数在返回前负责清理堆栈。用汇编语言表示就是:
调用方: mov esi, esp ;保存ESP push .... ;把参数压栈 call fun ;调用函数 cmp esi, esp ;检查堆栈 call xxxxxxxx ;如果堆栈溢出,发生异常 ..... ;继续执行 函数: ..... ;函数代码 ret ? ;函数返回,同时清理堆栈 - _cdecl
这是C/C++默认的函数调用方式。调用者负责把函数参数从右到左压入堆栈;函数在返回前不负责清理堆栈,调用者在函数返回后清理堆栈,这种调用方式允许函数有不定参数。用汇编语言表示就是:
调用方: mov esi, esp ;保存ESP push .... ;把参数压栈 call fun ;调用函数 add esp ? ;清理堆栈 cmp esi, esp ;检查堆栈 call xxxxxxxx ;如果堆栈溢出,发生异常 ..... ;继续执行 函数: ..... ;函数代码 ret ;函数返回 - thiscall
这是C++成员函数默认的调用方式。调用者负责把函数参数从左到右压入堆栈,同时把this指针放入ECX寄存器;函数在返回前清理堆栈。用汇编语言表示就是:
调用方: mov esi, esp ;保存ESP push .... ;把参数压栈 mov ecx, this ;传递this call fun ;调用函数 cmp esi, esp ;检查堆栈 call xxxxxxxx ;如果堆栈溢出,发生异常 ..... ;继续执行 函数: ..... ;函数代码 ret ? ;函数返回,同时清理堆栈 - __fastcall
这种函数调用方式是这样的:调用者把第一个参数放入ECX,第二个参数(如果有的话)放入EDX,其他参数压入堆栈。函数在返回前清理堆栈。
调用方: mov esi, esp ;保存ESP push ecx, arg1 ;传递参数1 call fun ;调用函数 cmp esi, esp ;检查堆栈 call xxxxxxxx ;如果堆栈溢出,发生异常 ..... ;继续执行 函数: ..... ;函数代码 ret ? ;函数返回,同时清理堆栈
在C++下,成员函数前也可以通过使用_cdecl、__stdcall或者__fastcall修饰改变函数调用方式,这种情况下把this指针做第一个参数处理。如果成员函数有不定参数,编译器也会做为_cdecl来调用。各种函数调用方式在函数名修饰等方面还有区别,因为这里主要讨论thunk,就不说了。
我们在这里利用thunk技术实现在C函数中回调C++成员函数。考虑到调用函数和回调函数之间几种调用方式的组合,有以下几种情形,它们需要的thunnk代码是不同的。
- __stdcall回调stdcall
- _cdecl回调_cdecl
这2种函数调用的特点是函数和成员函数的调用方式一致,只要根据成员函数的调用函数把this指针保存在适当的方式就可以了。当然如果没有适当的地方保存this指针,那么thunk就没法实现了。对于stdcall和_cdecl来说,this指针做为第一个参数被传递。有2种情况:
(1)回调函数的第一个参数可以被替换成this指针,ATL中实现CWindowImpl的thunk就属于这种情况。因为窗口过程的第一个参数HWND在窗口类中做为数据成员m_hWnd保存,所以它对于成员函数来说是没有用的。ATL用this指针替换了窗口过程的HWND参数,把成员函数传递给了SetWindowLong实现窗口子类化。这种方式的thunk是最容易的。
(2)回调函数的第一个参数不能被替换成this指针,那么thunk就要把this指针压入堆栈,这样调整堆栈的操作就复杂多了。而且,对于stdcall和_cdecl,在实现上又有所不同,因为_cdecl在回调结束后还要调整堆栈以保证正确返回原来的函数。C连函数调用都这么麻烦,所以总有人说C太难学了。 - __stdcall回调thiscall
- _cdecl回调thiscall
这2种函数调用的thunk的特点是要想办法把this指针保存到ECX中,而不是替换原来函数调用参数。因为_cdecl是由调用者清理堆栈,所以在实现thunk时还要在函数返回后调整堆栈,要比__stdcall要复杂得多。
其他的回调方式用的就很少了,我就不去研究了。