Visual C/C++调用协定

 

翻译:郭真林

guozhenlin@163.com

 

10/22/2010

 

 

开发平台及工具:Visual Studio 2003.NET, Intel X86

 

原文地址:http://msdn.microsoft.com/en-us/library/k2b2ssfy(v=VS.71).aspx

 

 

目录

Visual C/C++调用协定... 1

摘要:... 3

1.      参数传递与命名协定... 3

1.1.       __cdecl 3

1.2.       __stdcall 4

1.3.       __fastcall 5

1.4.       thiscall 6

2.         调用举例:函数原型与调用... 6

2.1.       调用的结果... 6

2.1.1.         __cdecl 6

2.1.2.         __stdcall thiscall 7

2.1.3.         __fastcall 7

3.         裸函数调用... 8

3.1.       裸函数... 8

3.2.       裸函函数的规则与限制... 9

3.3.       编写Prolog/Epilog代码的思考... 10

3.3.1.         栈帧布局... 10

3.3.2.         __LOCAL_SIZE. 10

4.         浮点协处理器与调用协定... 11

5.         废弃的调用协定... 11

 

 


 

Visual C/C++编译器提供了几种不同的调用内部程外部函数的协定。理解这些不同的协定可助于调试程序和将你的代码与由汇编编写的子函数链接。 本主题解释了这些调用协定间的不同:参数是怎样被传递的,以及函数是怎样返回值的。还将讨论裸函数调用一种使你可以编写你自己的prologepilog代码。

 

所有的参数当它们被传递时都被扩展为32位。返回值也被扩展为32位并且存在EAX寄存器中,除了8字节的结构,它被存在EDX:EAX寄存器对中。大数据结构返回值的指针被存在EAX寄存器中以隐藏返回结构。参数被依次从右往左被压入堆栈。

编译器生成prologepilog代码来保存ESI,EDI,EBXEBP寄存器(如果它们在函数中被使用的话)。

注意    当一个结构,联合或类作为一个函数的返回值时,这种类型的所有定义需要相同,否则程序在运行时可能运行失败。

关于怎样定义你自己函数的prologepilog代码,详情请看的裸函数调用.

Visual C/C++编译器支持以下调用协定.

关键字

堆栈清理

参数传递

__cdecl

调用者

从右到左将参数压入堆栈

__stdcall

被调用者

从右到左将参数压入堆栈

__fastcall

被调用者

存储在寄存器中,多出的压入堆栈

thiscall(非关键字)

被调用者

压入堆栈,this 指针存入ECX

它是CC++程序的默认调用协定.因为它们堆栈由调用者清理,它可以实现可变参数函数。 __cdecl 调用协定比__stcall生成更大的可执行文件,因为它要求每个调用它的函数都要有堆栈清理的代码。下面列出了这种调用协定的实现

元素

实现

参数传递顺序

从右到左

堆栈维护

调用函数将参数从堆栈中弹出

名称修饰协定

下划线作(_)名称前缀

大小写转换协定

不执行大小写转换

注意  相关信息请看 Decorated Names.

__cdecl 修饰符置在变量或函数名称前.因为C命名与调用协定是默认的,你唯一需要用__cdecl 是当你指定 /Gz (stdcall) /Gr (fastcall) 编译选项时. /Gd 编译选项强制使用 __cdecl 调用协定.

示例

下列代码,为系统函数配置编译器使用C命名和调用协定:

// 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)]

下面列出了这种协定的实现:

元素

实现

参数传递顺序

从右到左

参数传递协定

按值传递,除非传递的是一个指针或引用类型

堆栈维护

被调用函数从栽倒弹出它自己的参数

名称修饰协定

下划线作名称前缀.名称后跟@,然后是对应参数的字节大小。因此,函数声明为 int func( int a, double b )被修饰为 _func@12

大小写转换协定

 /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 调用协定规定当可能的时候函数的参数以寄存器传递.下面列出了这种调用协定的实现。

元素

实现

参数传递顺序

前两个双字或更小的参数通过ECXEDX寄存器传递;其它参数从右向左依次入栈

堆栈维护

被调用函数从堆栈弹出参数

名称修饰协定

@作名称前缀;后再跟对应参数的字节大小;再跟一个@号后缀

大小写转换协定

不执行大小定转换

注意  将来的编译器版本可能会使用不同的寄 存器存储参数。

 /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()

{

}

在编写你自己的prologepilog代码前,理解栈帧是如何布局相当重要。这对理解如何使用__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

下面的例子是一个裸 函数包含了自定义的prologepilog序列,在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
      }
}

如果你正为浮点协处理器写汇编函数,你必须保存浮点控制字和清理协处理器堆栈,除非你正返加一个floatdouble值,这种情况你的函数应将返回存在ST(0)中。

__pascal__fortran, __syscall 调用协定不再受支持。你可以用受支持的调用协定和合适的链接器选项来模拟它们的功能。

WINDOWS.H 现在支持 WINAPI ,它将为目标平台转换成合适的调用协定。在你先前使用 PASCAL  __far __pascal 的地方使用WINAPI

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值