姓名:
电话:
QQ:
学历:

对函数调用方式的研究

发布时间:2007-12-03 09:21   内容发布:武汉科锐软件安全教育机构

函数的调用约定主要是声明以下信息:
?    参数的入栈顺序;
?    校正栈顶者;
?    参数传递方式;
?    可否不定参数等。

VC++6.0中支持__stdcall__cdeclpascal__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''标记了参数的结束。在这些方式中,无一不是使用指针进行数据的读取,而且,如果操作不慎,导致指针越界,指向了参数表之外,这时候编译器不会报告任何错误,而这类错误不但后果严重,而且错误隐蔽,很难查到。

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

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

Copyright©2007-2015 武汉市科锐软件技术有限公司.
公司地址:武汉市东湖新技术开发区关南园一路当代光谷梦工场5号楼十层
鄂ICP备17007538号-1