呼び出し規約について

転載元: http://www.cnblogs.com/xw885/articles/153691.html 

C 言語で次のような関数があるとします。

int 関数(int a,int b)

この関数は、result = function(1,2) によって呼び出される限り使用できます。しかし、高級言語がコンピュータが認識できるマシンコードにコンパイルされると、問題が生じます。CPU では、コンピュータは関数呼び出しに必要なパラメータの数とパラメータを知る方法がなく、これらのパラメータを保存するハードウェアもありません。つまり、コンピュータはこの関数にパラメータを渡す方法を知らないため、パラメータを渡す作業は関数の呼び出し元と関数自体によって調整される必要があります。この目的のために、コンピュータはパラメータの受け渡しをサポートするスタックと呼ばれるデータ構造を提供します。

スタックは先入れ後出しのデータ構造であり、記憶領域とスタックトップポインタを有する。スタックの最上位ポインタは、スタック上の最初の使用可能なデータ項目 (スタックの最上位と呼ばれます) を指します。ユーザーは、スタックの最上部にあるスタックにデータを追加できます。この操作はスタックのプッシュ (Push) と呼ばれます。スタックがプッシュされた後、スタックの最上部は自動的に新しく追加されたデータ項目の位置になり、スタックの最上部のポインタもそれに応じて変更されます。ユーザーは、スタックからスタックの最上位を削除することもできます。これは、スタックのポップ (ポップ) と呼ばれます。スタックがポップされた後、スタックの最上位の下にある要素がスタックの最上位になり、スタックの最上位へのポインタがそれに応じて変更されます。

関数が呼び出されるとき、呼び出し元はパラメータをスタックに順番にプッシュしてから関数を呼び出し、関数呼び出し後にスタックからデータを取得して計算します。関数の計算が完了すると、呼び出し元または関数自体がスタックを変更して元のスタックを復元します。

パラメーターの受け渡しには、明確に述べておく必要がある非常に重要な問題が 2 つあります。

  • パラメータの数が複数の場合、パラメータはどのような順序でスタックにプッシュされますか
  • 関数が呼び出された後、誰が元のスタックを復元するか

高級言語では、これらの問題は両方とも関数呼び出し規約によって説明されます。一般的な呼び出し規則は次のとおりです。

  • 標準呼び出し
  • cdecl
  • ファストコール
  • この電話
  • 裸の通話

stdcall 呼び出し規約

Pascal は初期の非常に一般的な教育用コンピューター プログラミング言語であり、その構文は厳密で、使用される関数呼び出し規則は stdcall であるため、stdcall はよく Pascal 呼び出し規則と呼ばれます。Microsoft C++ シリーズの C/C++ コンパイラでは、この呼び出し規約を宣言するために PASCAL マクロがよく使用され、同様のマクロには WINAPI や CALLBACK などがあります。

stdcall 呼び出し規約宣言の構文は次のとおりです (前の関数を例にします)。

int __ stdcall 関数(int a,int b)

stdcall の呼び出し規約は次のことを意味します: 1) パラメーターは右から左にスタックにプッシュされます。 2) 関数自体がスタックを変更します。 3) 関数名の先頭には自動的にアンダースコアが付けられ、その後に @ 記号が続き、その後にパラメーターのサイズが続きます。

上記の関数を例にとると、最初にパラメータ b がスタックにプッシュされ、次にパラメータ a、関数呼び出し function(1,2) のアセンブリ言語への変換は次のようになります。

 
 
  
  
Push 2 2 番目のパラメータがスタックにプッシュされます。 Push 1 最初のパラメータが スタックにプッシュされます。 call 関数呼び出しパラメータ。この時点で cs:eip が自動的にスタックにプッシュされることに注意してください。

関数自体は次のように翻訳できます。

 
 
  
  
Push ebp は、スタックのスタック トップ ポインタを保存するために使用される ebp レジスタを保存します。関数が終了すると復元できます。 mov ebp、esp はスタック ポインタを保存します。 mov eax、[ebp + 8H] ebp は、スタック の ebp が指す位置の前に順番に保存されます。 cs:eip, a, b, ebp +8 は、add eax を指します。[ebp + 0CH] b は、ebp + に保存されます。スタック内の 12 mov esp、ebp 復元 esp ポップ 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函数将告诉你,堆栈被破坏了。

おすすめ

転載: blog.csdn.net/besidemyself/article/details/7275214