翻译:郭真林
10/22/2010 日
开发平台及工具:Visual Studio 2003.NET, Intel X86
原文地址:http://msdn.microsoft.com/en-us/library/k2b2ssfy(v=VS.71).aspx
3.3. 编写Prolog/Epilog代码的思考... 10
Visual C/C++编译器提供了几种不同的调用内部程外部函数的协定。理解这些不同的协定可助于调试程序和将你的代码与由汇编编写的子函数链接。 本主题解释了这些调用协定间的不同:参数是怎样被传递的,以及函数是怎样返回值的。还将讨论裸函数调用—一种使你可以编写你自己的prolog和epilog代码。
所有的参数当它们被传递时都被扩展为32位。返回值也被扩展为32位并且存在EAX寄存器中,除了8字节的结构,它被存在EDX:EAX寄存器对中。大数据结构返回值的指针被存在EAX寄存器中以隐藏返回结构。参数被依次从右往左被压入堆栈。
编译器生成prolog和epilog代码来保存ESI,EDI,EBX和EBP寄存器(如果它们在函数中被使用的话)。
注意 当一个结构,联合或类作为一个函数的返回值时,这种类型的所有定义需要相同,否则程序在运行时可能运行失败。
关于怎样定义你自己函数的prolog与epilog代码,详情请看的裸函数调用.
Visual C/C++编译器支持以下调用协定.
关键字 | 堆栈清理 | 参数传递 |
调用者 | 从右到左将参数压入堆栈 | |
被调用者 | 从右到左将参数压入堆栈 | |
被调用者 | 存储在寄存器中,多出的压入堆栈 | |
thiscall(非关键字) | 被调用者 | 压入堆栈,this 指针存入ECX |
它是C和C++程序的默认调用协定.因为它们堆栈由调用者清理,它可以实现可变参数函数。 __cdecl 调用协定比__stcall生成更大的可执行文件,因为它要求每个调用它的函数都要有堆栈清理的代码。下面列出了这种调用协定的实现。
元素 | 实现 |
参数传递顺序 | 从右到左 |
堆栈维护 | 调用函数将参数从堆栈中弹出 |
名称修饰协定 | 下划线作(_)名称前缀 |
大小写转换协定 | 不执行大小写转换 |
注意 相关信息请看 Decorated Names.
将 __cdecl 修饰符置在变量或函数名称前.因为C命名与调用协定是默认的,你唯一需要用__cdecl 是当你指定 /Gz (stdcall) 或 /Gr (fastcall) 编译选项时. /Gd 编译选项强制使用 __cdecl 调用协定.
示例
// Example of the __cdecl keyword on function
_CRTIMP int __cdecl system(const char *);
// Example of the __cdecl keyword on function pointer
typedef BOOL (__cdecl *funcname_ptr)(void * arg1, const char * arg2, DWORD flags, ...);
__stdcall 调用协定用于调用Win32API函数. 被调用者负责清理堆栈。使用这种协定的函数原型要求为:
return-type __stdcall function-name[(argument-list)]
下面列出了这种协定的实现:
元素 | 实现 |
参数传递顺序 | 从右到左 |
参数传递协定 | 按值传递,除非传递的是一个指针或引用类型 |
堆栈维护 | 被调用函数从栽倒弹出它自己的参数 |
名称修饰协定 | 下划线作名称前缀.名称后跟@,然后是对应参数的字节大小。因此,函数声明为 |
大小写转换协定 | 无 |
/Gz 编译选项规定所有没有显示声明的调用协定的函数的调用协定为 __stdcall
用 __stdcall 修饰符声明的函数返回值的方式与用 __cdecl修饰的函数方式相同。
示例
在下面的例子中, __stdcall 使所有的WINAPI函数类型以标准调用方式来处理:
// Example of the __stdcall keyword
#define WINAPI __stdcall
// Example of the __stdcall keyword on function pointer
typedef BOOL (__stdcall *funcname_ptr)(void * arg1, const char * arg2, DWORD flags, ...);
__fastcall 调用协定规定当可能的时候函数的参数以寄存器传递.下面列出了这种调用协定的实现。
元素 | 实现 |
参数传递顺序 | 前两个双字或更小的参数通过ECX或EDX寄存器传递;其它参数从右向左依次入栈 |
堆栈维护 | 被调用函数从堆栈弹出参数 |
名称修饰协定 | @作名称前缀;后再跟对应参数的字节大小;再跟一个@号后缀 |
大小写转换协定 | 不执行大小定转换 |
注意 将来的编译器版本可能会使用不同的寄 存器存储参数。
/Gr 编译选项使模块中的每个函数被编译为fastcall,除非这个函数声明了一个冲突的属性或这个函数是main函数。
示例
下面的例子,函数 DeleteAggrWrapper
的参数由寄存器传递
:
// Example of the __fastcall keyword
#define FASTCALL __fastcall
void FASTCALL DeleteAggrWrapper(void* pWrapper);
// Example of the __ fastcall keyword on function pointer
typedef BOOL (__fastcall *funcname_ptr)(void * arg1, const char * arg2, DWORD flags, ...);
这是C++成员函数默认的调用协定,它并没有使用可变参数。堆栈由被调用者清理,可变参数就不可能了。参数从右向式依次入栈,而this指针通过ECX寄存器(X86体系结构)传递. thiscall调用协定不能在函数中显示地指定,因为它不是一个关键字.
带可变参数的成员使用__cdecl调用协定。所有函数参数被压入堆栈,this指针最后压入堆栈。因为此调用协定只用于C++,所以没有C的名称修饰协定。
:函数原型与调用
这个例子基于下面的函数模板。用合适的调用协定替换calltype 即可.
void calltype MyFunc( char c, short s, int i, double f );
.
.
.
void MyFunc( char c, short s, int i, double f )
{
.
.
.
}
.
.
.
MyFunc ('x', 12, 8192, 2.7183);
2.1.1. __cdecl
C 修饰的函数名是 "_MyFunc."
__cdecl 调协定
C修饰名为 (__stdcall) "_MyFunc@20." C++ 修饰名称是专有的.
__stdcall 和thiscall 调用协定
C 修饰名(__fastcall)为 "@MyFunc@20." C++ 修饰名称是专有的。
__fastcall 调用协定
用 naked 属性声明的函数不会发射prolog 或 epilog 代码code,这使你可以用内联汇编编写自己的prolog/epilog序列。 裸函数是一种提供的高级特性。它们使你可以声明一种从从上下文调用的函数而不是从C/C++,因此可以做对参数的位置,保存的寄存器做不同的假设。例如中断处理函数。这个特性对虚拟设备(VXD)开发者相当有用。
对于用 naked属性声明的函数,编译器生成代码时不会插入prolog/epilog代码。你可使用这个特性用内联汇编编写你自己的 prolog/epilog 代码序列。这个特性对虚拟设备(VXD)开发者相当有用。
__declspec( naked ) declarator
因为 naked 属性仅与一个函数的定义有关而非类型修饰符,裸函数必须使用扩展属性语法和__declspec 关键字.
编译器不能为一个标记了naked属性的函数生成内联函数,即使这个函数标记为 __forceinline。
示例
这段代码定义一个naked属性的函数:
__declspec( naked ) int func( formal_parameters )
{
// 函数体
}
或:
#define Naked __declspec( naked )
Naked int func( formal_parameters )
{
// 函数体
}
naked 属性只影响编译器是否生成这个函数prolog 和 epilog 序列.并不影响调用这种函数的代码生生成。因此 naked 属性并不认为是函数类型的一部分,并且函数指针并没有naked 属性.此外 naked 属性不能用于数据定义。例如下面的代码会产生错误:
__declspec( naked ) int i; // Error--naked attribute not
// permitted on data declarations.
naked 仅与函数的定义有关,并不能不在函数的原型中指定。例如下面的声明是会产生编译错误:
__declspec( naked ) int func(); // Error--naked attribute not
// permitted on function declarations
- 不允许有 return 语句.
- 结构化异常处理和C++异常处理不允许,因为它们必须跨栈帧展开(unwind).
- 出于某些原因,任何形式的setjmp 都不允许.
- 不允许使用 _alloca 函数.
- 要确保没有局部变量的初始化代码出现在prolog序列之前,已初始化局部变量在函数范围是不允许的。具体地说,C++对象的声明不允许出现在函数范围中。然而可能有已初始化数据在内嵌范围中。
- 不推荐帧指针优化 ( /Oy 编译选项),但是对于一个裸函数它自动被忽略。
- 你不能在函数的词法范围内删除C++类对象。然而你可在内嵌块中声明一个对象。当有编译参数/clr时, naked 关键字被忽略。
- 对采用 __fastcall 调用协定的裸函数,在C/C++代码中有个对寄存器参数之一的一个引用,prolog代码应该存储那个寄存器的值到那个变量的堆栈位置。例如:
// nkdfastcl.cpp
__declspec(naked) int __fastcall power(int i, int j)
{
/* calculates i^j, assumes that j >= 0 */
/* prolog */
__asm {
push ebp
mov ebp, esp
sub esp, __LOCAL_SIZE
// store ECX and EDX into stack locations allocated for i and j
mov i, ecx
mov j, edx
}
{
int k=1; // return value
while (j-- > 0) k *= i;
__asm { mov eax, k };
}
/* epilog */
__asm
{
mov esp, ebp
pop ebp
ret
}
}
int main()
{
}
在编写你自己的prolog和epilog代码前,理解栈帧是如何布局相当重要。这对理解如何使用__LOCAL_SIZE 符号也有帮助.
3.3.1. 栈帧布局
这个例子展示了可能出现在一个32位函数中的标准prolog代码:
push ebp ; 保存 ebp
mov ebp, esp ; 设置栈帧指针
sub esp, localbytes ; 为局部变量分配空间
push <registers> ; 保存寄存器
localbytes
变量表示局部变量所需要的堆栈空间的字节数, <registers>
变量是一个占位符,表示被保存到堆栈的寄存器列表。在将寄存器压入堆栈后,你可以放置任何合适的数据到堆栈上。下面是相应的epilog代码:
pop <registers> ; 恢复寄存器
mov esp, ebp ; 恢复堆栈指针
pop ebp ; 恢复ebp
ret ; 返回
堆栈总是向下生长(从高地址向低地址)。基址指针指向压入 ebp中的值。局部变量区从ebp-2开始。要访问局部变量,计算距ebp的偏移量—ebp减去一个合适的值。
3.3.2. __LOCAL_SIZE
编译器提供了一个符号 __LOCAL_SIZE ,用于函数prolog代码的内联汇编块。在自定义的prolog代码中,这个符号用于在栈帧上为局部变量分配空间。
编译器确定 __LOCAL_SIZE 的值.它是用户定义的局部变量和编译器生成临时变量的总的字节数。 __LOCAL_SIZE 仅能被用作立即操作数,而不能用在表达式中。你不能改变蔌重定义它的值。例如:
mov eax, __LOCAL_SIZE ;Immediate operand--Okay
mov eax, [ebp - __LOCAL_SIZE] ;Error
下面的例子是一个裸 函数包含了自定义的prolog和epilog序列,在prolog序列中使用了 __LOCAL_SIZE 符号:
// the__local_size_symbol.cpp
__declspec ( naked ) main()
{
int i;
int j;
__asm /* prolog */
{
push ebp
mov ebp, esp
sub esp, __LOCAL_SIZE
}
/* Function body */
__asm /* epilog */
{
mov esp, ebp
pop ebp
ret
}
}
如果你正为浮点协处理器写汇编函数,你必须保存浮点控制字和清理协处理器堆栈,除非你正返加一个float或double值,这种情况你的函数应将返回存在ST(0)中。
__pascal, __fortran,和 __syscall 调用协定不再受支持。你可以用受支持的调用协定和合适的链接器选项来模拟它们的功能。
WINDOWS.H 现在支持 WINAPI 宏,它将为目标平台转换成合适的调用协定。在你先前使用 PASCAL 或 __far __pascal 的地方使用WINAPI。