对函数调用方式的研究
函数的调用约定主要是声明以下信息:
? 参数的入栈顺序;
? 校正栈顶者;
? 参数传递方式;
? 可否不定参数等。
在 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 != ';'; )
{
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 ;
}
struct sa{
double a;
char b;
int c;
// char d;
int f;
};
struct bita
{
int a:8;
int b:8;
int c:8;
int d:8;
};
int main(int argc, char* argv[])
{
sa sa;
sa.a=1.0;
sa.b=';A';;
sa.c=4;
// sa.d=';D';;
sa.f=5;
printf("sa is %d\n",sizeof(sa));
bita bita;
bita.a=1;
bita.b=2;
bita.c=3;
bita.d=4;
test_stdcall( ';a';, ';b'; );
test_cdecl(';c';,';d';,';e';,';f';,';g'; ,';h'; ,';';);
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 != \';\\'; )
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 传递。
按照 MASM32 的约定来看, 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 ; 校正栈顶
如果定义了, pascal 和 __stdcall 没有什么区别,
如果没有定义程序一编译就会报错。
可能是为了兼容才让 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
}
}
完全是一样的,只不过没有我写的好看:)。
这种处理方式高度自由,编译器不帮你生成保存现场和分配局部变量空间的代码,一切得靠自己的双手来解决,包括返回值的处理。
可能在底层的控制方面会有作用,编译器不会碍事嘛。
|