函数调用约定,是指当一个函数被调用时,函数的参数会被传递给被调用的函数和返回值会被返回给调用函数。函数的调用约定就是描述参数是怎么传递和由谁平衡堆栈的,当然还有返回值。
当一个函数被调用时,函数的参数会被传递给被调用的函数和返回值会被返回给调用函数。函数的调用约定就是描述参数是怎么传递和由谁平衡堆栈的,当然还有返回值。
__stdcall,__cdecl,__fastcall,__thiscall,__nakedcall,__pascal
1.从右到左依次入栈:__stdcall,__cdecl,__thiscall
2.从左到右依次入栈:__pascal,__fastcall
1.调用者清除栈。
2.被调用函数返回后清除栈。
1、参数是从右向左传递的,也是放在堆栈中。
2、堆栈平衡是由调用函数来执行的(在call B,之后会有add esp x,x表示参数的字节数)。
3、函数的前面会加一个前缀_(_sumExample)
下面来看看具体的反汇编代码,这是从VC反汇编的代码截取的一部分代码。
10: int c = 0;
00401088 C7 45 FC 00 00 00 00 mov dword ptr [ebp-4],0
11: c = sumExample(2, 3);
0040108F 6A 03 push 3
00401091 6A 02 push 2
从上面的两个push操作我们就可以知道参数是从右向左传递的了。另外这里也回答前面的问题为什么参数会被扩展为4个字节,因为堆栈的操作都是对一个字进行操作的,所以参数都是4个字节的。
00401093 E8 7C FF FF FF call @ILT+15(_Max) (00401014)
这里就是调用函数操作了。在进行call操作之后,会自动将call的下一条语句作为函数的返回地址保存在栈中,也就是下面的(00401098)。在地址00401014处我们可以看到这样的一小段代码
@ILT+0(_Max):
00401005 E9 26 00 00 00 jmp _sumExample (00401030)
这里就可以知道程序在编译之后会在函数前面加上前缀_
00401098 83 C4 08 add esp,8
这里就是平衡堆栈操作了。可以看出是在调用者进行的。
0040109B 89 45 FC mov dword ptr [ebp-4],eax
保存值是由eax寄存器返回的,从这里就可以看出了。
12:
13: return 0;
0040109E 33 C0 xor eax,eax
14: }
3: int __cdecl sumExample(int a, int b)
4: {
00401030 55 push ebp
00401031 8B EC mov ebp,esp
00401033 83 EC 40 sub esp,40h 为局部变量预留空间
00401036 53 push ebx
00401037 56 push esi
00401038 57 push edi
00401039 8D 7D C0 lea edi,[ebp-40h]
0040103C B9 10 00 00 00 mov ecx,10h
00401041 B8 CC CC CC CC mov eax,0CCCCCCCCh
00401046 F3 AB rep stos dword ptr [edi]
这上面的一段代码就是函数的开端了。也就是function prolog。通过将一些寄存器来对它们进行保存,也就像中断发生后,需要保护现场一样。
5: return (a + b);
00401048 8B 45 08 mov eax,dword ptr [ebp+8]
0040104B 03 45 0C add eax,dword ptr [ebp+0Ch]
6: }
0040104E 5F pop edi
0040104F 5E pop esi
00401050 5B pop ebx
00401051 8B E5 mov esp,ebp
00401053 5D pop ebp
00401054 C3 ret
这里就是函数收尾,也就是function epilog
经过上面的分析,相信你对__cdecl调用约定有了比较清晰的认识了。但是这里我们应该想想为什么不在被调函数内进行堆栈平衡呢?在这里我们应该要考虑类似于像scanf和printf这样的函数,这里我们应该明白这两个函数的参数都是可变的,如果参数不固定的话,在被调用函数内就无法知道参数究竟使用了多少个字节,所以为了实现可变参数,我们必须要在被调函数执行之后我们才知道参数究竟用了多少字节,所以我们在调用者来进行堆栈平衡操作。在后面我们将要对printf函数内部是怎么实现做一些探究。
Win32 API函数绝大部分都是采用__stdcall调用约定的。WINAPI其实也只是__stacall的一个别名而已。
#define WINAPI __stdcall
还是与上面一样,我们在函数的面前用__stdcall作为修饰符。此时函数将会采用__stdcall调用约定
int __stdcallsumExample (int a, int b);
__stdcall调用约定的主要特征是:
1、参数是从右往左传递的,也是放在堆栈中。
3、在函数名的前面用下划线修饰,在函数名的后面由@来修饰并加上栈需要的字节数的空间(_sumExample@8)。
main函数
push 3
push 2
这两个push可以说明函数的参数是由右向左传递的。
call _sumExample@8 //调用函数
mov dword ptr [c], eax //eax寄存器保存函数的返回值,此时将返回值赋值给局部变量c。
再来看看函数的代码。
函数的开端与__cdecl调用约定是相同的
mov eax, dword ptr [a]
add eax, dword ptr [b]
函数的收尾也是和__cdecl调用约定是相同的
另外在最后面将对堆栈进行平衡操作。
ret 8 //两个4字节的参数
上面的是文章本来的说明,但在VC中却好像有点区别。
main函数
0040108F 6A 03 push 3
00401091 6A 02 push 2
00401093 E8 81 FF FF FF call @ILT+20(_sumExample) (00401019)
FC mov dword ptr [ebp-4],eax
sumExample函数
5: return (a + b);
00401048 8B 45 08 mov eax,dword ptr [ebp+8]
0040104B 03 45 0C add eax,dword ptr [ebp+0Ch]
00401054 C2 08 00 ret 8 //堆栈平衡操作
因为栈的清理(堆栈平衡操作)是由被调用函数执行的。所以使用__stdcall调用约定生成的可执行文件要比__cdecl的要小,因为在每次的函数调用都要产生堆栈清理的代码。函数具有可变参数像我wsprintf这个函数,与前面的prinf一样,都必须使用__cdecl调用约定,因为只有调用者才知道参数的数量在每一次的函数调用,因此也只有调用者才能够执行堆栈清理操作。
__fastcall见名知其意,其特点就是快。__fastcall函数调用约定表明了参数应该放在寄存器中,而不是在栈中,VC编译器采用调用约定传递参数时,最左边的两个不大于4个字节(DWORD)的参数分别放在ecx和edx寄存器。当寄存器用完的时候,其余参数仍然从右到左的顺序压入堆栈。像浮点值、远指针和__int64类型总是通过堆栈来传递的。
下面来看看使用测试的源代码
#include <stdio.h>
int __fastcall sumExample(int a, int b, int c)
{
return (a + b + c);
}
double __fastcall sumExampled(double a, double b)
{
return (a + b);
}
int main()
{
int c = 0;
double d = 0.0;
c = sumExample(2, 3, 5);
d = sumExampled(2.3, 2.5);
return 0;
}
15: int c = 0;
004010C8 C7 45 FC 00 00 00 00 mov dword ptr [ebp-4],0
16: double d = 0.0;
004010CF C7 45 F4 00 00 00 00 mov dword ptr [ebp-0Ch],0
004010D6 C7 45 F8 00 00 00 00 mov dword ptr [ebp-8],0
17: c = sumExample(2, 3, 5);
004010DD 6A 05 push 5
004010DF BA 03 00 00 00 mov edx,3
004010E4 B9 02 00 00 00 mov ecx,2
004010E9 E8 26 FF FF FF call @ILT+15(@sumExample@8) (00401014)
004010EE 89 45 FC mov dword ptr [ebp-4],eax
18:
19: d = sumExampled(2.3, 2.5);
004010F1 68 00 00 04 40 push 40040000h
004010F6 6A 00 push 0
004010F8 68 66 66 02 40 push 40026666h
004010FD 68 66 66 66 66 push 66666666h
00401102 E8 FE FE FF FF call @ILT+0(@sumExampled@16) (00401005)
00401107 DD 5D F4 fstp qwordptr[ebp-0Ch]