1 概念
栈是一种具有后进先出性质 的数据组织方式,也就是说后存放的先取出,先存放的后取出。栈底是第一个进栈
的数据所处的位置,栈顶是最后一个进栈的数据所处的位置。
2 分类
根据SP指针指向的位置,栈可以分为满栈和空栈。
1. 满栈:当堆栈指针SP总是 指向最后压入堆栈的数据
2. 空栈:当堆栈指针SP总是 指向下一个将要放入数据空位置
ARM采用满栈!
根据SP指针移动的方向,栈 可以分为升栈和降栈。
1. 升栈:随着数据的入栈, SP指针从低地址->高地址 移动
2. 降栈:随着数据的入栈, SP指针从高地址->低地址 移动
ARM采用降栈!
3 栈桢
简单的讲,栈帧(stack frame) 就是一个函数所使用的那部分栈,所有函数的栈帧串起来就组成了一个完整的栈。
栈帧的两个边界分别由fp(r11)和sp(r13)来限定。
3 代码分析
通过一个简单的程序分析栈的作用与栈的操作。
代码清单:
1 int func_a(int a0, int a1, int a2, int a3, int a4, int a5)2 {34 return a5;5 }678 int main(void)9 {10 int a, b;1112 a = 1;13 b = 2;14 b = func_a(0, 1, 2, 3, 4, 5);1516 return 0;17 }
通过编译、反汇编得到相应的汇编代码,真相就在汇编代码。
编译:arm-linux-gcc -g test.c -o test
反汇编:arm-linux-objdump -D -S test > test_dump
查看test_dump: vi test_dump
184 00008380 <func_a>:185 int func_a(int a0, int a1, int a2, int a3, int a4, int a5)186 {187 8380: e52db004 push {fp} ; (str fp, [sp, #-4]!)188 8384: e28db000 add fp, sp, #0189 8388: e24dd014 sub sp, sp, #20190 838c: e50b0008 str r0, [fp, #-8]191 8390: e50b100c str r1, [fp, #-12]192 8394: e50b2010 str r2, [fp, #-16]193 8398: e50b3014 str r3, [fp, #-20]194195 return a5;196 839c: e59b3008 ldr r3, [fp, #8]197 }198 83a0: e1a00003 mov r0, r3199 83a4: e28bd000 add sp, fp, #0200 83a8: e8bd0800 pop {fp}201 83ac: e12fff1e bx lr202203 000083b0 <main>:204205206 int main(void)207 {208 83b0: e92d4800 push {fp, lr}209 83b4: e28db004 add fp, sp, #4210 83b8: e24dd010 sub sp, sp, #16211 int a, b;212213 a = 1;214 83bc: e3a03001 mov r3, #1215 83c0: e50b300c str r3, [fp, #-12]216 b = 2;217 83c4: e3a03002 mov r3, #2218 83c8: e50b3008 str r3, [fp, #-8]219 b = func_a(0, 1, 2, 3, 4, 5);220 83cc: e3a03004 mov r3, #4221 83d0: e58d3000 str r3, [sp]222 83d4: e3a03005 mov r3, #5223 83d8: e58d3004 str r3, [sp, #4]224 83dc: e3a00000 mov r0, #0225 83e0: e3a01001 mov r1, #1226 83e4: e3a02002 mov r2, #2227 83e8: e3a03003 mov r3, #3228 83ec: ebffffe3 bl 8380 <func_a>229 83f0: e1a03000 mov r3, r0230 83f4: e50b3008 str r3, [fp, #-8]231232 return 0;233 83f8: e3a03000 mov r3, #0234 }235 83fc: e1a00003 mov r0, r3236 8400: e24bd004 sub sp, fp, #4237 8404: e8bd8800 pop {fp, pc}
3.1 函数内部分析
[1] main函数
第208条汇编指令 push {fp, lr}; 即str fp [sp, #-4]!; str lr [sp, #-4]!;
作用:
(1)将fp中的值赋给地址sp-4;
(2)将lr中的值赋给地址sp-8;
(3)将sp-8保存到sp中,“!”表示同时改变sp本身的值;
第209条指令 add fp, sp, #4
作用:
(1)将sp-4赋值给fp,作为main新栈的上边界;
第210条指令 sub sp, sp, #16
作用:
(1)将sp的值减16,作为main新栈的下边界,至此main新栈已开辟完成;
a = 1;
对应的汇编指令为:
mov r3, #1;
str r3, [fp, #-12]; 地址 fp-12存储的就是a的值,且值为1
对b的赋值也是如此。说明
若一个变量被声明了,但没有使用,则不会在栈中分配内存。
如果上诉代码中的b = 2;注释掉,则汇编代码中不会有str r3, [fp, #-8],就是说没有分配栈的内存。
b = func_a(0, 1, 2, 3, 4, 5);
后2个参数保存在栈mov r3, #4str r3, [sp]mov r3, #5str r3, [sp, #4]
前4个保存在寄存器mov r0, #0mov r1, #1mov r2, #2mov r3, #3
跳转到func_abl 8380 <func_a>
将返回值存储在r3mov r3, r0
将返回值返回给局部变量str r3, [fp, #-8]
func_a的形参达到6个,从左往右的后2个参数被保存到栈中,前四个直接保存在CPU的寄存器。
mov r0, r3; r3存放的是main的返回值
add sp, fp, #0; 将fp的值返回给sp,fp存储的是原先的sp的值减4
pop {fp}; 弹出fp,并且sp的值加4,fp和sp均恢复到调用main函数之前的状态
bx lr; 跳转到原先的指令处
待main函数执行完了,则对栈进行回收处理。
[2]func_1函数
push {fp} ; (str fp, [sp, #-4]!); 将寄存器fp中的值存储在地址sp-4中add fp, sp, #0; fp中保存sp的值sub sp, sp, #20; 确定func_a的栈的下边界将寄存器的形参存储在栈中,寄存器只起一个中转的作用str r0, [fp, #-8];str r1, [fp, #-12];str r2, [fp, #-16];str r3, [fp, #-20];
return a5;将a5的值存储在main的栈中,即将a5的值返回给main中的局部变量ldr r3, [fp, #8];
[3]func_a 函数执行完成后
4 作用mov r0, r3; 将返回值转存在r0中,r0座位默认的函数返回寄存器add sp, fp, #0; 将fp的值返回给sp,fp存储的是原先的sp的值减4pop {fp}; 弹出fp,并且sp的值加4,fp和sp均恢复到调用main函数之前的状态bx lr; 跳转到原先的指令,lr存储的是返回的地址
总的来说,栈的作用为如下所示:
[1]保存局部变量
[2]传递函数的参数
[3]保存寄存器的值