Your Ad Here
首页 | 编程语言 | 网站建设 | 游戏天堂 | 冲浪宝典 | 网络安全 | 操作系统 | 软件时空 | 硬件指南 | 病毒相关 | IT 认证
软讯网络 > 编程语言 > Visual C++ > 函数调用约定和堆栈
【标  题】:函数调用约定和堆栈
【关键字】:
【来  源】:BLOG.CSDN.NET

函数调用约定和堆栈

Your Ad Here

函数调用约定和堆栈

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有一个共同点:实参和形参数目不符不会导致堆栈错误。

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

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错误,程序会继续运行,直到“遇到问题需要关闭”。

CString的一个问题:【上一篇】
谈谈编程(3) 编程实践:【下一篇】
【相关文章】
没有相关文章
【随机文章】
  • 使用prototype.js 的时候应该特别注意的几个问题.
  • 在Word中打造自己的“自动图文集”
  • 老板的留言
  • 获得键盘扫描码
  • 如何得知当前用的是哪个版本的shell
  • 瞒天过海html文件技术一瞥
  • ARP简介
  • asp.net控件开发基础(14)
  • VC++中使用CAPI连接MySql
  • Solaris系统性能维护
  • 【相关评论】
    没有相关评论
    【发表评论】
    姓名:
    邮件:
    随机码*
    评论*
          
    |  首 页  |  版权声明  |  联系我们   |  网站地图  |
    CopyRight © 2004-2007 软讯网络 All Rigths Reserved.