咨询电话 027-87678580 金老师 QQ 2056813683倪老师 QQ 1564396558

在线留言 提交作业 联系我们

对函数调用方式的研究

admin

通过 VC++ 6.0 示例与反汇编代码,对比 __stdcall、__cdecl、pascal 和 __fastcall 等函数调用约定,说明参数传递、栈平衡与可变参数的区别。

函数的调用约定主要是声明以下信息:

  • 参数的入栈顺序;
  • 校正栈顶者;
  • 参数传递方式;
  • 可否不定参数等。

在 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   |.  6A62                push 62        ''b''
0040113A   |.  6A61                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   |.  8A450C              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   |.  6A64                push 64
	00401143   |.  6A63                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 FC0A           mov byte ptr ss:[ebp-4],0A

va_start( marker, para )

	0040B6BC   |.  8D450C              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 =0041F10CASCII "%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   |.  E87A5AFFFF          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   |.  6A66                push 66
0040114F   |.  6A65                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''标记了参数的结束。在这些方式中,无一不是使用指针进行数据的读取,而且,如果操作不慎,导致指针越界,指向了参数表之外,这时候编译器不会报告任何错误,而这类错误不但后果严重,而且错误隐蔽,很难查到。

多参函数的优点是使用灵活,定参函数的优点是稳定,高效。

在实际开发中,多参函数通常可以改用定参函数实现,所以,我的建议是,若非万不得已,尽量少定义多参函数。

返回技术交流