函数调用约定和堆栈

函数调用约定和堆栈

1 什么是堆栈

编译器通常使用堆栈实现函数调用。堆栈是存储器的一个区域,嵌入式环境有时须要程序员本身定义一个数组做为堆栈。Windows为每一个线程自动维护一个堆栈,堆栈的大小能够设置。编译器使用堆栈来堆放每一个函数的参数、局部变量等信息。程序员

函数调用常常是嵌套的,在同一时刻,堆栈中会有多个函数的信息,每一个函数占用一个连续的区域。一个函数占用的区域被称做帧(frame)。数组

编译器从高地址开始使用堆栈。 假设咱们定义一个数组a[1024]做为堆栈空间,一开始栈顶指针指向a[1023]。若是栈里有两个函数a和b,且a调用了b,栈顶指针会指向函数b的 帧。若是函数b返回。栈顶指针就指向函数a的帧。若是在栈里放了太多东西形成溢出,破坏的是a[0]上面的东西。多线程

在多线程(任务)环境,CPU的堆栈指针指向的存储器区域就是当前使用的堆栈。切换线程的一个重要工做,就是将堆栈指针设为当前线程的堆栈栈顶地址。函数

不一样CPU,不一样编译器的堆栈布局、函数调用方法均可能不一样,但堆栈的基本概念是同样的。布局

2 函数调用约定

函数调用约定包括传递参数的顺序,谁负责清理参数占用的堆栈等,例如 :线程

  参数传递顺序 谁负责清理参数占用的堆栈
__pascal 从左到右 调用者
__stdcall 从右到左 被调函数
__cdecl 从右到左 调用者

调用函数的代码和被调函数必须采用相同的函数的调用约定,程序才能正常运行。在Windows上,__cdecl是C/C++程序的缺省函数调用约定。指针

在有的cpu上,编译器会用寄存器传递参数,函数使用的堆栈由被调函数分配和释放。这种调用约定在行为上和__cdecl有一个共同点:实参和形参数目不符不会致使堆栈错误。调试

不过,即便用寄存器传递参数,编译器在进入函数时,仍是会将寄存器里的参数存入堆栈指定位置。参数和局部变量同样应该在堆栈中有一席之地。参数能够被理解为由调用函数指定初值的局部变量。blog

3 例子:__cdecl和__stdcall

不一样的CPU,不一样的编译器,堆栈的布局多是不一样的。本文以x86,VC++的编译器为例。编译器

VC++编译器的已经再也不支持__pascal, __fortran, __syscall等函数调用约定。目前只支持__cdecl和__stdcall。

采用__cdecl或__stdcall调用方式的程序,在刚进入子函数时,堆栈内容是同样的。esp指向的栈顶是返回地址。这是被call指令压入堆栈的。下面是参数,左边参数在上,右边参数在下(先入栈)。

如前表所示,__cdecl和__stdcall的区别是:__cdecl是调用者清理参数占用的堆栈,__stdcall是被调函数清理参数占用的堆栈。

因为__stdcall的被调函数在编译时就必须知道传入参数的准确数目(被调函数要清理堆栈),因此不能支持变参数函数,例如printf。并且若是调用者使用了不正确的参数数目,会致使堆栈错误。

经过查看汇编代码,__cdecl函数调用在call语句后会有一个堆栈调整语句,例如:

      a = 0x1234;

 

      b = 0x5678;

 

    c = add(a, b);

对应x86汇编:

      mov dword ptr [ebp-4],1234h

 

      mov dword ptr [ebp-8],5678h

 

      mov eax,dword ptr [ebp-8]

 

      push eax

 

      mov ecx,dword ptr [ebp-4]

 

      push ecx

 

      call 0040100a


add esp,8

    mov dword ptr [ebp-0Ch],eax


__stdcall的函数调用则不须要调整堆栈:

      call 00401005

 

    mov dword ptr [ebp-0Ch],eax

函数

      int __cdecl add(int a, int b)

 

      {

 

      return a+b;

 

    }

产生如下汇编代码(Debug版本):

      push ebp

 

      mov ebp,esp

 

      sub esp,40h

 

      push ebx

 

      push esi

 

      push edi

 

      lea edi,[ebp-40h]

 

      mov ecx,10h

 

      mov eax,0CCCCCCCCh

 

      rep stos dword ptr [edi]

 

      mov eax,dword ptr [ebp+8]

 

      add eax,dword ptr [ebp+0Ch]

 

      pop edi

 

      pop esi

 

      pop ebx

 

      mov esp,ebp

 

      pop ebp


ret // 跳转到esp所指地址,并将esp+4,使esp指向进入函数时的第一个参数

再查看__stdcall函数的实现,会发现与__cdecl函数只有最后一行不一样:

    ret 8 // 执行ret并清理参数占用的堆栈

对于调试版本,VC++编译器在“直接调用地址”时会增长检查esp的代码,例如:

      ta = (TAdd)add; // TAdd定义:typedef int (__cdecl *TAdd)(int a, int b);

 

    c = ta(a, b);

产生如下汇编代码:

      mov [ebp-10h],0040100a

 

      mov esi,esp

 

      mov ecx,dword ptr [ebp-8]

 

      push ecx

 

      mov edx,dword ptr [ebp-4]

 

      push edx


call dword ptr [ebp-10h]
add esp,8

      cmp esi,esp


call __chkesp (004011e0)

    mov dword ptr [ebp-0Ch],eax

__chkesp 代码以下。若是esp不等于函数调用前保存的值,就会转到错误处理代码。

      004011E0 jne __chkesp+3 (004011e3)

 

      004011E2 ret

 

    004011E3 ;错误处理代码

__chkesp的错误处理会弹出对话框,报告函数调用形成esp值不正确。 Release版本的汇编代码要简洁得多。也不会增长 __chkesp。若是发生esp错误,程序会继续运行,直到“遇到问题须要关闭”。

3 补充说明

函数调用约定只是“调用函数的代码”和被调用函数之间的关系。

假设函数A是__stdcall,函数B调用函数A。你必须经过函数声明告诉编译器,函数A是__stdcall。编译器天然会产生正确的调用代码。

若是函数A是__stdcall。但在引用函数A的地方,你却告诉编译器,函数A是__cdecl方式,编译器产生__cdecl方式的代码,与函数A的调用约定不一致,就会发生错误。

以delphi调用VC函数为例,delphi的函数缺省采用__pascal约定,VC的函数缺省采用__cdecl约定。咱们通常将VC的函数设为__stdcall,例如:

    int __stdcall add(int a, int b);

在delphi中将这个函数也声明为__stdcall,就能够调用了:

      function add(a: Integer; b: Integer): Integer;

 

    stdcall; external 'a.dll';

由于考虑到可能被其它语言的程序调用,很多API采用__stdcall的调用约定。

相关文章
相关标签/搜索