函数的调用约定

来源:互联网 发布:网络执法官绿色版 编辑:程序博客网 时间:2024/04/29 13:56
转自:http://blog.csdn.net/Blue_Dream_/archive/2007/08/29/1763471.aspx

一:函数调用约定:
函数调用约定是函数调用者和被调用的函数体之间关于参数传递、返回值传递、堆栈清除、寄存器使用的一种约定; 它是需要二进制级别兼容的强约定,函数调用者和函数体如果使用不同的调用约定,将可能造成程序执行错误,必须把它看作是函数声明的一部分;

参考文章:

 http://liue.spaces.live.com/blog/cns!d126ff4c28b17ad1!237.entry 

 http://dev.csdn.net/article/52/52485.shtm

二:常见的调用约定:

VC6中的函数调用约定;

调用约定   堆栈清除    参数传递
__cdecl     调用者       从右到左,通过堆栈传递
__stdcall    函数体      从右到左,通过堆栈传递
__fastcall   函数体      从右到左,优先使用寄存器(ECX,EDX) , 然后使用堆栈
thiscall       函数体       this指针默认通过 ECX 传递, 其它参数从右到左入栈

__cdecl 是 C/C++ 的默认调用约定; VC 的调用约定中并没有 thiscall 这个关键字,它是类成员函数默认调用约定;
C/C++中的main(或wmain)函数的调用约定必须是 __cdecl , 不允许更改;

默认调用约定一般能够通过编译器设置进行更改,如果你的代码依赖于调用约定,请明确指出需要使用的调用约定;

Delphi6中的函数调用约定;

调用约定  堆栈清除   参数传递
register     函数体        从左到右, 优先使用寄存器( EAX , EDX , ECX ),然后使用堆栈
pascal      函数体        从左到右, 通过堆栈传递
cdecl        调用者        从右到左, 通过堆栈传递(与C/C++默认调用约定兼容)
stdcall      函数体        从右到左, 通过堆栈传递(与VC中的__stdcall兼容)
safecall     函数体       从右到左, 通过堆栈传递(同stdcall)

Delphi中的默认调用约定是 register , 它也是我认为最有效率的一种调用方式, 而cdecl是我认为综合效率最差的一种调用方式;
VC中的__fastcall调用约定一般比register效率稍差一些;

C++Builder6中的函数调用约定;

调用约定 堆栈清除 参数传递
__fastcall     函数体       从左到右,优先使用寄存器(EAX,EDX,ECX), 然后使用堆栈 (兼容Delphi的register)
                                    (register与__fastcall等同)
__pascal      函数体       从左到右,通过堆栈传递
__cdecl        调用者       从右到左,通过堆栈传递(与C/C++默认调用约定兼容)
__stdcall      函数体       从右到左,通过堆栈传递(与VC中的__stdcall兼容)
__msfastcall 函数体       从右到左,优先使用寄存器(ECX,EDX),然后使用堆栈(兼容VC的__fastcall)

VB一般使用的是stdcall调用约定;(ps:有更强的保证吗)  

A:  测试代码:


int x;
int __cdecl add(int a,int b) { return a+b; }//使用__cdecl调用约定
int main(int argc, char* argv[])
{
      x=add(1,2);
    return 0;
}

; Debug模式编译后得到的汇编代码

PUBLIC ?x@@3HA ; x
_BSS SEGMENT
?x@@3HA DD 01H DUP (?) ; x 变量
_BSS ENDS
PUBLIC ?add@@YAHHH@Z ; add
PUBLIC _main
EXTRN __chkesp:NEAR
; COMDAT _main
_TEXT SEGMENT

_main PROC NEAR ; COMDAT //main函数体

push ebp          ; //保存ebp的值到堆栈,退出函数前用pop ebp恢复
mov ebp, esp   ; //ebp指向当前堆栈; 函数中可以通过ebp来进行堆栈访问
sub esp, 64      ; //在堆栈中开辟64byte局部空间

; //说明:这三条汇编指令是很多函数体开始的惯用法;
; //用ebp指向堆栈(不会改变);并通过ebp来访问参数和局部变量;  

push ebx ; //一般按照函数间调用的约定,函数中可以自由使用eax,ecx,edx;
push esi ; //其它寄存器如果需要使用则需要保存,用完时恢复;也就是寄存器的使用约定; 这也使函数调用约定的一部分;
push edi ; //即:在函数中调用了别的函数后,eax,ecx,edx很可能已经改变,
                 ; //而其它寄存器(ebx,esi,edi,ebp ) 的值可以放心继续使用( esp除外 )

lea edi, DWORD PTR [ebp-64]
mov ecx, 16 ; 00000010H
mov eax, -858993460 ; ccccccccH
rep stosd ; //前面开辟的( 16*4 ) byte 局部空间全部填充 0xCC
                  ; // 注意: 0xCC 是调试中断(__asm int 3 ) 的指令码 , 所以可以想象,当 
                  ; // 程序错误的跳转到这个区域进行执行时将产生调试中断

push 2                 ; //代码: x=add(1,2);
push 1                  ; //从右到左入栈 (__cdecl调用约定!!!)
call ?add@@YAHHH@Z     ; 调用add函数;  call指令将把下一条指令的地址(返回地址)压入堆栈
add esp, 8                ; add函数调用完以后,调用者负责清理堆栈 (__cdecl调用约定!!!)
                                   ; 两个int型参数共使用了8byte空间的堆栈
mov DWORD PTR ?x@@3HA, eax   ; 将add函数的返回值存入x变量中 , 可以看出add函数的返回值放在eax中
xor eax, eax ;            // 原代码:return 0; 执行eax清零,main函数的返回值0放在eax中

pop edi
pop esi
pop ebx ;           // 恢复edi,esi,ebx寄存器
add esp, 64 ;    // 恢复64byte局部空间
cmp ebp, esp
call __chkesp ; //到这里时应该ebp==esp, Debug版进行确认,如果不等,抛出异常等
mov esp, ebp
pop ebp ;          //恢复ebp寄存器
ret 0
_main ENDP
_TEXT ENDS

;//下面是add函数的代码,就不用解释的像上面那么详细了

; COMDAT ?add@@YAHHH@Z
_TEXT SEGMENT
_a$ = 8          ;// 参数a相对于堆栈偏移8
_b$ = 12       ;// 参数b相对于堆栈偏移12

?add@@YAHHH@Z PROC NEAR ; add, COMDAT //add函数体

push ebp
mov ebp, esp
sub esp, 64 ; 00000040H
push ebx
push esi
push edi
lea edi, DWORD PTR [ebp-64]
mov ecx, 16 ; 00000010H
mov eax, -858993460 ; ccccccccH
rep stosd

mov eax, DWORD PTR _a$[ebp] ;将参数a的值移动到eax
add eax, DWORD PTR _b$[ebp] ;将参数b的值累加到eax; 可以看出返回值通过eax返回

pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret 0 ; 函数体不管堆栈的参数清理 (__cdecl调用约定!!!)
; ret指令将取出call指令压入的返回地址,并跳转过去继续执行

?add@@YAHHH@Z ENDP ; add
_TEXT ENDS
END

; 再来看一下Release模式编译后得到的汇编代码
; 可以看出,这比Debug模式少了很多的汇编指令,速度当然可能更快了;不再做详细说明了,请对照上面的解释


PUBLIC ?x@@3HA ; x
_BSS SEGMENT
?x@@3HA DD 01H DUP (?) ; x
_BSS ENDS
PUBLIC ?add@@YAHHH@Z ; add
PUBLIC _main
; COMDAT _main
_TEXT SEGMENT

_main PROC NEAR ; COMDAT //main函数体

push 2
push 1 ; //从右到左入栈 (__cdecl调用约定!!!)
call ?add@@YAHHH@Z ; //调用add函数;
mov DWORD PTR ?x@@3HA, eax ; x
add esp, 8 ; //调用者负责清理堆栈 (__cdecl调用约定!!!)

xor eax, eax
ret 0
_main ENDP
_TEXT ENDS

; COMDAT ?add@@YAHHH@Z
_TEXT SEGMENT
_a$ = 8
_b$ = 12

?add@@YAHHH@Z PROC NEAR ; add, COMDAT //add函数体

mov eax, DWORD PTR _b$[esp-4] ;将参数b的值移动到eax
mov ecx, DWORD PTR _a$[esp-4] ;将参数a的值移动到ecx
add eax, ecx ;将ecx的值累加到eax; 返回值通过eax传递
ret 0 ;函数体不管堆栈的参数清理 (__cdecl调用约定!!!)
?add@@YAHHH@Z ENDP ; add
_TEXT ENDS
END

 

下面的分析中将只给出Release模式编译后的汇编代码

B:   声明add函数为__stdcall调用约定

int x;
int __stdcall add(int a,int b) { return a+b; }
int main(int argc, char* argv[])
{
x=add(1,2);
return 0;
}

;来看产生的汇编代码:

; //main函数体
push 2
push 1 ; //从右到左入栈
call ?add@@YGHHH@Z ; add
mov DWORD PTR ?x@@3HA, eax ; x
xor eax, eax
ret 0

; //add函数体
mov eax, DWORD PTR _b$[esp-4]
mov ecx, DWORD PTR _a$[esp-4]
add eax, ecx
ret 8 ; //函数体负责清栈 ;两个int型参数共使用了8byte空间的堆栈

C:  声明add函数为__fastcall调用约定

int x;
int __fastcall add(int a,int b) { return a+b; }
int main(int argc, char* argv[])
{
x=add(1,2);
return 0;
}

;来看产生的汇编代码:


; //main函数体
mov edx, 2                   ; b通过寄存器edx传递
mov ecx, 1                   ; a通过寄存器ecx传递
call ?add@@YIHHH@Z      ; add
mov DWORD PTR ?x@@3HA, eax       ; x
xor eax, eax
ret 0

; //add函数体
lea eax, DWORD PTR [ecx+edx] ; //a,b参数值已经在ecx,edx中,该句将这两个值的和放到eax作为返回值;
ret 0                 ; //这里应该函数体负责清栈 ;但因为两个参数已经通过寄存器传递  ; //了,没有使用堆栈,所以ret 0;

; //T::add函数体
mov eax, DWORD PTR [ecx]            ; //通过this指针(保存在ecx)将start0的值移动到eax
mov ecx, DWORD PTR _a$[esp-4] ; //把a的值移动到ecx; this的值将丢失,但函数体中已经不需要了
add eax, ecx           ; //将a的值累加到eax
mov ecx, DWORD PTR _b$[esp-4]  ; //把b的值移动到ecx;
add eax, ecx         ; //将b的值累加到eax
ret 8                       ; //函数体负责清栈 ; 

五: 其他

1.在VC中实现一个函数体时可以使用__declspec(naked)声明,它告诉编译器,不要为函数体自动产生开始和结束码;
2.在VC6中,想得到汇编代码清单,设置方法为:

引用:[Project]->[Setting...]->[C++]->[Category:]->[Listing Files]->[Listing file type:]->[Assembily ,...]

3. VC6中嵌入汇编代码的方式为:

__asm { <汇编语句s> }  或  __asm <一条汇编语句> 

4.VC6中重新设定函数使用的默认调用约定的方法是:
   引用:
   在 [ Project ]->[ Setting...]->[ C++ ]->[ Project Options: ]中增加编译设置
   比如:/Gd 代表__cdecl;   /Gr 代表__fastcall;  /Gz 代表__stdcall 

总结:

 1._stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。

    2、C调用约定(即用__cdecl关键字说明)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。

    _cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀。是MFC缺省调用约定。

   3、__fastcall调用约定是“人”如其名,它的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。

    _fastcall方式的函数采用寄存器传递参数,VC将函数编译后会在函数名前面加上"@"前缀,在函数名后加上"@"和参数的字节数。   

    4、thiscall仅仅应用于“C++”成员函数。this指针存放于CX寄存器,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。

    5、nakedcall采用1-4的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。naked call不产生这样的代码。naked call不是类型修饰符,故必须和_declspec共同使用。

    关键字__stdcall、__cdecl和__fastcall可以直接加在要输出的函数前,也可以在编译环境的Setting.../C/C++/CodeGeneration项选择。当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效。它们对应的命令行参数分别为/Gz、/Gd和/Gr。缺省状态为/Gd,即__cdecl。

   要完全模仿PASCAL调用约定首先必须使用__stdcall调用约定,至于函数名修饰约定,可以通过其它方法模仿。还有一个值得一提的是WINAPI宏,Windows.h支持该宏,它可以将出函数翻译成适当的调用约定,在WIN32中,它被定义为__stdcall。使用WINAPI宏可以创建自己的APIs。

常见的函数调用约定中,只有 cdecl 约定需要调用者来清除堆栈;
C/C++中的函数支持参数数目不定的参数列表, 比如printf函数;由于函数体不知道调用者在堆栈中压入了多少参数,所以函数体不能方便的知道应该怎样清除堆栈,那么最好的办法就是把清除堆栈的责任交给调用者;
这应该就是cdecl调用约定存在的原因吧;
Windows的API中,一般使用的是 stdcall 约定; 建议在不同语言间的调用中(如DLL)最好采用stdcall调用约定,因为它在语言间兼容性支持最好;


原创粉丝点击