浅析C和C++中的调用约定

转帖|其它|编辑:郝浩|2009-02-20 11:05:10.000|阅读 729 次

概述:本文是一篇关于调用约定的小教程,旨在解释调用约定如何运作,并且与C和C + +中书写动态链接库以及在C#利用他们相关联。这可能对理解骨架代码有用。

# 界面/图表报表/文档/IDE等千款热门软控件火热销售中 >>

  当您开始使用动态链接库,编写涉及其他语言中的函数代码时,你会遇到诸如stdcall,safecall,cdecl和winapi之类的词。这些调用约定定义了如何在运行时调用外部函数,即使是单独编译,甚至可能是和不同的编译器和连接器。

  本文是一篇关于调用约定的小教程,旨在解释调用约定如何运作,并且与C和C + +中书写动态链接库以及在C#利用他们相关联。这可能对理解骨架代码有用。

  备注:这无关链接,而是使用动态数据库并在运行时间调用它们。

  一个调用约定定义了参数如何被传到堆栈,调用者或函数是否需要在结束调用后清理堆栈。

  关于堆栈

  堆栈追踪函数的调用来源。而这通常是硬件通过使用注册簿来完成的,注册簿是对记忆的指向标。按照惯例,一个堆栈开始是指向内存。随着每个项目都堆到堆栈上,堆栈指针就会减少。当你从堆栈上删除数据时。

  一般来说,编译器具备一些设置可以指定堆栈可得的内存数量。堆栈可以保存以下三种类型的数据:

  • 返回地址;
  • 函数参数;
  • 本地变量。

  CPU会有一些针对堆栈记忆分配的特殊指令。编译器会计算出所有本地变量和参数所需要的内存量,并且相应地对其进行内存分配。在调用快结束时,准确的反面指令被调用来减少同样的量。基本上,这些指令通过登记的数额减少了堆栈指针,并且在最后又相应地增加了空间。

  返回地址

  当一个函数被调用的时候,CPU要做的第一件事是在调用的函数生成时,计算出下一个指令是什么。下表就是对该过程进行的一个小小的演示:

  Address 101

  Address 102 Call Function 201

  Address 103...

  ...

  Address 151 Call Function 201

  Address 152...

  Address 201.. First instruction of function

  Address 202.. Allocate memory for local variables

  ...

  Address 206.. Release the allocated memory

  Address 207.. return from function

  在上面的例子中,如果CPU刚刚在101处理完指令,那么其下一步操作就是处理102的指令并且调用201的函数。那之后的指令就是103。它会一直持续到151直到再次调用201的函数。这次,当它返回时,它会一直与152的指令持续。地址103和152是保存在堆栈的返回地址。

  堆栈状态

  如果堆栈以5000的指标开始,它就会像这样:

  Stack Pointer= 5000

  Address 5000

  当函数201在202指令运行完后被调用,堆栈就会有数据。让我们想象一下这个函数没有参数但是有一个拥有本地变量总数,该总数是由10个整数组成的数组。

  int totals[10]

  如果每个地址保存一个整数,那么这个数组就有十个地点。在201,堆栈指数会减少10。

  Stack Pointer= 4989

  Address 5000 .. 103

  Address 4999 .. total[9]

  Address 4998 .. total[8]

  ...

  Address 4990 .. total[0]

  Address 4989 <- Current top of stack.
 
  执行206时,堆栈指针将增加10个。207的指令会弹到堆栈的极限。这一情况正是一种警告。与推送一起,某个值也被存储在当前地址中,该地址由堆栈指针持有,然后堆栈指针递减。膨胀使堆栈指针增加了1,然后从堆栈指针所持有的地址处得到值。因此,从5000(地址)得到值,即103 ,而这就是下一个要执行的指令。

  由数值审核还是由参考审核?

  前者把整个的变量复制到堆栈上,这不但很慢而且如果变量很大,可能使堆栈无法承担。例如,10,000个数组会减少10,000个堆栈指数并且把这10,000个整数的值到复制到堆栈上。相比之下,使用参考审核则要快得多,因为它只会把变量的地址送到堆栈上。所以在C语言中是用指数(*),在c++中使用参考(&)来保持其简便。[SPAN]

  调用约定

  了解参数是很重要的,比方,a,b,c,它们是按照这个顺序送到堆栈的吗?还是按照c,b,a的顺序?返回地址会去到哪里呢?同样,被调用的函数从堆栈删除参数还是从主程序?为什么不从登记簿通过数值来加速呢?指定发生的事情就是调用约定所做的事。

  约定列表

  主要的约定如下所示。

  function f(A, B, C)

  • cdecl 这是C和C + +中默认的。参数从右到左推入,从C到B然后到A。所谓的函数得到各种参数值,但不会改变原来的堆栈,因此,编译器在增加堆栈的请求之后要增加一个指令,使其平衡。不同的平台之间在注册使用,一些语言方面有差别,如Visual Basic中不能使用cdecl 。但是它支持variadic函数,如printf()。
  • stdcall 这可能是最常见的,当然,前提是涉及非C / C + +语言。与cdecl不同的是,参数从左至右推入而且函数本身必须清理堆栈后才能返回。
  • winapi 如果未特别指定的话,这是Windows默认的。它在Windows CE上默认为stdcall和cdecl Windows CE。不过它和stdcall不一样,所以如果您尝试调用标为winapi的DLL函数,虽然你期待中是stdcall,但Windows是找不到它的。
  • fastcall它使用登记簿加速,但是在不同的编译器中它算是别具一格的。所以如果你坚信来自某个源的编译器,而不是被要求出自某种语言,如Visual Basic或C # ,那就可以使用fastcall。
  • safecall Borland公司用它来实现COM/OLE调用。
  • thiscall 用于调用出自未管理代码分类出的函数。

  除非你的dlls只被C或C++使用,否则的话,你就相信winapi。则会使默认情况,所以不需要指定。

  C++中的名字改编

  因为C + +语言是一种安全且相当严谨的语言类型,它提出了名字改编。这意味着,输出的函数,包括来自类型的参数类型的额外函数。这有助于防止函数被调用时出现参数错误,而这种错误可能会导致莫名的死机。然而,当非C + +代码调用C + +的函数时也可能会出现问题。例如,你有一个C #应用程序调用dll的C + =代码。由于dll中的函数名称毁坏,函数就无法被找到。操作系统可能会期望一个叫做GetValue(int a, float b)的函数,但在该dll中,名称错位后看起来就像GetValueif。

  有一种简单的修复方法。任何C++ dll必须把代码在任何输出函数之前和之后的说明中。就像这样:

  #define MYEXPORT __declspec(dllexport)

  #ifdef __cplusplus

  extern "C" {

  #endif

  MYEXPORT char * WINAPI GetBotName(void) ; // returns name of your Bot

  ...

  #ifdef __cplusplus

  }

  #endif
 
  这一行:#define MYEXPORT __declspec(dllexport),就告诉了编译器在任何由MYEXPORT定于的函数上添加_declspec。这使得该函数可见与使用dll的任何应用程序。

  macro_cplusplus只定义在兼容C++编译器的Ansi 98中。所以macro会被所有包含在C文件夹的页眉文件忽视。

  在你拥有C代码的反例中,你希望与C++链接,那样你就把说明用这个语句包装起来:

  extern “C++” {

  这一操作可迫使编译器粉碎函数名称。


标签:

本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@evget.com

文章转载自:IT专家网

为你推荐

  • 推荐视频
  • 推荐活动
  • 推荐产品
  • 推荐文章
  • 慧都慧问
扫码咨询


添加微信 立即咨询

电话咨询

客服热线
023-68661681

TOP