栈帧


http://blog.csdn.net/zhongguoren666/article/details/7586074

http://www.cnblogs.com/fxplove/articles/2574451.html

首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(地址地)。下图为典型的存取器安排,观察栈在其中的位置

 

入栈操作:push eax; 等价于 esp=esp-4,eax->[esp];如下图

出栈操作:pop eax; 等价于 [esp]->eax,esp=esp+4;如下图

我们来看下面这个C程序在执行过程中,栈的变化情况

void func(int m, int n) {

    int a, b;

    a = m;

    b = n;

}

main() {

...

    func(m, n);

L:  下一条语句

...

 

在main调用func函数前,栈的情况,也就是说main的栈帧:

从低地址esp到高地址ebp的这块区域,就是当前main函数的栈帧。当main中调用func时,写成汇编大致是:

push m

push n; 两个参数压入栈

call func; 调用func,将返回地址填入栈,并跳转到func

当跳转到了func,来看看func的汇编大致的样子:

__func:

        push ebp; 这个很重要,因为现在到了一个新的函数,也就是说要有自己的栈帧了,那么,必须把上面的函数main的栈帧底部保存起                        ; 来,栈顶是不用保存的,因为上一个栈帧的顶部讲会是func的栈帧底部。(两栈帧相邻的)

        mov ebp, esp; 上一栈帧的顶部,就是这个栈帧的底部

        ;暂时先看现在的栈的情况

                 ;到这里,新的栈帧开始了

                 sub esp, 8   ;  int a, b 这里声明了两个int,所以esp减小8个字节来为a,b分配空间

                 mov dword ptr [esp+4], [ebp+12];   a=m

                 mov dword ptr [esp], [ebp+8]; b=n         

   这样,栈的情况变为:

                    ret 8     ;  返回,然后8是什么意思呢,就是参数占用的字节数,当返回后,esp-8,释放参数m,n的空间

 

由此可见,通过ebp,能够很容易定位到上面的参数。当从func函数返回时,首先esp移动到栈帧底部(即释放局部变量),然后把上一个函数的栈帧底部指针弹出到ebp,再弹出返回地址到cs:ip上,esp继续移动划过参数,这样,ebp,esp就回到了调用函数前的状态,即现在恢复了原来的main的栈帧。


函数调用过程栈帧变化详解

函数调用另一个词语表示叫作 过程。一个过程调用包括将数据和控制从代码的一部分传递到另一部分。另外,它还必须在进入时为过程的局部变量分配空间,并在推出时释放这些空间。而数据传递,局部变量的分配和释放通过操纵程序栈来实现。在了解本文章之前,您需要先对程序的进程空间有所了解,即对进程如何使用内存?如果你知道这些,下面的内容将是很easy的事情了。为了您的回顾还是将简单的分布图贴出来,便于您的回顾。

我们先来了解一个概念,栈帧(stack frame),机器用栈来传递过程参数,存储返回信息,保存寄存器用于以后恢复,以及本地存储。为单个过程(函数调用)分配的那部分栈称为栈帧。栈帧其实是两个指针寄存器,寄存器%ebp为帧指针,而寄存器%esp为栈指针,当程序运行时,栈指针可以移动(大多数的信息的访问都是通过帧指针的)。总之简单一句话,栈帧的主要作用是用来控制和保存一个过程的所有信息的。栈帧结构如下所示:

  

如果你已经对这个图已经非常了解了,那么就没有必要再看下去了。因为下面的内容都是对这幅图的讲解。

  假设过程P(调用者)调用过程Q(被调用者),则Q的参数放在P的栈帧中。另外,当P调用Q时,P中的返回地址被压入栈中,形成P的栈帧的末尾(返回地址就是当程序从Q返回时应该继续执行的地方)。Q的栈帧从保存的帧指针的值开始,后面到新的栈指针之间就是该过程的部分了。

  过程实例讲解:

下面以这个程序为例进行简要说明函数调用的基本过程。

复制代码
int swap_add(int* xp,int* yp) {
    int x = *xp;
    int y = *yp;
    *xp = y;
    *yp = x;
    return x+y;
}
int caller(){
    int arg1 = 534;
    int arg2 = 1057;
    int sum = swap_add(&arg1,&arg2);
    int diff = arg1 - arg2;
    
    return sum * diff;
}
复制代码

经过汇编之后caller部分的代码如下:

复制代码
caller:
    pushl %ebp   //保存%ebp
    movl %esp,%ebp    //设置新的帧指针为旧的栈指针
    subl $24,%esp  //分配24子节的栈空间
    movl $534,-4(%ebp) //设置arg1=534
    movl $1057,-8(%ebp) //设置arg2=1057
    leal -8(%ebp),%eax //计算&arg2
    movl %eax,4(%esp) //将&arg2存入栈中
    leal -4(%ebp),%eax //计算&arg1
    movl %eax,(%esp) //将&arg1存入栈中
    call swap_add //调用swap_add
复制代码

这段代码先保存了%ebp的一个副本,将新的过程(该函数的ebp)的ebp设置为栈帧的开始位置。然后将栈指针减去24,从而在栈上分配了24字节的空间(你应该思考一下为什么是24字节),然后是初始化两个局部变量,计算两个局部变量的地址并存入栈中,形成了函数swap_add的参数。将这些参数存储到相对于栈指针偏移量为0和+4的地方,留待稍后的swap_add调用访问。然后调用swap_add.

接下的代码是swap_add的函数部分:

swap_add:
     pushl %ebp
     movl %esp,%ebp
     pushl %ebx
     
     movl 8(%ebp),%edx
     movl 12(%ebp),%ecx
     movl (%edx),%ebx
     movl (%ecx),%eax
     movl %eax,(%edx)
     movl %ebx,(%ecx)
     addl %ebx,%eax
     
     popl %ebx
     popl %ebp
     ret
    

代码分为3部分 建立部分:初始化栈帧;主体部分:执行过程的实体计算;结束部分:回复栈帧的状态,以及过程返回。这一部分的代码比较简单,就不在一一介绍,根据以上的3部分,划分的已经很清晰了。(说明一点程序在执行到swap_add的代码之前,也就是在执行call语句已经把返回地址压入栈中)值得注意的是最后一部分的popl %ebx   popl %ebp。它的作用是恢复了之前存储的栈帧指针的值,也就是调用程序的原始栈帧指针。从而程序就可以得到返回(有些细心的人会问那返回值咋么办?呵呵,返回值是存入了%eax中,在接下来的调用程序caller中直接访问该寄存器就可以了)。

下面就是返回之后继续执行的部分代码了:

movl -4(%ebp),%edx
subl -8(%ebp),%edx
imull %edx,%eax
leave
ret

为了计算diff,从栈中取出arg1,和arg2的值,并将寄存器%eax当做swap_add的返回值。

整个过程的栈变化如下所示:



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值