C - 函数调用的栈分配

来源:互联网 发布:设备维修保养软件 编辑:程序博客网 时间:2024/05/19 02:43

摘录一

        当调用(call)一个函数时,主调函数将声明中的参数表以逆序压栈,然后将当前的代码执行指针(eip)压栈,跳转到被调函数的入口点。
        进入被调函数时,函数将esp减去相应字节数获取局部变量存储空间。被调函数返回(ret)时,将esp加上相应字节数,归还栈空间,弹出主调函数压在栈中的代码执行指针(eip),跳回主调函数。再由主调函数恢复到调用前的栈。
        为了访问函数局部变量,必须有方法定位每一个变量。变量相对于栈顶esp的位置在进入函数体时就已确定,但是由于esp会在函数执行期变动,所以将esp的值保存在ebp中,并事先将原ebp的值压栈保存,以声明中的顺序(即压栈的相反顺序)来确定偏移量。

访问函数的局部变量和访问函数参数的区别:
局部变量总是通过将ebp减去偏移量来访问,函数参数总是通过将ebp加上偏移量来访问。对于32位变量而言,第一个局部变量位于ebp-4,第二个位于ebp-8,以此类推,32位局部变量在栈中形成一个逆序数组;第一个函数参数位于ebp+8,第二个位于ebp+12,以此类推,32位函数参数在栈中形成一个正序数组。
 

        函数的返回值不同于函数参数,可以通过寄存器传递。如果返回值类型可以放入32位变量,比如int、short、char、指针等类型,将通过eax寄存器传递。如果返回值类型是64位变量,如_int64,则通过edx+eax传递,edx存储高32位,eax存储低32位。如果返回值是浮点类型,如float和double,通过专用的浮点数寄存器栈的栈顶返回。如果返回值类型是struct或class类型,编译器将通过隐式修改函数的签名,以引用型参数的形式传回。由于函数返回值通过寄存器返回,不需要空间分配等操作,所以返回值的代价很低。基于这个原因,C89规范中约定,不写明返回值类型的函数,返回值类型默认为int。这一规则与现行的C++语法相违背,因为C++中,不写明返回值类型的函数返回值类型为void,表示不返回值。这种语法不兼容性是为了加强C++的类型安全,但同时也带来了一些代码兼容性问题。

代码示例

VarType Func (Arg1, Arg2, Arg3, ... ArgN

    VarType Var1, Var2, Var3, ...VarN;
    //... 
    return VarN
}

假设sizeof(VarType) = 4(DWORD), 则一次函数调用汇编代码示例为:

调用方代码: 

push ArgN ; 依次逆序压入调用参数
push ... 
push Arg1 
call Func_Address ; 压入当前EIP后跳转

跳转至被调方代码: 

push ebp ; 备份调用方EBP指针
mov ebp, esp ; 建立被调方栈底
sub esp, N * 4; 为局部变量分配空间
mov dword ptr[esp - 4 * 1 ], 0 ; 初始化各个局部变量 = 0 这里假定VarType不是类 
mov dword ptr[esp - 4 * ... ], 0
mov dword ptr[esp - 4 * N ], 0
. . . . . . ; 这里执行一些函数功能语句(比如将第N个参数[ebp + N * 4]存入局部变量), 功能完成后将函数返回值存至eax
add esp, N * ; 销毁局部变量
mov esp, ebp ; 恢复主调方栈顶
pop ebp ; 恢复主调方栈底
ret ; 弹出EIP 返回主调方代码

接上面调用方代码: 
add esp, N * ; 释放参数空间, 恢复调用前的栈 
mov dword ptr[ebp - 4], eax ; 将返回值保存进调用方的某个VarType型局部变量

进入函数时堆栈分配示意图
内存低地址 | ESP - - - - - - - - - - - - - - - - EBP - - - - - - - - - - - - - - - - - - - - - >| 内存高地址
Stack State: VarN . . . Var3 Var2 Var1 SFP EIP Arg1 Arg2 Arg3 . . . ArgN

//资料区...............................................................................................................................

SFP 解释:        除了堆栈指针(ESP指向堆栈顶部的的低地址)之外, 为了使用方便还有指向帧内固定地址的指针叫做帧指针(FP)。有些文章把它叫做局部基指针(LB-local base pointer)。从理论上来说, 局部变量可以用SP加偏移量来引用。 然而, 当有字被压栈和出栈后, 这些偏移量就变了。 尽管在某些情况下编译器能够跟踪栈中的字操作, 由此可以修正偏移量, 但是在某些情况下不能。而且在所有情况下, 要引入可观的管理开销。 而且在有些机器上, 比如Intel处理器, 由SP加偏移量访问一个变量需要多条指令才能实现。
        因此, 许多编译器使用第二个寄存器, FP, 对于局部变量和函数参数都可以引用, 因为它们到FP的距离不会受到PUSH和POP操作的影响。 在Intel CPU中, BP(EBP)用于这个目的。 在Motorola CPU中, 除了A7(堆栈指针SP)之外的任何地址寄存器都可以做FP。考虑到我们堆栈的增长方向, 从FP的位置开始计算, 函数参数的偏移量是正值, 而局部变量的偏移量是负值。
        当一个例程被调用时所必须做的第一件事是保存前一个FP(这样当例程退出时就可以恢复这个被保存的FP称为SFP)。 然后它把SP复制到FP, 创建新的FP, 把SP向前移动为局部变量保留空间。 这称为例程的序幕(prolog)工作。当例程退出时, 堆栈必须被清除干净, 这称为例程的收尾(epilog)工作。 Intel的ENTER和LEAVE指令, Motorola的LINK和UNLINK指令, 都可以用于有效地序幕和收尾工作。

         所有局部变量都在栈中由函数统一分配,形成了类似逆序数组的结构,可以通过指针逐一访问。这一特点具有很多有趣性质,比如,考虑如下函数,找出其中的错误及其造成的结果:

void f()

{

int i,a[10];

for(i=0;i<=10;++i)a[i]=0;/An error occurs here!

}

        这个函数中包含的错误,即使是C++新手也很容易发现,这是老生常谈的越界访问问题。但是这个错误造成的结果,是很多人没有想到的。这次的越界访问,并不会像很多新手预料的那样造成一个“非法操作”消息,也不会像很多老手估计的那样会默不作声,而是导致一个死循环。
        错误的本质显而易见,我们访问了a[10],但是a[10]并不存在。C++标准对于越界访问只是说“未定义操作”。我们知道,a[10]是数组a所在位置之后的一个位置,但问题是,是谁在这个位置上。是i! 
        根据前面的讨论,i在数组a之前被声明,所以在a之前分配在栈上。但是,I386上栈是向下增长的,所以,a的地址低于i的地址。其结果是在循环的最后,a[i]引用到了i自己!接下来的事情就不难预见了,a[i],也就是i,被重置为0,然后继续循环的条件仍然成立……这个循环会一直继续下去,直到在你的帐单上产生高额电费,直到耗光地球电能,直到太阳停止燃烧……呵呵,或者直到聪明的你把程序Kill了……


摘录二

  • 函数的调用操作包括:从一块代码到另一块代码之间的双向数据传递换和执行控制。其中:数据传递:函数参数和返回值。
  • 大多数CPU上的程序实现使用栈来支持函数调用操作。栈用来传递函数参数、存储返回信息、临时保存寄存器原有值以用于回复以及存储局部数据。
  • 单个函数调用操作所使用的栈部分称为栈帧结果(stack frame)。栈帧结构的两端由两个指针来指定。寄存器ebp通常用作栈帧的指针、esp用作栈的指针。esp随着数据的入栈和出栈。因此对于函数中大部分数据的访问都是通过基于帧帧指针ebp来实现。
         如下图显示:



批注:在C语言中,如果C语言的地址符'&'被应用到一个局部变量时,栈则需要为该变量生成一个地址值即变量的地址值分配一个空间。
call和ret用于处理函数调用和返回操作。
call:把返回地址压入栈中并且跳转到调用函数开始处并且执行。
ret:调用ret之前要好好处理栈中的内容。使得当前栈指针所指向的位置刚好为call指令保存的返回地址。另外若函数返回一个整数或指针,那么用eax返回。
main():一个函数:在编译的链接的时候它将会作为Ctr0.s汇编程序的函数被调用。ctro.s是一个桩(stu)程序,它被链接在一个用户程序的开始部分,主要用于设置一些初始化的全局变量等。
举例;
void  swap (int *a, int *b)
{
    int  c;
    c= *a;
    *a = *b;
    *b = c;
}
void caller()
{
int i =0,b=3;
    swap(&i,&b);
}
如图显示:(栈是向低地址扩展)


 注意:对于返回地址的保存是在跳转到调用函数之前(调用swap之前),然后对调用函数callee的栈帧指针进行保存,此外以该栈(内存地址)作为swap的栈帧指针,通过对ebp的上下移动来调用函数的参数和局部变量。对于该函数的参数则是ebp加正整数,对于局部变量的引用是通过edp减一个整数。
ebp前面的e:32位寄存器
rbp前面的r:    64位寄存器


原创粉丝点击