Sobre las convenciones de convocatoria

Reenviado desde: http://www.cnblogs.com/xw885/articles/153691.html 

En lenguaje C, supongamos que tenemos una función como esta:

función int(int a,int b)

Esta función se puede usar siempre que sea llamada por result = function(1,2). Sin embargo, cuando el lenguaje de alto nivel se compila en un código de máquina que la computadora puede reconocer, surge un problema: en la CPU, la computadora no tiene forma de saber cuántos y qué parámetros requiere una llamada de función, y no hay hardware para almacenar estos parámetros. Es decir, la computadora no sabe cómo pasar parámetros a esta función, y el trabajo de pasar parámetros debe ser coordinado por la persona que llama a la función y la función misma. Con este fin, la computadora proporciona una estructura de datos llamada pila para admitir el paso de parámetros.

La pila es una estructura de datos de tipo "primero en entrar, último en salir. La pila tiene un área de almacenamiento y un puntero en la parte superior de la pila. El puntero de la parte superior de la pila apunta al primer elemento de datos disponible en la pila (llamado la parte superior de la pila). El usuario puede agregar datos a la pila en la parte superior de la pila. Esta operación se llama empujar la pila (Push). Después de empujar la pila, la parte superior de la pila se convertirá automáticamente en la posición del elemento de datos recién agregado, y el puntero en la parte superior de la pila también se modificará en consecuencia. El usuario también puede eliminar la parte superior de la pila de la pila, lo que se denomina hacer estallar la pila (pop).Después de que se haya extraído la pila, un elemento debajo de la parte superior de la pila se convierte en la parte superior de la pila, y el puntero a la parte superior de la pila se modifica en consecuencia.

Cuando se llama a la función, la persona que llama empuja los parámetros en la pila a su vez y luego llama a la función.Después de llamar a la función, los datos se obtienen de la pila y se calculan. Una vez que se completa el cálculo de la función, la persona que llama o la función misma modifica la pila para restaurar la pila original.

En el paso de parámetros, hay dos cuestiones muy importantes que deben establecerse claramente:

  • Cuando el número de parámetros es más de uno, ¿en qué orden se colocan los parámetros en la pila?
  • Después de llamar a la función, ¿quién restaurará la pila original?

En lenguajes de alto nivel, ambos problemas se ilustran mediante la convención de llamada de función. Las convenciones de llamada comunes son:

  • llamada estándar
  • cdecl
  • llamada rapida
  • esta llamada
  • llamada desnuda

convención de llamadas stdcall

stdcall a menudo se llama la convención de llamada de pascal, porque pascal es un lenguaje de programación de computadoras de enseñanza muy común en los primeros días, su sintaxis es rigurosa y la convención de llamada de función utilizada es stdcall. En los compiladores C/C++ de la serie Microsoft C++, las macros PASCAL se usan a menudo para declarar esta convención de llamadas, y macros similares incluyen WINAPI y CALLBACK.

La sintaxis de la declaración de la convención de llamada stdcall es (tomando la función anterior como ejemplo):

función int __ stdcall  (int a, int b)

La convención de llamada de stdcall significa: 1) los parámetros se colocan en la pila de derecha a izquierda, 2) la función misma modifica la pila 3) el nombre de la función se antepone automáticamente con un guión bajo, seguido de un símbolo @, seguido del tamaño del parámetro

Tomando la función anterior como ejemplo, el parámetro b se coloca primero en la pila, y luego el parámetro a, la traducción de la función de llamada de función (1,2) al lenguaje ensamblador se convertirá en:

 
 
  
  
push 2 El segundo parámetro se inserta en la pila push 1 El primer parámetro se inserta en la función de llamada de la pila parámetro de llamada, tenga en cuenta que cs:eip se inserta automáticamente en la pila en este momento

Para la función en sí, se puede traducir como:

 
 
  
  
push ebp guarda el registro ebp, que se usará para guardar el puntero superior de la pila, y se puede restaurar cuando la función sale mov ebp, esp guarda el puntero de la pila mov eax, [ebp + 8H] ebp se guarda a su vez antes de la posición señalada por ebp en la pila, cs:eip, a, b, ebp +8 apunta a un add eax, [ebp + 0CH] b se guarda en ebp + 12 en la pila mov esp, ebp restaura esp pop ebp ret 8

En tiempo de compilación, el nombre de esta función se traduce a _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函数将告诉你,堆栈被破坏了。

Supongo que te gusta

Origin blog.csdn.net/besidemyself/article/details/7275214
Recomendado
Clasificación