`
isiqi
  • 浏览: 16367196 次
  • 性别: Icon_minigender_1
  • 来自: 济南
社区版块
存档分类
最新评论

函数调用约定 : _stdcall _cdecl fastcall 调用方式详解

阅读更多

在C语言中,假设我们有这样的一个函数:

int function(int a,int b)

调用时只要用result = function(1,2)这样的方式就可以使用这个函数。但是,当高级语言被编译成计算机可以识别的机器码时,有一个问题就凸现出来:在CPU中,计算机没有办法知道一个函数调用需要多少个、什么样的参数,也没有硬件可以保存这些参数。也就是说,计算机不知道怎么给这个函数传递参数,传递参数的工作必须由函数调用者和函数本身来协调。为此,计算机提供了一种被称为栈的数据结构来支持参数传递。

栈是一种先进后出的数据结构,栈有一个存储区、一个栈顶指针。栈顶指针指向堆栈中第一个可用的数据项(被称为栈顶)。用户可以在栈顶上方向栈中加入数据,这个操作被称为压栈(Push),压栈以后,栈顶自动变成新加入数据项的位置,栈顶指针也随之修改。用户也可以从堆栈中取走栈顶,称为弹出栈(pop),弹出栈后,栈顶下的一个元素变成栈顶,栈顶指针随之修改。

函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在堆栈中取得数据,并进行计算。函数计算结束以后,或者调用者、或者函数本身修改堆栈,使堆栈恢复原装。

在参数传递中,有两个很重要的问题必须得到明确说明:

  • 当参数个数多于一个时,按照什么顺序把参数压入堆栈
  • 函数调用后,由谁来把堆栈恢复原装

在高级语言中,通过函数调用约定来说明这两个问题。常见的调用约定有:

  • stdcall
  • cdecl
  • fastcall
  • thiscall
  • naked call

stdcall调用约定

stdcall很多时候被称为pascal调用约定,因为pascal是早期很常见的一种教学用计算机程序设计语言,其语法严谨,使用的函数调用约定就是stdcall。在Microsoft C++系列的C/C++编译器中,常常用PASCAL宏来声明这个调用约定,类似的宏还有WINAPI和CALLBACK。

stdcall调用约定声明的语法为(以前文的那个函数为例):

int __stdcall function(int a,int b)

stdcall的调用约定意味着:1)参数从右向左压入堆栈,2)函数自身修改堆栈 3)函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸

以上述这个函数为例,参数b首先被压栈,然后是参数a,函数调用function(1,2)调用处翻译成汇编语言将变成:

push 2 第二个参数入栈 push 1 第一个参数入栈 call function 调用参数,注意此时自动把cs:eip入栈

而对于函数自身,则可以翻译为:

push ebp 保存ebp寄存器,该寄存器将用来保存堆栈的栈顶指针,可以在函数退出时恢复 mov ebp,esp 保存堆栈指针 mov eax,[ebp + 8H] 堆栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向a add eax,[ebp + 0CH] 堆栈中ebp + 12处保存了b mov esp,ebp 恢复esp pop ebp ret 8

而在编译时,这个函数的名字被翻译成_function@8

注意不同编译器会插入自己的汇编代码以提供编译的通用性,但是大体代码如此。其中在函数开始处保留esp到ebp中,在函数结束恢复是编译器常用的方法。

从函数调用看,2和1依次被push进堆栈,而在函数中又通过相对于ebp(即刚进函数时的堆栈指针)的偏移量存取参数。函数结束后,ret 8表示清理8个字节的堆栈,函数自己恢复了堆栈。

cdecl调用约定

cdecl调用约定又称为C调用约定,是C语言缺省的调用约定,它的定义语法是:

int function (int a ,int b) //不加修饰就是C调用约定 int __cdecl function(int a,int b)//明确指出C调用约定

在写本文时,出乎我的意料,发现cdecl调用约定的参数压栈顺序是和stdcall是一样的,参数首先由有向左压入堆栈。所不同的是,函数本身不清理堆栈,调用者负责清理堆栈。由于这种变化,C调用约定允许函数的参数的个数是不固定的,这也是C语言的一大特色。对于前面的function函数,使用cdecl后的汇编码变成:

调用处 push 1 push 2 call function add esp,8 注意:这里调用者在恢复堆栈 被调用函数_function处 push ebp 保存ebp寄存器,该寄存器将用来保存堆栈的栈顶指针,可以在函数退出时恢复 mov ebp,esp 保存堆栈指针 mov eax,[ebp + 8H] 堆栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向a add eax,[ebp + 0CH] 堆栈中ebp + 12处保存了b mov esp,ebp 恢复esp pop ebp ret 注意,这里没有修改堆栈

MSDN中说,该修饰自动在函数名前加前导的下划线,因此函数名在符号表中被记录为_function,但是我在编译时似乎没有看到这种变化。

由于参数按照从右向左顺序压栈,因此最开始的参数在最接近栈顶的位置,因此当采用不定个数参数时,第一个参数在栈中的位置肯定能知道,只要不定的参数个数能够根据第一个后者后续的明确的参数确定下来,就可以使用不定参数,例如对于CRT中的sprintf函数,定义为:

int sprintf(char* buffer,const char* format,...)

由于所有的不定参数都可以通过format确定,因此使用不定个数的参数是没有问题的。

fastcall

fastcall调用约定和stdcall类似,它意味着:

  • 函数的第一个和第二个DWORD参数(或者尺寸更小的)通过ecx和edx传递,其他参数通过从右向左的顺序压栈
  • 被调用函数清理堆栈
  • 函数名修改规则同stdcall

其声明语法为:int fastcall function(int a,int b)

thiscall

thiscall是唯一一个不能明确指明的函数修饰,因为thiscall不是关键字。它是C++类成员函数缺省的调用约定。由于成员函数调用还有一个this指针,因此必须特殊处理,thiscall意味着:

  • 参数从右向左入栈
  • 如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入堆栈。
  • 对参数个数不定的,调用者清理堆栈,否则函数自己清理堆栈

为了说明这个调用约定,定义如下类和使用代码:

class A
{
public:
      int function1(int a,int b);
      int function2(int a,...);
};
int A::function1 (int a,int b)
{
      return a+b;
}
#include 
int A::function2(int a,...)
{
      va_list ap;
      va_start(ap,a);
      int i;
      int result = 0;
      for(i = 0 ; i < a ; i ++)
      {
         result += va_arg(ap,int);
      }
      return result;
}
void callee()
{
      A a;
      a.function1 (1,2);
      a.function2(3,1,2,3);
}

callee函数被翻译成汇编后就变成:

//函数function1调用 0401C1D push 2 00401C1F push 1 00401C21 lea ecx,[ebp-8] 00401C24 call function1 注意,这里this没有被入栈 //函数function2调用 00401C29 push 3 00401C2B push 2 00401C2D push 1 00401C2F push 3 00401C31 lea eax,[ebp-8] 这里引入this指针 00401C34 push eax 00401C35 call function2 00401C3A add esp,14h

可见,对于参数个数固定情况下,它类似于stdcall,不定时则类似cdecl

naked call

这是一个很少见的调用约定,一般程序设计者建议不要使用。编译器不会给这种函数增加初始化和清理代码,更特殊的是,你不能用return返回返回值,只能用插入汇编返回结果。这一般用于实模式驱动程序设计,假设定义一个求和的加法程序,可以定义为:

__declspec(naked) int  add(int a,int b)
{
   __asm mov eax,a
   __asm add eax,b
   __asm ret 
}

注意,这个函数没有显式的return返回值,返回通过修改eax寄存器实现,而且连退出函数的ret指令都必须显式插入。上面代码被翻译成汇编以后变成:

mov eax,[ebp+8] add eax,[ebp+12] ret 8

注意这个修饰是和__stdcall及cdecl结合使用的,前面是它和cdecl结合使用的代码,对于和stdcall结合的代码,则变成:

__declspec(naked) int __stdcall function(int a,int b)
{
    __asm mov eax,a
    __asm add eax,b
    __asm ret 8        //注意后面的8
}

至于这种函数被调用,则和普通的cdecl及stdcall调用函数一致。

函数调用约定导致的常见问题

如果定义的约定和使用的约定不一致,则将导致堆栈被破坏,导致严重问题,下面是两种常见的问题:

  1. 函数原型声明和函数体定义不一致
  2. DLL导入函数时声明了不同的函数约定

以后者为例,假设我们在dll种声明了一种函数为:

__declspec(dllexport) int func(int a,int b);//注意,这里没有stdcall,使用的是cdecl

使用时代码为:

typedef int (*WINAPI DLLFUNC)func(int a,int b);
      hLib = LoadLibrary(...);
      DLLFUNC func = (DLLFUNC)GetProcAddress(...)//这里修改了调用约定
      result = func(1,2);//导致错误

由于调用者没有理解WINAPI的含义错误的增加了这个修饰,上述代码必然导致堆栈被破坏,MFC在编译时插入的checkesp函数将告诉你,堆栈被破坏了。

该文来自网络: http://hi.baidu.com/lihao102/blog/item/29ca491ed9011ffd1bd5764f.html转载请说明。

分享到:
评论

相关推荐

    MFC中stdcall调用约定

    在C和C++编程中,调用约定(call convention)定义了如何在函数调用期间传递参数、清理栈空间等规则。不同的调用约定适用于不同的场景,其中`_stdcall`是较为常见的一种。本文将深入探讨`_stdcall`调用约定,并通过...

    C++调用的多种形式

    C++调用约定是指在C++中函数调用的多种形式,它们是:_stdcall、_cdecl、_fastcall、thiscall和naked call。每种调用约定都有其特点和应用场景。 _stdcall调用约定 _stdcall调用约定是Pascal程序的缺省调用方式,...

    编译原理课程设计之函数调用分析

    本文将详细介绍Windows系统下的三种主要函数调用约定:`__stdcall`、`__cdecl`以及`PASCAL`(在某些情况下也被称为`__fastcall`)。通过对比这些调用约定的特点及其对程序的影响,帮助读者更好地理解编译原理中的...

    stdcall与 cdecl

    ### stdcall 与 cdecl:函数调用方式详解 在编程领域,特别是在C和C++语言中,函数调用约定(Calling Convention)是一个重要的概念。它规定了如何传递参数、谁来清理栈空间以及如何处理返回值等细节。本文将详细...

    函数调用约定解析及dll中调用约定[收集].pdf

    ### 函数调用约定解析及DLL中调用约定详解 #### 一、引言 本文旨在深入探讨函数调用约定的基础知识以及其在不同编程环境中的应用,特别是针对DLL(动态链接库)中的函数调用约定。理解这些概念对于编写高效、可...

    函数调用约定与函数名称修饰规则.pdf

    ### 函数调用约定与函数名称修饰规则 #### 调用约定(Calling Convention) 调用约定是指在程序设计语言中为了实现函数调用而建立的一种协议。这种协议规定了该语言的函数中的参数传送方式、参数是否可变以及由谁...

    Windows调用约定.pdf

    本文将深入探讨Windows环境下常见的四种调用约定:__cdecl、__stdcall、__thiscall和__fastcall。 首先,__cdecl是C/C++函数的默认调用约定。在这种约定下,参数是从右向左依次压入堆栈,然后由调用函数负责清理...

    DLL中调用约定和名称修饰

    调用约定是在程序设计语言中为了实现函数调用而建立的一种协议。它规定了该语言的函数中的参数传递方式、参数是否可变以及由谁来处理堆栈等问题。不同的编程语言定义了不同的调用约定。 在C++中,为了支持操作符...

    学习window编程的API中winapi

    _stdcall调用约定详解 - **参数传递**:在_stdcall调用约定中,函数参数是从右到左依次压入堆栈的。 - **栈帧管理**:调用者负责清理堆栈。这意味着每当一个函数被调用后,调用者需要清理掉所有传递给该函数的参数...

    C++关键字技术

    ### C++关键字技术详解:深入理解VC++中的调用约定 在C++编程语言中,调用约定(Calling Convention)是编译器处理函数参数、返回值以及如何清理调用堆栈的重要规则。不同的调用约定会影响代码的性能、可移植性和...

    c++调用约定 c++的集中约定分析

    ### C++调用约定详解:理解C++中的不同调用方式 #### 一、引言 在C++编程中,函数调用约定是一种规范化的机制,用于规定如何传递参数、如何清理栈空间以及如何确定函数名称等。这些约定对程序员来说至关重要,尤其...

    2013年微软暑期实习生校园招聘笔试试题加详解

    1. **_cdecl**:这是C和C++默认的函数调用约定。参数从右向左压栈,由调用者负责清理堆栈。因此,_cdecl允许可变参数列表,如`printf`函数。但这种调用约定会导致生成的代码较大,因为每个调用者都需要处理堆栈清理...

    详解VS2019 dumpbin查看DLL的导出函数

    调用约定(Calling Convention)是指函数调用时参数传递和返回值的方式,常见的调用约定有_stdcall、_cdecl、_fastcall等。函数符号是指函数的名称和参数列表,例如CreateNativeManager@0表示一个名为...

    详解C++ 动态库导出函数名乱码及解决

    在 C++ 中,有三种调用约定:__stdcall、__cdecl 和 __fastcall。每种调用约定都有其自己的修饰规则。 二、C++ 动态库导出函数名乱码的解决 要解决 C++ 动态库导出函数名乱码的问题,我们需要了解函数名修饰规则。...

    从汇编到c 调用约定 堆栈原理

    调用约定(Calling Convention)规定了函数调用时参数传递的方式、局部变量的分配方式以及函数返回后的清理工作由谁来完成等规则。常见的调用约定有`stdcall`、`cdecl`、`fastcall`等。本文将以C语言中的`cdecl`为例...

    BCB调用VC的DLL

    - 此处的`__stdcall`指定了函数调用约定,这与DLL中的导出函数保持一致是非常重要的。 4. **处理未解析外部引用问题**: - 在编译过程中可能会遇到未解析外部引用错误,这是因为BCB无法找到相应的函数实现。 - ...

    亲密接触VC6.0编译器

    - Calling convention:设定函数调用约定,如__cdecl、__fastcall和__stdcall,影响参数传递和堆栈清理。 **运行时库选择** 运行时库的选择非常重要,因为它影响程序的多线程支持和大小。静态连接(Single-...

    VC++6.0编译器详解

    Calling convention设置函数调用约定,包括__cdecl(默认)、__fastcall(快速调用)和__stdcall(标准调用),它们决定了参数压栈的方向。 理解这些编译器设置对于提高代码质量和调试效率至关重要。例如,正确选择...

Global site tag (gtag.js) - Google Analytics