Windows下的函数调用约定

注:这里的函数调用指的是C/C++中的函数调用,平台是Windows。如果要写可移植的代码,最好不要显式的使用这些调用约定。

Windows上目前最常见的调用约定应该有如下四种:__cdecl,__stdcall,__fastcall和__thiscall,另外有好多从中#define出来的macros。这四种每种都规定了函数在调用时和调用后的处理步骤。

在分析各种调用前,首先要了解下一些基本的函数调用常识:

  1. 函数调用前会将参数(如果有的话)存入指定的存储空间,一般来说是栈,但是也可以是寄存器。参数会被自动扩展为机器字大小。例如:32位的程序是32bits。这个和push指令有关。
  2. 函数调用指令call会自动将EIP压入栈中;相应的ret指令会自动将栈顶保存的地址还给EIP
  3. 大部分情况下,在刚进入函数后,编译器会将EBP设置成栈帧基址,并在函数的整个执行中以EBP为地址基准。接着编译器会把可能用到的寄存器压住栈中。通常情况下,编译器还会自动保留一段栈空间用以保存临时变量,而且这段地址常常会用0xCCCCCCCC填充
  4. 在函数的结尾部分,会执行寄存器和EBP的复原操作。并通常将返回值置入EAX。
  5. 上面的操作结束后就需要清理参数传递消耗的栈空间。这部分清理可以在函数内部和外部进行,依据调用约定

为了简化说明,使用两个数求和的DEMO是汇编中的传统。在这里,我们选择沿袭这个传统(说实在,我真不知道为什么传统是这个,望天)。我们的demo code如下:

1 int sumexp(int a, int b)
2 {
3     return a + b;
4 }
5  
6 int c = sumexp(5, 10);

0.__cdecl

__cdecl是C/C++中普通函数默认的调用约定。其实我很怀疑“cdecl”是“c declaration”的缩写。

__cdecl的主要特点如下:

  1. 函数参数逆序压栈,即从右往左依次压入栈中
  2. 由调用者[caller]清栈

逆序压栈是因为需要更好的支持C中的format specification。而caller清栈指的自然是清理参数消耗的栈空间。

上面这段代码被编译后的汇编代码如下(代码来自OD):

view source
print ?
01 ; calling
02 push    0A
03 push    5
04 call    00DD120D  ; sumexp
05 add     esp, 8
06  
07  
08 ; in function sumexp
09 push    ebp
10 mov     ebp, esp
11 sub     esp, 0C0
12 push    ebx
13 push    esi
14 push    edi
15 lea     edi, [ebp-C0]
16 mov     ecx, 30
17 mov     eax, CCCCCCCC
18 rep     stos dword ptr es:[edi]
19  
20 ; summing
21 ; return a + b
22 mov     eax, [ebp+8]    ; 1st param
23 add     eax, [ebp+C]    ; 2nd param
24  
25 pop     edi
26 pop     esi
27 pop     ebx
28 mov     esp, ebp
29 pop     ebp
30  
31 ; return
32 retn

上面反汇编代码中,在发生函数调用后,编译器手动增加了ESP的值以清理堆栈。

需要注意的是,C编译器在编译时会将采用__cdecl约定的函数名作如下修饰:

_functionname

在C++中由于要支持函数重载,所以会有额外的修饰操作。

1. __stdcall

__stdcall是Windows API使用的调用约定,故也经常被定义成WINAPI。

__stdcall有如下主要特点:

  1. 函数参数逆序压栈,即从右往左依次压入栈中
  2. 由被调用者[callee]清栈

函数的反汇编代码如下:

01 ; calling
02 push    0A
03 push    5
04 call    00CA1212
05  
06 push    ebp
07 mov     ebp, esp
08 sub     esp, 0C0
09 push    ebx
10 push    esi
11 push    edi
12 lea     edi, [ebp-C0]
13 mov     ecx, 30
14 mov     eax, CCCCCCCC
15 rep     stos dword ptr es:[edi]
16  
17 ; summing
18 mov     eax, [ebp+8]
19 add     eax, [ebp+C]
20  
21 pop     edi
22 pop     esi
23 pop     ebx
24 mov     esp, ebp
25 pop     ebp
26  
27 ; cleanup
28 retn    8

观察可以发现,清理参数消耗的栈空间直接在函数内部完成了。但是这里有个问题,编译器怎么知道参数用了多少栈空间?答案是不知道。

为了干掉这个不知道,编译器会将使用__stdcall的函数做如下修饰

__functionname@num

@后面num是参数消耗的栈空间。而且这个数在32bits下一定是4的倍数。

由于清理栈空间在函数内部完成,少了一条指令,所以采用__stdcall的函数用起来会比__cdecl更快更小。但是由于不定参数的存在,使得C和普通的C++函数无法使用__stdcall调用约定。而绝大多数的WinAPI由于在调用时就已经确定了参数个数,所以大量使用__stdcall调用约定。

2. __fastcall

使用__fastcall调用约定,可以将参数存入寄存器,从而减少使用栈的开销。但是考虑到寄存器的个数有限以及相当一部分寄存器都有特定的用途,所以__fastcall只使用了ECX和EDX两个寄存器。

__fastcall的主要特点如下:

  1. 某两个函数参数分别压入ECX和EDX(同样逆序)。其余的逆序压入栈
  2. 由被调用者[callee]清栈

相应的反汇编代码如下:

01 mov     edx, 0A
02 mov     ecx, 5
03 call    00FE1217
04  
05 push    ebp
06 mov     ebp, esp
07 sub     esp, 0D8
08 push    ebx
09 push    esi
10 push    edi
11 push    ecx
12 lea     edi, [ebp-D8]
13 mov     ecx, 36
14 mov     eax, CCCCCCCC
15 rep     stos dword ptr es:[edi]
16 pop     ecx
17  
18 mov     [ebp-14], edx
19 mov     [ebp-8], ecx
20 mov     eax, [ebp-8]
21 add     eax, [ebp-14]
22  
23 pop     edi
24 pop     esi
25 pop     ebx
26 mov     esp, ebp
27 pop     ebp
28  
29 retn

编译器会在编译时将采用__fastcall的函数做如下修饰

@functionname@num

num是所有参数的大小

使用__fastcall需要非常的注意,因为这容易引起一些莫名其妙的问题,而且VC保留对这个调用约定的决定权。

3. __thiscall

__thiscall是C++中成员函数的默认调用约定,其主要特点如下:

  1. this指针置于ECX,其余参数逆序压栈
  2. 由被调用者[callee]清栈

为了使用成员函数,我们将函数放入struct中:

01 struct SUM
02 {
03     int sumexp(int a, int b)
04     {
05         return a + b;
06     }
07 };
08  
09     SUM s;
10     int c = s.sumexp(5, 10);

相应的反汇编代码如下:

01 push    0A
02 push    5
03 lea     ecx, [ebp-5]  ; this ptr
04 call    0002121C
05  
06 push    ebp
07 mov     ebp, esp
08 sub     esp, 0CC
09 push    ebx
10 push    esi
11 push    edi
12 push    ecx
13 lea     edi, [ebp-CC]
14 mov     ecx, 33
15 mov     eax, CCCCCCCC
16 rep     stos dword ptr es:[edi]
17 pop     ecx
18  
19 mov     [ebp-8], ecx
20 mov     eax, [ebp+8]
21 add     eax, [ebp+C]
22  
23 pop     edi
24 pop     esi
25 pop     ebx
26 mov     esp, ebp
27 pop     ebp
28  
29 retn    8

另外,如果成员函数使用了不定参数,那么编译器则会转而使用__cdecl作为调用约定。

你可能感兴趣的:(c/c++,windows)