返回列表 发帖

[转帖]VC中函数调用方式的研究

对函数调用方式的研究 函数的调用约定主要是声明以下信息: ? 参数的入栈顺序; ? 校正栈顶者; ? 参数传递方式; ? 可否不定参数等。 在 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 } } 完全是一样的,只不过没有我写的好看:)。 这种处理方式高度自由,编译器不帮你生成保存现场和分配局部变量空间的代码,一切得靠自己的双手来解决,包括返回值的处理。 可能在底层的控制方面会有作用,编译器不会碍事嘛。

返回列表 回复 发帖