在C程序中,带参数的函数调用有几种方式;这里主要介绍三种:stdcall, cdecl, fastcall;
stdcall是WINDOWS API的默认方式;这种方式,参数由被调用者清除,因此产生的代码体略微小一点点(不明显,毕竟就那么几条汇编指令)。
但该方式不能应付变长参数的函数,比如printf;因为printf本身不知道传进来多少个参数,参数大小,只能运行时解析获得;而对于void test(int a, int b); test函数编译时就知道参数一定是两个int,所以test能自行清理参数。
Cdecl是C默认的函数调用约定;它和上者的区别是,由调用者清除参数;所以它能处理printf族函数,调用者当然知道传进去的参数大小。
Fastcall是尽量将参数用寄存器传递而不是入栈;好像使用的不多;简单测了一个计算两数和的函数,速度甚至比cdecl之类的慢。而且只能使用两个寄存器,比如说你有5个参数,那么只有前两个参数压入寄存器。
几种方式无所谓好与坏,主要是看是否变长参数调用,还有调第三方库时要遵循约定。C默认是cdecl。Windows函数基本默认都是WINAPI,也就是__stdcall。
下面给出代码,以更好的明白这几种方式。代码很简单,main调用一个add函数,计算两数之和;
STDCALL
int __stdcall Add(int a, int b)
{
008B13B0 push ebp
008B13B1 mov ebp,esp
008B13B3 sub esp,0CCh
008B13B9 push ebx
008B13BA push esi
008B13BB push edi
008B13BC lea edi,[ebp-0CCh]
008B13C2 mov ecx,33h
008B13C7 mov eax,0CCCCCCCCh
008B13CC rep stos dword ptr es:[edi]
int c = a + b;
008B13CE mov eax,dword ptr [a]
008B13D1 add eax,dword ptr [b]
008B13D4 mov dword ptr [c],eax
return c;
008B13D7 mov eax,dword ptr [c]
}
008B13DA pop edi
008B13DB pop esi
008B13DC pop ebx
008B13DD mov esp,ebp
008B13DF pop ebp
008B13E0 ret 8 // Add自己清除参数,8个字节,也就是两个int
int main()
{
008B1430 push ebp
008B1431 mov ebp,esp
008B1433 sub esp,0D8h
008B1439 push ebx
008B143A push esi
008B143B push edi
008B143C lea edi,[ebp-0D8h]
008B1442 mov ecx,36h
008B1447 mov eax,0CCCCCCCCh
008B144C rep stos dword ptr es:[edi]
int aa = 7, bb = 9;
008B144E mov dword ptr [aa],7
008B1455 mov dword ptr [bb],9
Add(aa, bb);
008B145C mov eax,dword ptr [bb]
008B145F push eax
008B1460 mov ecx,dword ptr [aa]
008B1463 push ecx // 调用者main负责参数入栈,都是这样的,除了FASTCALL
008B1464 call Add (8B11EAh) // 调用完了main什么也不敢,Add自己清除堆栈参数
return 0;
008B1469 xor eax,eax
}
CDECL
int __cdecl Add(int a, int b)
{
001213B0 push ebp
001213B1 mov ebp,esp
001213B3 sub esp,0CCh
001213B9 push ebx
001213BA push esi
001213BB push edi
001213BC lea edi,[ebp-0CCh]
001213C2 mov ecx,33h
001213C7 mov eax,0CCCCCCCCh
001213CC rep stos dword ptr es:[edi]
int c = a + b;
001213CE mov eax,dword ptr [a]
001213D1 add eax,dword ptr [b]
001213D4 mov dword ptr [c],eax
return c;
001213D7 mov eax,dword ptr [c]
}
001213DA pop edi
001213DB pop esi
001213DC pop ebx
001213DD mov esp,ebp
001213DF pop ebp
001213E0 ret // 被调用者Add不管参数,直接返回
int main()
{
00121430 push ebp
00121431 mov ebp,esp
00121433 sub esp,0D8h
00121439 push ebx
0012143A push esi
0012143B push edi
0012143C lea edi,[ebp-0D8h]
00121442 mov ecx,36h
00121447 mov eax,0CCCCCCCCh
0012144C rep stos dword ptr es:[edi]
int aa = 7, bb = 9;
0012144E mov dword ptr [aa],7
00121455 mov dword ptr [bb],9
Add(aa, bb); // 调用者main负责入站,和STDCALL一样
0012145C mov eax,dword ptr [bb]
0012145F push eax
00121460 mov ecx,dword ptr [aa]
00121463 push ecx
00121464 call Add (1211EFh)
00121469 add esp,8 // Add函数返回后,main执行栈清除,以适用于变长参数的函数
return 0;
0012146C xor eax,eax
}
FASTCALL
int __fastcall Add(int a, int b)
{
001613B0 push ebp
001613B1 mov ebp,esp
001613B3 sub esp,0E4h
001613B9 push ebx
001613BA push esi
001613BB push edi
001613BC push ecx
001613BD lea edi,[ebp-0E4h]
001613C3 mov ecx,39h
//011B144E mov byte ptr [ebp-0E9h],0 这句话在有 未初始化变量时产生,是个标志
001613C8 mov eax,0CCCCCCCCh // 将局部变量区域初始化成0XCC;
// 如果局部变量没有初始化,会执行断点中断指令,因为0XCC就是int 3指令码。
// 这是VS DEBUG一个特性,预防使用未初始化的变量。它会插入这样的代码:
// 011B1473 cmp byte ptr [ebp-0E9h],0
//011B147A jne main+59h (11B1489h) 相等则有问题
//011B147C push offset (11B14BAh)
//011B1481 call @ILT+175(__RTC_UninitUse) (11B10B4h)
//011B1486 add esp,4
001613CD rep stos dword ptr es:[edi]
001613CF pop ecx
001613D0 mov dword ptr [ebp-14h],edx
001613D3 mov dword ptr [ebp-8],ecx
int c = a + b;
001613D6 mov eax,dword ptr [a]
001613D9 add eax,dword ptr [b]
001613DC mov dword ptr [c],eax
return c;
001613DF mov eax,dword ptr [c]
}
001613E2 pop edi
001613E3 pop esi
001613E4 pop ebx
001613E5 mov esp,ebp
001613E7 pop ebp
001613E8 ret // 这里会做栈的清除(如果有,参数多的话不能全入寄存器) 仍然是由Add处理,因此该方式也不适用于printf; 本例参数太少,没有入栈的参数。
int main()
{
00161430 push ebp
00161431 mov ebp,esp
00161433 sub esp,0D8h
00161439 push ebx
0016143A push esi
0016143B push edi
0016143C lea edi,[ebp-0D8h]
00161442 mov ecx,36h
00161447 mov eax,0CCCCCCCCh
0016144C rep stos dword ptr es:[edi]
int aa = 7, bb = 9;
0016144E mov dword ptr [aa],7
00161455 mov dword ptr [bb],9
Add(aa, bb);
0016145C mov edx,dword ptr [bb]
0016145F mov ecx,dword ptr [aa] // MAIN将两个参数放入寄存器而不是入栈
00161462 call Add (1611E5h)
return 0;
00161467 xor eax,eax
}