栈帧的实现实际跟系统本身的架构密切相关,这里主要对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]的反过程
至此,一个函数栈帧的销毁过程实际上已经完成