调用约定和函数导出名

来源:互联网 发布:首都医科大学网络教育 编辑:程序博客网 时间:2024/05/16 14:30

        本文主要介绍C/C++代码编译中的“调用约定”选择和不同的编译器产生的不同规则的“函数导出名”的问题。主要涉及:cdecl、stdcall、thiscall、VC++编译器、mingw编译器、gcc编译器、g++编译器。

一、调用约定

        调用约定指的是函数在被调用的时候,会按照不同的规则,翻译成不同的汇编代码。为了解释这个概念,首先要了解一下调用堆栈的概念。当一个函数被调用时,首先压入函数的各个参数,然后压入函数的返回地址。当函数退出时会以相反的顺序依次退出堆栈。因此,函数在被调用前和调用后的堆栈保持平衡。

        编译中常见的cdecl、stdcall、thiscall等调用约定,主要约定两件事:

        1)参数入栈的顺序;

        2)函数调用结束后,谁(调用者or被调用者)来恢复栈。


1,C语言调用约定,__cdecl

        VS新建win32工程,查看VS工程“属性-> c/c++ -> 高级”,可知VS Win32工程默认为cdecl调用约定,它是C语言的调用约定,它的约定如下:

        1)函数调用时,参从右到左入栈。

        2)函数调用结束后,调用者负责恢复栈。


示例代码(新建win32控制台程序):

<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">int Foo(int a, int b){    return a + b;}int main(){    int x = Foo(2,3);    return 0;}</span></span></span>


 反汇编,如下:

<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">int main(){00A52290  push        ebp  00A52291  mov         ebp,esp  00A52293  sub         esp,0CCh  00A52299  push        ebx  00A5229A  push        esi  00A5229B  push        edi  00A5229C  lea         edi,[ebp-0CCh]  00A522A2  mov         ecx,33h  00A522A7  mov         eax,0CCCCCCCCh  00A522AC  rep stos    dword ptr es:[edi]  <span style="white-space:pre"></span>int x = Foo(2,3);00A522AE  push        3  00A522B0  push        2  00A522B2  call        Foo (0A51366h)  00A522B7  add         esp,8  00A522BA  mov         dword ptr [x],eax      return 0;00A522BD  xor         eax,eax  }00A522BF  pop         edi  00A522C0  pop         esi  00A522C1  pop         ebx  00A522C2  add         esp,0CCh  00A522C8  cmp         ebp,esp  00A522CA  call        __RTC_CheckEsp (0A51339h)  00A522CF  mov         esp,ebp  00A522D1  pop         ebp  00A522D2  ret</span></span></span>


如上,Foo函数的调用过程:

        1)先“push 3";

        2)再压入左边的参数”2“;

        3)压入”Foo函数的入口地址“,call表示跳转到该地址;

        4)”add     esp,8“,即将”栈顶指针“加8,表示函数调用完成后,堆栈回退8个字节,刚好是两个参数(2* 4byte)所占的内存大小。

注:
       1) 在内存地址空间中,栈是由高地址向低地址增长,即“栈是向下生长的”。故,堆栈回退,栈顶指针进行”+“运算。

        2)参数是按4字节对齐,不管是int型还是char型。



注:

1,MinGW编译器默认也是采用的__cdecl调用约定。

2,对于动态链接库,采用动态加载方式(即“resolve a symbol in the library at runtime”),由于不需要将DLL关联的“.h文件”编译进调用者工程,调用者编译器将采用默认的的调用约定,一般为“__cdecl”。

        如果DLL采用的是__stdcall,而调用者采用动态加载,这个时候怎么办?

        办法是有的。大致原理是:调用者实际上是先“resolve a symbol in the library at runtime”,拿到被调函数的地址,并保存到一个函数指针中,然后通过函数指针来调用该函数。因此,我们可以为该函数指针指定调用约定。形式如下:

<span style="font-size:14px;"><span style="font-size:14px;">long (__stdcall *pFunctionPtr) (void) = (long(__stdcall*)(void)) m_MyLibrary.resolve("Foo");</span></span>

        此外,也可通过typedef先行指定函数指针的形式。

        此处参考:DLL的调用约定 和 C++成员指针 this指针 调用约定,因为C++成员函数采用的是“__thiscall”,而VS和MinGW等默认为__cdecl,在类外通过类成员函数指针调用时,就需要注意到这些差异引起的问题。

3,Qt - MinGW版 采用QLibrary类来管理动态加载。可以阅读Qt的文档,摘要如下:

The QLibrary class loads shared libraries at runtime.


An instance of a QLibrary object operates on a single shared object file (which we call a "library", but is also known as a "DLL"). A QLibrary provides access to the functionality in the library in a platform independent way. You can either pass a file name in the constructor, or set it explicitly with setFileName(). When loading the library, QLibrary searches in all the system-specific library locations (e.g. LD_LIBRARY_PATH on Unix), unless the file name has an absolute path.


The most important functions are load() to dynamically load the library file, isLoaded() to check whether loading was successful, and resolve() to resolve a symbol in the library. The resolve() function implicitly tries to load the library if it has not been loaded yet. Multiple instances of QLibrary can be used to access the same physical library. Once loaded, libraries remain in memory until the application terminates. You can attempt to unload a library using unload(), but if other instances of QLibrary are using the same library, the call will fail, and unloading will only happen when every instance has called unload().


A typical use of QLibrary is to resolve an exported symbol in a library, and to call the C function that this symbol represents. This is called "explicit linking" in contrast to "implicit linking", which is done by the link step in the build process when linking an executable against a library.


The following code snippet loads a library, resolves the symbol "mysymbol", and calls the function if everything succeeded. If something goes wrong, e.g. the library file does not exist or the symbol is not defined, the function pointer will be 0 and won't be called.


<span style="font-size:14px;"><span style="font-size:14px;">QLibrary myLib("mylib");typedef void (*MyPrototype)();MyPrototype myFunction = (MyPrototype) myLib.resolve("mysymbol");if (myFunction)    myFunction();</span></span>

The symbol must be exported as a C function from the library for resolve() to work. This means that the function must be wrapped in an extern "C" block if the library is compiled with a C++ compiler. On Windows, this also requires the use of a dllexport macro; see resolve() for the details of how this is done. For convenience, there is a static resolve() function which you can use if you just want to call a function in a library without explicitly loading the library first:


<span style="font-size:14px;"><span style="font-size:14px;">typedef void (*MyPrototype)();MyPrototype myFunction =        (MyPrototype) QLibrary::resolve("mylib", "mysymbol");if (myFunction)    myFunction();</span></span>

值得注意的是,它只加载C接口。





三、标准调用约定,__stdcall

        VS默认的win32工程采用的是__cdecl。但是,如果用VS2015+WDK10新建KMDF驱动工程,默认的调用约定为”__stdcal“。事实上,Windows API采用的都是stdcall,如关键字WINAPI 、CALLBACK。但__stdcall不能用于可变参数函数调用。

        __stdcall调用会在目标文件中产生一个符号来代表这个函数(实际是用符号替换地址),此符号形式为”下划线+函数名+X“。其中,X代表清理堆栈时需要的数字,函数以”ret X“的形式返回。它的约定如下:

        1)函数调用时,参从右到左入栈。

        2)函数调用结束后,被调用函数返回前,自己负责恢复栈。


将上述示例中的Foo函数改为如下形式:

<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">int __stdcall Foo(int a, int b)</span></span></span>

反汇编后,如下:

<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">int main(){00D92290  push        ebp  00D92291  mov         ebp,esp  00D92293  sub         esp,0CCh  00D92299  push        ebx  00D9229A  push        esi  00D9229B  push        edi  00D9229C  lea         edi,[ebp-0CCh]  00D922A2  mov         ecx,33h  00D922A7  mov         eax,0CCCCCCCCh  00D922AC  rep stos    dword ptr es:[edi]  <span style="white-space:pre"></span>int x = Foo(2,3);00D922AE  push        3  00D922B0  push        2  00D922B2  call        Foo (0D9136Bh)  00D922B7  mov         dword ptr [x],eax      return 0;00D922BA  xor         eax,eax  }00D922BC  pop         edi  00D922BD  pop         esi  00D922BE  pop         ebx  00D922BF  add         esp,0CCh  00D922C5  cmp         ebp,esp  00D922C7  call        __RTC_CheckEsp (0D91339h)  00D922CC  mov         esp,ebp  00D922CE  pop         ebp  00D922CF  ret</span></span></span>

注:

1,调用者调用完成后,未移动栈顶指针。

2,被调函数,最后"ret 8",自行清理堆栈。

<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">int __stdcall Foo(int a, int b){00D91A70  push        ebp  00D91A71  mov         ebp,esp  00D91A73  sub         esp,0C0h  00D91A79  push        ebx  00D91A7A  push        esi  00D91A7B  push        edi  00D91A7C  lea         edi,[ebp-0C0h]  00D91A82  mov         ecx,30h  00D91A87  mov         eax,0CCCCCCCCh  00D91A8C  rep stos    dword ptr es:[edi]  <span style="white-space:pre"></span>return a + b;00D91A8E  mov         eax,dword ptr [a]  00D91A91  add         eax,dword ptr [b]  }00D91A94  pop         edi  00D91A95  pop         esi  00D91A96  pop         ebx  00D91A97  mov         esp,ebp  00D91A99  pop         ebp  00D91A9A  ret         8</span></span></span>


二、函数符号导出名

        同样一个函数,用在C语言编译器和C++编译器编译出的符号导出名是不同的。而在链接的时候,链接器不知道源程序的函数名,它只会去目标(Object)文件中寻找相应的函数符号表。VC或DDK提供的编译器cl.exe,既可以编译C语言,又可以编译C++语言。默认情况下,编译器会根据源文件的扩展名,来判断使用哪种方式编译。当文件扩展名是.cpp时,编译器会使用C++方式编译,当文件扩展名是.c的时候,则使用C语言方式编译。例如,同样使用stdcall调用约定编译的函数:

<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">int _declspec(dllexport) __stdcall Foo(int a, int b){return a + b;}</span></span></span>
    在C++编译方式下,导出符号如下:



而在C编译方式中,导出符号为:“_Foo@8”。C++复杂的函数导出符号是为了支持函数重载而设计的,不同的C++编译器的函数导出符号各不相同。故,在跨编译器调用动态链接库的函数时,最好将导出函数用“extern C”修饰,变为C语言方式导出函数符号。如下:

<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">extern "C" int _declspec(dllexport) __stdcall Foo(int a, int b){return a + b;}</span></span></span>

注:

        Windows驱动程序的入口函数规定为“_DriverEntry@8”,因此,用C++编译器编译时,需要添加“extern c”修饰,否则会导致链接错误。



具体参考博客:调用约定、mingw和mvc转换、msvc mingw dll

0 0