函数的调用约定主要是声明以下信息:
? 参数的入栈顺序;
? 校正栈顶者;
? 参数传递方式;
? 可否不定参数等。
在 VC++6.0中支持__stdcall、__cdecl、pascal、__fastcall等调用约定,在这里我们研究一下这几种调用约定的具体行为。
编译环境:[VC++6.0]
编译方式:[DEBUG]
代码如下:
// TestCall.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
int __stdcall test_stdcall(char para1, char para2)
{
para1 = para2
return 0
}
int __cdecl test_cdecl(char para, ...)
{
char p = ''\n''
va_list marker
va_start( marker, para )
while( p != ''\0'' )
{
p = va_arg( marker, char)
printf("%c\n", p)
}
va_end( marker )
return 0
}
int pascal test_pascal(char para1, char para2)
{
return 0
}
int __fastcall test_fastcall(char para1, char para2, char para3, char para4)
{
para1 = (char)1
para2 = (char)2
para3 = (char)3
para4 = (char)4
return 0
}
__declspec(naked) void __stdcall test_naked(char para1, char para2)
{
__asm
{
push ebp
mov ebp, esp
push eax
mov al,byte ptr [ebp + 0Ch]
xchg byte ptr [ebp + 8],al
pop eax
pop ebp
ret 8
}
// return
}
int main(int argc, char* argv[])
{
test_stdcall( ''a'', ''b'' )
test_cdecl( ''c'',''d'',''e'',''f'',''g'' ,''h'' ,''\0'')
test_pascal( ''e'', ''f'' )
test_fastcall( ''g'', ''h'', ''i'', ''j'' )
test_naked( ''k'', ''l'')
return 0
}
*********************************************************************************************************
首先调用的是test_stdcall( ''a'', ''b'' ),这个函数被声明为 __stdcall 的调用方式,其调用代码如下:
00401138 |. 6A 62 push 62 ''b''
0040113A |. 6A 61 push 61 ''a''
0040113C |. E8 D3FEFFFF call TestCall.00401014
在这里可以清楚地看到,__stdcall的参数传递使用栈方式,入栈顺序是先 ''b'' 后 ''a'' ,所以入栈顺序是从右往左。
其返回值用 eax 传递。
然后看看函数体内的代码:
00401050 /> \55 push ebp
00401051 |. 8BEC mov ebp,esp
00401053 |. 83EC 40 sub esp,40
00401056 |. 53 push ebx
00401057 |. 56 push esi
00401058 |. 57 push edi
00401059 |. 8D7D C0 lea edi,dword ptr ss:[ebp-40]
0040105C |. B9 10000000 mov ecx,10
00401061 |. B8 CCCCCCCC mov eax,CCCCCCCC
00401066 |. F3:AB rep stos dword ptr es:[edi] 以上是保存现场和分配局部变量空间
00401068 |. 8A45 0C mov al,byte ptr ss:[ebp+C]
0040106B |. 8845 08 mov byte ptr ss:[ebp+8],al para1 = para2
0040106E |. 33C0 xor eax,eax return 0
00401070 |. 5F pop edi
00401071 |. 5E pop esi
00401072 |. 5B pop ebx
00401073 |. 8BE5 mov esp,ebp
00401075 |. 5D pop ebp 恢复现场
00401076 \. C2 0800 retn 8 校正栈顶
__stdcall在函数体内完成了栈顶的校正。
*********************************************************************************************************
然后调用了test_cdecl( ''c'',''d'' ),这个函数被声明为 __cdecl 的调用方式,其调用代码如下:
00401141 |. 6A 64 push 64
00401143 |. 6A 63 push 63
00401145 |. E8 BBFEFFFF call TestCall.00401005
一样, __cdecl 的参数传递使用栈方式,入栈顺序是从右往左。
其返回值用 eax 传递。
不过 __cdecl 方式有个特点,他支持可变的参数个数。
来看看函数体内的代码:
0040B6A0 /> \55 push ebp
0040B6A1 |. 8BEC mov ebp,esp
0040B6A3 |. 83EC 48 sub esp,48
0040B6A6 |. 53 push ebx
0040B6A7 |. 56 push esi
0040B6A8 |. 57 push edi
0040B6A9 |. 8D7D B8 lea edi,dword ptr ss:[ebp-48]
0040B6AC |. B9 12000000 mov ecx,12
0040B6B1 |. B8 CCCCCCCC mov eax,CCCCCCCC
0040B6B6 |. F3:AB rep stos dword ptr es:[edi] 以上是保存现场和分配局部变量空间
char p = ''\n''
0040B6B8 |. C645 FC 0A mov byte ptr ss:[ebp-4],0A
va_start( marker, para )
0040B6BC |. 8D45 0C lea eax,dword ptr ss:[ebp+C]
0040B6BF |. 8945 F8 mov dword ptr ss:[ebp-8],eax
while( p != ''\0'' )
0040B6C2 |> 0FBE4D FC /movsx ecx,byte ptr ss:[ebp-4]
0040B6C6 |. 85C9 |test ecx,ecx
0040B6C8 |. 74 26 |je short TestCall.0040B6F0
|
| p = va_arg( marker, char) // 返回当前参数,并使参数指针指向下一个参数
0040B6CA |. 8B55 F8 |mov edx,dword ptr ss:[ebp-8]
0040B6CD |. 83C2 04 |add edx,4
0040B6D0 |. 8955 F8 |mov dword ptr ss:[ebp-8],edx
0040B6D3 |. 8B45 F8 |mov eax,dword ptr ss:[ebp-8]
0040B6D6 |. 8A48 FC |mov cl,byte ptr ds:[eax-4]
0040B6D9 |. 884D FC |mov byte ptr ss:[ebp-4],cl
|
| printf("%c\n", p)
0040B6DC |. 0FBE55 FC |movsx edx,byte ptr ss:[ebp-4]
0040B6E0 |. 52 |push edx /Arg2
0040B6E1 |. 68 0CF14100 |push TestCall.0041F10C |Arg1 = 0041F10C ASCII "%c"
0040B6E6 |. E8 45020000 |call TestCall.0040B930 \TestCall.0040B930
|
0040B6EB |. 83C4 08 |add esp,8 printf 声明为 __cdecl , 由调用者校正栈顶
0040B6EE |.^ EB D2 \jmp short TestCall.0040B6C2
0040B6F0 |> C745 F8 00000000 mov dword ptr ss:[ebp-8],0 va_end( marker )
0040B6F7 |. 33C0 xor eax,eax return 0
0040B6F9 |. 5F pop edi
0040B6FA |. 5E pop esi
0040B6FB |. 5B pop ebx
0040B6FC |. 83C4 48 add esp,48
0040B6FF |. 3BEC cmp ebp,esp
0040B701 |. E8 7A5AFFFF call TestCall.00401180
0040B706 |. 8BE5 mov esp,ebp
0040B708 |. 5D pop ebp 恢复现场
0040B709 \. C3 retn
由于支持可变的参数个数,函数无法校正栈顶,所以这个活就留给了调用者:
0040114A |. 83C4 08 add esp,8 校正栈顶
可变参数的使用很危险,如果不知道怎样结束的话,va_arg宏会执行到出现内存访问错误为止,
而且对参数的类型控制和识别也很麻烦。
*********************************************************************************************************
看看test_pascal( ''e'', ''f'' ),这个函数被声明为 pascal 的调用方式,其调用代码如下:
0040114D |. 6A 66 push 66
0040114F |. 6A 65 push 65
00401151 |. E8 B9FEFFFF call TestCall.0040100F
和 __stdcall 一样, pascal 的参数传递使用栈方式,入栈顺序是从右往左,其返回值用 eax 传递。
按照约定来看, pascal 的参数入栈顺序是从左往右才对啊,奇怪。然后看看函数体内的代码:
004010B0 /> \55 push ebp
004010B1 |. 8BEC mov ebp,esp
004010B3 |. 83EC 40 sub esp,40
004010B6 |. 53 push ebx
004010B7 |. 56 push esi
004010B8 |. 57 push edi
004010B9 |. 8D7D C0 lea edi,dword ptr ss:[ebp-40]
004010BC |. B9 10000000 mov ecx,10
004010C1 |. B8 CCCCCCCC mov eax,CCCCCCCC
004010C6 |. F3:AB rep stos dword ptr es:[edi] 以上是保存现场和分配局部变量空间
004010C8 |. 33C0 xor eax,eax return 0
004010CA |. 5F pop edi
004010CB |. 5E pop esi
004010CC |. 5B pop ebx
004010CD |. 8BE5 mov esp,ebp
004010CF |. 5D pop ebp 恢复现场
004010D0 \. C2 0800 retn 8 校正栈顶
如果定义了<windows.h>, pascal 和 __stdcall 没有什么区别,
如果没有定义<windows.h>程序编译就会报错。
可能是为了兼容才让 pascal 约定存在的。
*********************************************************************************************************
接着是test_fastcall( ''g'', ''h'', ''i'', ''j'' ),这个函数被声明为 __fastcall 的调用方式,其调用代码如下:
00401156 |. 6A 6A push 6A
00401158 |. 6A 69 push 69
0040115A |. B2 68 mov dl,68
0040115C |? B1 67 mov cl,67
0040115E |? E8 C0FEFFFF call TestCall.00401023
这个有意思,是通过寄存器方式传递参数的,不过只能有两个寄存器参与, ecx 和 edx ,其余的还是用栈了,要珍惜啊,呵呵。
嗯,用寄存器的确是比访问内存要快得多。
其返回值用 eax 传递。
看看函数体内的代码:
004010E0 /> \55 push ebp
004010E1 |. 8BEC mov ebp,esp
004010E3 |. 83EC 48 sub esp,48
004010E6 |. 53 push ebx
004010E7 |. 56 push esi
004010E8 |. 57 push edi
004010E9 |. 51 push ecx
004010EA |. 8D7D B8 lea edi,dword ptr ss:[ebp-48]
004010ED |. B9 12000000 mov ecx,12
004010F2 |. B8 CCCCCCCC mov eax,CCCCCCCC
004010F7 |. F3:AB rep stos dword ptr es:[edi] 以上是保存现场和分配局部变量空间
004010F9 |. 59 pop ecx 获得第一个参数''g''
004010FA |. 8855 F8 mov byte ptr ss:[ebp-8],dl 获得第二个参数''h''
004010FD |. 884D FC mov byte ptr ss:[ebp-4],cl 获得第一个参数''g''
00401100 |. C645 FC 01 mov byte ptr ss:[ebp-4],1 para1 = (char)1
00401104 |. C645 F8 02 mov byte ptr ss:[ebp-8],2 para2 = (char)2
00401108 |. C645 08 03 mov byte ptr ss:[ebp+8],3 para3 = (char)3
0040110C |. C645 0C 04 mov byte ptr ss:[ebp+C],4 para4 = (char)4
00401110 |. 33C0 xor eax,eax return 0
00401112 |. 5F pop edi
00401113 |. 5E pop esi
00401114 |. 5B pop ebx
00401115 |. 8BE5 mov esp,ebp
00401117 |. 5D pop ebp 恢复现场
00401118 \. C2 0800 retn 8 校正栈顶
开始就把寄存器参数保存在 [ebp - 4] 和 [ebp - 8] 的局部变量里,其余的参数还是在栈底。
在函数体内完成了栈顶的校正。
*********************************************************************************************************
最后试一下 __declspec(naked) 的感觉,这个不是函数的调用约定,而是指定编译器的处理方式。
其调用代码如下:
00401163 |. 6A 6C push 6C
00401165 |. 6A 6B push 6B
00401167 |. E8 B2FEFFFF call TestCall.0040101E
这里使用的是 __stdcall 方式,在前面已经研究过了。
函数体内的代码:
0040B5D0 /> \55 push ebp
0040B5D1 |. 8BEC mov ebp,esp
0040B5D3 |. 50 push eax
0040B5D4 |. 8A45 0C mov al,byte ptr ss:[ebp+C]
0040B5D7 |. 8645 08 xchg byte ptr ss:[ebp+8],al
0040B5DA |. 58 pop eax
0040B5DB |. 5D pop ebp
0040B5DC \. C2 0800 retn 8
和我写的比较一下看看:
__declspec(naked) void __stdcall test_naked(char para1, char para2)
{
__asm
{
push ebp
mov ebp, esp
push eax
mov al,byte ptr [ebp + 0Ch]
xchg byte ptr [ebp + 8],al
pop eax
pop ebp
ret 8
}
}
完全是一样的,只不过没有我写的好看:)。
这种处理方式高度自由,编译器不帮你生成保存现场和分配局部变量空间的代码,一切得靠自己的双手来解决,包括返回值的处理。
可能在底层的控制方面会有作用,编译器不会碍事嘛。
*********************************************************************************************************
在 __cdecl 调用方式中,由编译器负责校正栈定,对于这一点而言确实比较安全。
我所说的危险在于开发者对参数的操作和处理上。
在 实现可变参数的函数时,由于其特殊性,导致参数的个数未知,如果要处理每一个参数就需要知道参数的个数,在printf函数实现中,是通过检查''%''的个 数而得知参数的个数,在这个例子中,我用''\0''标记了参数的结束。在这些方式中,无一不是使用指针进行数据的读取,而且,如果操作不慎,导致指针越界,指向了参数表之外,这时候编译器不会报告任何错误,而这类错误不但后果严重,而且错误隐蔽,很难查到。
多参函数的优点是使用灵活,定参函数的优点是稳定,高效。
在实际开发中,多参函数通常可以改用定参函数实现,所以,我的建议是,若非万不得已,尽量少定义多参函数。
(责任编辑:科锐软件教育机构) |