i386和arm32架构下函数栈帧分析

栈帧的实现实际跟系统本身的架构密切相关,这里主要对i386和arm32两种架构下的栈帧展开分析。

下面关于栈帧的分析都将基于这个例子进行:

int func(int a,int b)
{
    int sum;
    sum = a + b;
    return sum;
}
int main(void)
{
    int sum;
    sum = func(1,2);
    return 0;
}

1. i386中的栈帧
下面是i386架构系统上跟栈帧相关的一组寄存器:
        4个数据寄存器(eax、ebx、ecx、edx)       这4个是通用数据寄存器,主要用于存放变量值,其中eax还用于传递函数返回值
        ebp                                     基址指针寄存器,用于存放一个指针,指向当前栈帧的栈底
        esp                                     栈指针寄存器,用于存放一个指针,指向当前栈帧的栈顶
        eip                                     指令指针寄存器,用于存放一个指针,指向下一条将要执行的指令,该寄存器不能直接操作,只能通过call、ret指令间接操作

        
i386架构系统上当前函数对应的典型栈帧布局:
        ^           ^               |                                                                                                                  |
        |            |                | 函数实参n                                                                                               |
        |        偏移为正     | ......                                                                                                           |
        |            |           +8 | 函数实参1                                                                                               |
                                 +4 | 返回地址,实际就是指向当前函数返回后下一条将要执行的指令 |
     地址增大    ebp --> | 保存的前一个栈帧的ebp                                                                      |
                                  -4 | 函数局部变量1                                                                                       |
        |           |                 | ......                                                                                                            |
        |        偏移为负     | ......                                                                                                            |
        |           |    esp --> | 函数局部变量n                                                                                        |
        |           v                |                                                                                                                   |

当函数被调用时,新的栈帧被压入栈中,当函数返回时,相应的栈帧从栈中弹出。
ebp中存放的就是栈基址,指向当前函数的栈底,显然在当前函数生命周期中该位置是固定的。
访问当前函数的实参和局部变量的方法就是以ebp为基址,再加上一个偏移,由上图可知,实参的偏移为正,局部变量的偏移为负。
esp中存放的就是栈指针,指向当前函数的栈顶,显然该位置在当前函数生命周期中会发生变化。       
上面范例在i386中的汇编实现如下:

        func:
            pushl    %ebp
            movl     %esp, %ebp
            subl     $16, %esp
            movl     8(%ebp), %edx
            movl     12(%ebp), %eax
            addl     %edx, %eax
            movl     %eax, -4(%ebp)
            movl     -4(%ebp), %eax
            leave
            ret
        main:
            pushl    %ebp
            movl     %esp, %ebp
            subl     $16, %esp
            pushl    $2
            pushl    $1
            call     func
            addl     $8, %esp
            movl     %eax, -4(%ebp)
            movl     $0, %eax
            leave
            ret

基于上面的汇编实现可知i386中一个函数栈帧的建立和销毁过程:
    建立过程:
        [1]. 首先将新函数的实参从右向左依次压栈
        [2]. 接着调用call指令将eip中的地址压栈作为新函数栈帧销毁后的返回地址,并将新函数的入口地址赋给eip,从而跳转到新函数入口
        [3]. 最后将ebp压栈,并将esp赋给ebp,这一步确立了新函数的栈底和栈顶位置,也意味着新函数的栈帧框架正式建立
    销毁过程:
        [1]. 如果函数有返回值,首先将返回值赋给eax
        [2]. 接着调用leave指令将ebp赋给esp,并从新的栈顶弹出一个值赋给ebp,显然这就是栈帧建立过程中步骤[3]的反过程
        [3]. 最后调用ret指令从栈顶弹出一个值赋给eip,显然这就是栈帧建立过程中步骤[2]的反过程。
             至此,一个函数栈帧的销毁过程实际上已经完成


2. arm32中的栈帧
相对于i386架构系统上相对统一的栈帧结构,arm32架构cpu上的栈帧实现就显得非常多变,这里将分析的是较典型的一种。
下面是arm32架构系统上跟栈帧相关的一组寄存器:
            r0~r3      这些寄存器既用于传递函数的入参,又用于传递函数的返回值(通常只是r0),在当前函数运行期间还可用于存放普通变量
            fp          栈帧指针寄存器,相当于i386中的ebp
            sp          栈指针寄存器,相当于i386中的esp
            pc          程序计数器,存放了下一条将要执行指令的位置,相当于i386中的eip
            lr          连接寄存器,用于存放当前函数的返回地址,i386中返回地址被存放在栈中

上面范例在arm32中的汇编实现如下:
 

        func:
            str fp, [sp, #-4]!
            add fp, sp, #0
            sub    sp, sp, #20
            str    r0, [fp, #-16]
            str    r1, [fp, #-20]
            ldr    r2, [fp, #-16]
            ldr    r3, [fp, #-20]
            add    r3, r2, r3
            str    r3, [fp, #-8]
            ldr    r3, [fp, #-8]
            mov    r0, r3
            sub    sp, fp, #0
            ldr    fp, [sp], #4
            bx    lr
        main:
            stmfd    sp!, {fp, lr}
            add    fp, sp, #4
            sub    sp, sp, #8
            mov    r1, #2
            mov    r0, #1
            bl    func
            str    r0, [fp, #-8]
            mov    r3, #0
            mov    r0, r3
            sub    sp, fp, #4
            ldmfd    sp!, {fp, lr}
            bx    lr

基于上面的汇编实现可知arm32中一个函数栈帧的建立和销毁过程:
    建立过程:
        [1]. 首先将新函数的实参从左向右依次赋给r0~r3,如果函数存在超过4个的情况,则多余的参入从右向左依次压栈
        [2]. 接着调用bl指令将pc中的地址赋给lr作为新函数栈帧销毁后的返回地址,并将新函数的入口地址赋给pc,从而跳转到新函数入口
        [3]. 最后将fp压栈,并将sp赋给fp,这一步确立了新函数的栈底和栈顶位置,也意味着新函数的栈帧框架正式建立
    销毁过程:
        [1]. 如果函数有返回值,首先将返回值赋给r0
        [2]. 接着将fp赋给sp,并从新的栈顶弹出一个值赋给fp,显然这就是栈帧建立过程中步骤[3]的反过程
        [3]. 最后调用bx指令,跳转到lr指向的返回地址,显然这就是栈帧建立过程中步骤[2]的反过程
             至此,一个函数栈帧的销毁过程实际上已经完成

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值