`
saybody
  • 浏览: 871002 次
  • 性别: Icon_minigender_2
  • 来自: 西安
文章分类
社区版块
存档分类
最新评论

函数调用约定和堆栈

阅读更多

函数调用约定和堆栈

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

4补充说明

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

假设函数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的调用约定。

分享到:
评论

相关推荐

    剖析C++函数调用约定

    在这篇文章里,我就和大家共同探讨一些关于函数调用约定的内容。 Visual C/C++的编译器支持如下的函数调用约定: 关键字 清理堆栈 参数入栈顺序 函数名称修饰(C) __cdecl 调用函数 右à 左 _函数名 __stdcall 被...

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

    这些现象通常是出现在C和C++的代码混合使用的情况下或在C++程序中使用第三方的库的情况下(不是用C++语言开发的),其实这都是函数调用约定(Calling Convention)和函数名修饰(Decorated Name)规则惹的祸。...

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

    调用约定(Calling Convention)是指在程序设计语言中为了实现函数调用而建立的一种协议。这种协议规定了该语言的函数中的参数传送方式、参数是否可变和由谁来处理堆栈等问题。不同的语言定义了不同的调用约定。 在...

    DLL中调用约定和名称修饰

    调用约定(Calling Convention)是指在程序设计语言中为了实现函数调用而建立的一 种协议。这种协议规定了该语言的函数中的参数传送方式、参数是否可变和由谁来处理堆栈等问题。不同的语言定义了不同的调用约定!

    函数的调用规则(__cdecl,__stdcall,__fastcall,__pascal)

    关于函数的调用规则(调用约定),大多数时候是不需要了解的,但是如果需要跨语言的编程,比如VC写的dll要delphi调用,则需要了解。 microsoft的vc默认的是__cdecl方式,而windows API则是__stdcall,如果用vc开发...

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

    学习汇编的一个重要的方法就是将汇编代码和c代码之间进行转换练习,这样的练习做的越多,对汇编就越熟悉,同时对c代码的理解也更加深刻。很多时候,拿到一个程序的反汇编代码,虽然可能每一行汇编代码都知道什么意思...

    链接库知识锦集

    函数调用约定是指当调用一个函数时,参数会被传递给被调用函数和返回值会被传递给调用参数,函数调用约定就是描述参数是怎么被传递的和有谁平衡堆栈的,当然还有返回值。 函数调用约定有:__stdcall,__cdecl,__...

    C语言函数调用栈(二)

    1)函数参数的传递顺序和方式最常见的参数传递方式是通过堆栈传递。主调函数将参数压入栈中,被调函数以相对于帧基指针的正偏移量来访问栈中的参数。对于有多个参数的函数,调用约定需规定主调函数将参数压栈的顺序...

    外部函数接口LibFFI.zip

    调用约定(Calling Conventions)定义了程序中调用函数的方式,它决定了在函数调用的时候数据(比如说参数)在堆栈中的组织方式。 通常来说函数调用要用到的两条基本的指令:”CALL”指令和”RET”指令。”CALL”...

    浅谈C/C++中可变参数的原理

    要理解可变参数,首先要理解函数调用约定, 为什么只有__cdecl的调用约定支持可变参数,而__stdcall不支持?  实际上__cdecl和__stdcall函数参数都是从右到左入栈,它们的区别在于由谁来清栈,__cdecl由外部调用...

    MingW VC 之.a .lib .dll .def 关系

    VC上编译C和C++程序时,默认使用__cdecl函数调用约定。如果想生成__stdcall的函数,我们可以使用/GZ编译.例如: cl /Gz /LD mylib.cpp 这样生成的dll和lib就是使用的__stdcall约定 通过下面的命令 dumpbin /...

    C++中异常机制的实现机制详解

    我们先看一下函数调用和返回的流程。 下面是按调用约定__stdcall 调用函数test(int p1,int p2)的汇编代码 假设执行函数前堆栈指针ESP为NN push p2 ;参数2入栈, ESP -= 4h , ESP = NN – 4h push p1 ;参数1入

    windows用户称拦截api

    前面四条指令分别为参数压栈,因为MessageBoxA是__stdcall调用约定,所以参数是从右往左压栈的。最后再CALL 0x0042428c 看看0042428c这段内存的值: 0042428C 0B 05 D5 77 00 00 00 可以看到这个值0x77d5050b,正是...

    vld(Visual Leak Detector 内存泄露检测工具 源码)

     得到当前的堆栈的二进制表示并不是一件很复杂的事情,但是因为不同体系结构、不同编译器、不同的函数调用约定所产生的堆栈内容略有不同,要解释堆栈并得到整个函数调用过程略显复杂。不过windows提供一个StackWalk...

    lua 程序设计学习.doc 版

    25.3 通用的函数调用 第26章 调用C函数 26.1 C 函数 26.2 C 函数库 第27章 撰写C函数的技巧 27.1 数组操作 27.2 字符串处理 27.3 在C函数中保存状态 27.3.1 The Registry 27.3.2 References 27.3.3 Upvalues 第28章 ...

    计算机程序设计的要点与规范分析-.docx

    在计算机程序设计时,把需要调用的函数存放于内存中,然后把用于函数调用指令的地址保存在堆栈,通过这种方式,在之后的程序设计时可以通过堆栈中的函数调用指令,从而完成函数的调用。 2.3 掌握C语言的自增和自减...

    80x86汇编语言程序设计教程

    书分为三部分。第一部分是基础部分,以8086/8088为背景,...8.5 编写供Turbo C调用的函数 8.5.1 汇编格式的编译结果 8.5.2 汇编模块应该遵守的约定 8.5.3 参数传递和寄存器保护 8.5.4 举例 8.6 习题 第二部分 提高部分

    VC++与汇编语言的混合编程

    掌握嵌入汇编函数和汇编语言子程序与VC++的混合编程方法,入口、出口参数的传递方法以及在VC++环境下混合编程的调试方法。 二、 实验条件 硬件:计算机一台 软件:Visual Studio C++ 6.0、MASM 6.0 三、 实验原理...

    AtX:elf将Arm32转换为X86_64

    将ARM转换为X86 我试图回答将ARM32 elf二进制文件转换为X86_64 elf二进制文件有多困难。...基于ABI的函数调用约定,我决定了寄存器的映射: ARM32 X86_64 00 电子数据交换 R1 ESI R2 EDX R3 ECX R4 EAX

Global site tag (gtag.js) - Google Analytics