几乎每一种语言都有函数的概念,而作为函数就有参数,一般的说,参数的传递是通过堆栈来实现的(堆栈是一种先入后出的结构,使用Push压入,使用Pop弹出,Push和Pop必须成对使用),不过有两种不同的处理方法。
一种方法是按原顺序把参数压入堆栈,然后使用CALL指令呼叫函数的地址,函数使用POP将参数弹出堆栈然后处理。由于堆栈的先入后出特性,所以这种方法对于调用者有利,因为被调用的函数得到的反序的参数。
另一种方法相反,是按反序把参数压入堆栈,然后使用CALL指令呼叫函数的地址,函数使用POP将参数弹出堆栈然后处理。这种方法对于被调用者有利,因为被调用的函数得到的正序的参数。
C语言和Pascal语言分别使用这两种方式,而Windows使用的调用方式和Pascal相同,所以以前的C程序编写Windows程序的时候需要使用关键字PASCAL指明使用Pascal调用规则;现在一般的不使用PASCAL关键字,而是使用_stdcall说明符,表明是一个标准调用。这种现在称为标准调用的就是第二种方式——反序压栈。
函数调用约定有_stdcall、_cdcel、_fastcall、thiscall和naked call等多种,下面分别介绍如下:
_stdcall:该调用约定相当于在16位DLL中经常使用的PASCAL调用约定。在32位的Visual C++中PASCAL调用约定不再被支持(实际上它已被定义为_stdcall,除_pascal外,_fortran和_syscall也不被支持),取而代之的是_stdcall调用约定。两者实质上是一致的,即函数的参数自右向左通过栈传递,由被调用函数在返回前负责清理传送参数的内存栈,不同的是函数名的修饰部分(有关函数名的修饰部分将在后面详细说明)。_stdcall也是PASCAL程序的缺省调用方式,多用在Win32 API函数中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。Visual C++将函数编译后会在函数名前加上下划线前缀,在函数名后加上“@”和参数的字节数。
_cdecl:即用_cdecl关键字说明的C调用约定。此调用约定是按照从右向左的顺序对参数来压栈的,由调用者负责参数的参数的出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。_cdecl是C和C++程序的默认调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件会比调用_stdcall函数的要大。函数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀,这也是MFC默认的函数调用约定。
_fastcall:该调用约定的主要特点就是速度快,因为它是通过寄存器来传送参数的(是用ECX和EDX寄存器传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈)。在函数名约定方面,它和前两者均不同。采用_fastcall调用约定的函数在编译后将在函数名前加上“@”前缀,在函数名后加上“@”和参数的字节数。
thiscall:该调用约定仅应用于C++成员函数。this指针存放于CX寄存器,参数从右向左压栈。需要注意的是thiscall并非一个关键词,因此不能被程序员所指定。
naked call:对于前面四种函数调用约定,如果必要,在进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器的内容并在退出函数时产生代码来进行恢复。而使用naked call调用约定的函数则不产生这样的代码。需要特别指出的是,naked call并非类型修饰符,必须和_declspec共同使用。
关键字_stdcall、_cdecl和_fastcall均可以直接加在要输出的函数前,也可以在编译环境中设置。如果加在输出函数前的关键字与编译环境中的设置不同,则以所加的关键字为准。其对应的命令行参数分别为/Gz、/Gd和/Gr。默认状态为/Gd,即_cdecl。若要完全模仿PASCAL调用约定首先必须使用_stdcall调用约定,至于函数名修饰约定,可以通过其他方法模仿。还有一个值得一提的是WINAPI宏,在windows.h中有定义,它可以将输出函数翻译成适当的调用约定,在Win32中,它被定义为_stdcall。使用WINAPI宏可以创建自己的API函数。
修饰名约定随调用约定和编译种类(C和C++)的不同而变化。下面分别就C编译时和C++编译时的修饰名约定规则进行介绍。
(1)C编译时函数名修饰约定规则。
_stdcall调用约定:在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_functionname@number。
_cdecl调用约定:仅在输出函数名前加上一个下划线前缀,格式为_functionname。
_fastcall调用约定:在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,格式为@functionname@number。与PASCAL调用约定不同,它们均不改变输出函数名的字符大小写(PASCAL约定输出的函数名无任何修饰且全部大写)。
(2)C++编译时函数名修饰约定规则。_stdcall调用约定:以“?”标识函数名的开始,后跟函数名。函数名后面以@@YG标志参数表的开始,后跟参数表。下面给出了参数表的代号表示。
X void
D char
E unsigned char
F short
H int
I unsigned int
J long
K unsigned long
M float
N double
_N bool
PA 指针。其后为指针类型,如相同的指针连续出现,则以0代替。
参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型。指针标识在其所指的数据类型前。在参数表后以@Z标识整个名字的结束,如果该函数没有参数,则以Z标识结束。其格式为?functionname@YG*****@Z或?functionname@@YG*XZ,例如:int Test1(char * var , unsigned long)表示为?Test1@@YGHPADK@Z,而void Test2()则表示为?Test2@@YGXXZ。
对于_cdecl调用约定和_fastcall调用约定,规则同上面的_stdcall调用约定基本类似,只是参数表的开始标识由上面的@@YG分别变为@@YA和@@YI。Visual C++对函数的缺省声明是_cdecl,只能为C/C++所调用。
一种方法是按原顺序把参数压入堆栈,然后使用CALL指令呼叫函数的地址,函数使用POP将参数弹出堆栈然后处理。由于堆栈的先入后出特性,所以这种方法对于调用者有利,因为被调用的函数得到的反序的参数。
另一种方法相反,是按反序把参数压入堆栈,然后使用CALL指令呼叫函数的地址,函数使用POP将参数弹出堆栈然后处理。这种方法对于被调用者有利,因为被调用的函数得到的正序的参数。
C语言和Pascal语言分别使用这两种方式,而Windows使用的调用方式和Pascal相同,所以以前的C程序编写Windows程序的时候需要使用关键字PASCAL指明使用Pascal调用规则;现在一般的不使用PASCAL关键字,而是使用_stdcall说明符,表明是一个标准调用。这种现在称为标准调用的就是第二种方式——反序压栈。
函数调用约定有_stdcall、_cdcel、_fastcall、thiscall和naked call等多种,下面分别介绍如下:
_stdcall:该调用约定相当于在16位DLL中经常使用的PASCAL调用约定。在32位的Visual C++中PASCAL调用约定不再被支持(实际上它已被定义为_stdcall,除_pascal外,_fortran和_syscall也不被支持),取而代之的是_stdcall调用约定。两者实质上是一致的,即函数的参数自右向左通过栈传递,由被调用函数在返回前负责清理传送参数的内存栈,不同的是函数名的修饰部分(有关函数名的修饰部分将在后面详细说明)。_stdcall也是PASCAL程序的缺省调用方式,多用在Win32 API函数中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。Visual C++将函数编译后会在函数名前加上下划线前缀,在函数名后加上“@”和参数的字节数。
_cdecl:即用_cdecl关键字说明的C调用约定。此调用约定是按照从右向左的顺序对参数来压栈的,由调用者负责参数的参数的出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。_cdecl是C和C++程序的默认调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件会比调用_stdcall函数的要大。函数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀,这也是MFC默认的函数调用约定。
_fastcall:该调用约定的主要特点就是速度快,因为它是通过寄存器来传送参数的(是用ECX和EDX寄存器传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈)。在函数名约定方面,它和前两者均不同。采用_fastcall调用约定的函数在编译后将在函数名前加上“@”前缀,在函数名后加上“@”和参数的字节数。
thiscall:该调用约定仅应用于C++成员函数。this指针存放于CX寄存器,参数从右向左压栈。需要注意的是thiscall并非一个关键词,因此不能被程序员所指定。
naked call:对于前面四种函数调用约定,如果必要,在进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器的内容并在退出函数时产生代码来进行恢复。而使用naked call调用约定的函数则不产生这样的代码。需要特别指出的是,naked call并非类型修饰符,必须和_declspec共同使用。
关键字_stdcall、_cdecl和_fastcall均可以直接加在要输出的函数前,也可以在编译环境中设置。如果加在输出函数前的关键字与编译环境中的设置不同,则以所加的关键字为准。其对应的命令行参数分别为/Gz、/Gd和/Gr。默认状态为/Gd,即_cdecl。若要完全模仿PASCAL调用约定首先必须使用_stdcall调用约定,至于函数名修饰约定,可以通过其他方法模仿。还有一个值得一提的是WINAPI宏,在windows.h中有定义,它可以将输出函数翻译成适当的调用约定,在Win32中,它被定义为_stdcall。使用WINAPI宏可以创建自己的API函数。
修饰名约定随调用约定和编译种类(C和C++)的不同而变化。下面分别就C编译时和C++编译时的修饰名约定规则进行介绍。
(1)C编译时函数名修饰约定规则。
_stdcall调用约定:在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_functionname@number。
_cdecl调用约定:仅在输出函数名前加上一个下划线前缀,格式为_functionname。
_fastcall调用约定:在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,格式为@functionname@number。与PASCAL调用约定不同,它们均不改变输出函数名的字符大小写(PASCAL约定输出的函数名无任何修饰且全部大写)。
(2)C++编译时函数名修饰约定规则。_stdcall调用约定:以“?”标识函数名的开始,后跟函数名。函数名后面以@@YG标志参数表的开始,后跟参数表。下面给出了参数表的代号表示。
X void
D char
E unsigned char
F short
H int
I unsigned int
J long
K unsigned long
M float
N double
_N bool
PA 指针。其后为指针类型,如相同的指针连续出现,则以0代替。
参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型。指针标识在其所指的数据类型前。在参数表后以@Z标识整个名字的结束,如果该函数没有参数,则以Z标识结束。其格式为?functionname@YG*****@Z或?functionname@@YG*XZ,例如:int Test1(char * var , unsigned long)表示为?Test1@@YGHPADK@Z,而void Test2()则表示为?Test2@@YGXXZ。
对于_cdecl调用约定和_fastcall调用约定,规则同上面的_stdcall调用约定基本类似,只是参数表的开始标识由上面的@@YG分别变为@@YA和@@YI。Visual C++对函数的缺省声明是_cdecl,只能为C/C++所调用。