Windows下利用thunk实现回调成员函数(1)

今天由于工作需要,研究了下thunk技术,下面的内容算是个小小的总结吧。

我们知道,在Windows和x86下,函数调用主要有以下几种方式:

  1. __stdcall

    这是Windows下默认的函数调用方式。调用者负责把函数参数从右到左压入堆栈,函数在返回前负责清理堆栈。用汇编语言表示就是:

    调用方:
    mov esi, esp;保存ESP
    push ....;把参数压栈
    call fun;调用函数
    cmp esi, esp;检查堆栈
    call xxxxxxxx;如果堆栈溢出,发生异常
    .....;继续执行
    函数:
    .....;函数代码
    ret ?;函数返回,同时清理堆栈

  2. _cdecl

    这是C/C++默认的函数调用方式。调用者负责把函数参数从右到左压入堆栈;函数在返回前不负责清理堆栈,调用者在函数返回后清理堆栈,这种调用方式允许函数有不定参数。用汇编语言表示就是:

    调用方:
    mov esi, esp;保存ESP
    push ....;把参数压栈
    call fun;调用函数
    add esp ?;清理堆栈
    cmp esi, esp;检查堆栈
    call xxxxxxxx;如果堆栈溢出,发生异常
    .....;继续执行
    函数:
    .....;函数代码
    ret;函数返回
  3. thiscall

    这是C++成员函数默认的调用方式。调用者负责把函数参数从左到右压入堆栈,同时把this指针放入ECX寄存器;函数在返回前清理堆栈。用汇编语言表示就是:

    调用方:
    mov esi, esp;保存ESP
    push ....;把参数压栈
    mov ecx, this;传递this
    call fun;调用函数
    cmp esi, esp;检查堆栈
    call xxxxxxxx;如果堆栈溢出,发生异常
    .....;继续执行
    函数:
    .....;函数代码
    ret ?;函数返回,同时清理堆栈
  4. __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代码是不同的。

  1. __stdcall回调stdcall
  2. _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太难学了。

  3. __stdcall回调thiscall
  4. _cdecl回调thiscall

    这2种函数调用的thunk的特点是要想办法把this指针保存到ECX中,而不是替换原来函数调用参数。因为_cdecl是由调用者清理堆栈,所以在实现thunk时还要在函数返回后调整堆栈,要比__stdcall要复杂得多。

其他的回调方式用的就很少了,我就不去研究了。

 

如qsort 等函数需要函数指针才能回调 用此函数库可以将成员函数指针转为普通函数指针 测试代码如下 #include <stdio.h> #include <algorithm> #include <vector> #include <string> #include <iostream> #include <math.h> using cmpfunc = int(__cdecl*)(const void*, const void*); using DebugArrayFunc = void(__stdcall *)(std::string &out;); #include "thunk.h" class MySort { public: int Rule; MySort(int a):Rule(a){} // 回调函数 template<typename T> int __cdecl sort(const void* a, const void* b); }; class Test { public: std::vector<int> mm; void Sort(int (*comp)(const void *,const void *)) { return qsort(mm._Myfirst,mm.size(),sizeof(int),comp); } void Entry(DebugArrayFunc func) { std::string string; cmpfunc comp; TemplateThunk athunk; // 正序 comp = (cmpfunc)athunk.GetCall(&MySort;::sort<int>, &MySort;(0)); Sort(comp); func(string); std::cout << string << std::endl; // 逆序 comp = (cmpfunc)athunk.GetCall(&MySort;::sort<int>, &MySort;(1)); Sort(comp); func(string); std::cout << string << std::endl; } }; class CallBack { public: std::vector<int> *pthis; CallBack(std::vector<int> *ff):pthis(ff){} void __stdcall DebugArray(std::string &out;) { char buff[100]; char *aa = buff; for each (auto a in *pthis) { aa += sprintf(aa, "%d ", a); } out.assign(buff); } }; void main() { TemplateThunk athunk; Test tt; tt.mm = { 1, 3, 7, 8, 5, 6, 4, 2, 3, 10 }; tt.Entry(athunk.GetCall(&CallBack;::DebugArray,&CallBack;(&tt;.mm))); } template <typename T> int __cdecl MySort::sort(const void* a, const void* b) { return Rule ? *static_cast<const T*>(a)-*static_cast<const T*>(b) : *static_cast<const T*>(b)-*static_cast<const T*>(a); }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值