创建进程的时候,会分配内存作为栈。
实际栈指令(push,pop
)是其他指令的别名,实际指令是STR、LDR或者其延伸指令
。例STMDB sp,{R0,R1}
== push {r0, r1}
栈实现的几种情况
堆栈类型 | store | load |
---|---|---|
完全下降(高地址在上,低地址在下) | STMFD(STMDB) | LDMFD(LDM) |
完全升序(高地址在下,低地址在上) | STMFA (STMIB) | LDMFA (LDMDA) |
空升序(高地址在下,低地址在上) | STMEA (STM) | LDMEA (LDMDB) |
空降序(高地址在上,低地址在下) | STMED (STMDA) | LDMED (LDMIB) |
总结:
-
升序A
:高地址在下,低地址在上 -
降序D
:高地址在上,低地址在下 -
完全F
:入栈的时候:SP先减4,然后在写数据
。出栈的时候:先写数据再执行SP+4
-
空E
:入栈的时候:先写数据,然后再SP减4
。出栈的时候:先执行SP+4再写数据
基础姿势:栈操作
返回地址LR
:当前函数执行完,跳转的下一条指令,通常是call当前函数所在地址的下一条地址。例如,下面的代码中:当执行进son_add
函数中时,返回地址就是int c=10;
这条指令的地址,确保执行完son_add
函数后,返回到父函数father
中继续执行流程,调用后面的代码。也就是数返回地址保证的是每一个函数块间的指令流程
PC
:程序计数器,通常指向下一条指令的地址。例如:当前程序执行到了int a=8;
,当处理器编译这条指令的时候,PC所存储的值,就是int b = 9;
这条指令的内存地址,也就是PC寄存器保证的是每一条指令间的指令流程
栈帧(r11)
:函数调用的时候,会在栈中进行,每一个函数执行过程就是一个栈帧
帧指针:
栈帧首地址的内存地址。也就是返回地址的内存地址
int son_add(int a, int b)
{
return a+b;
}
int father()
{
int a = 8;
int b = 9;
int sum = 0;
sum = son_add(a, b);
int c=10;
return sum;
}
栈帧结构
:下面这个图可以适用于上面代码中的fahter
函数,如果是son_add
的栈帧,只需要减少一步:返回地址的入栈操作
,为什么会少这一步呢?首先需要知道:lr只表示当前函数的返回地址,如果当前函数有子函数,进入子函数lr就需要保存子函数的返回地址,但是父函数的返回地址就会被覆盖,所以需要先存起来,等子函数执行完成返回到父函数执行流程中,重新将返回地址存入lr中即可。因为上面的子函数内部不需要调用函数,所以不存在覆盖情况,就只用lr即可。
函数栈调用的详细分析
- 当函数传入的参数超过4个时,需要用到堆栈存储其余参数
- r0来保存返回结果
- 当返回结果超过32位,就需要用r0和r1结合来保存结果
/* azeria@labs:~$ gcc func.c -o func && gdb func */
int main()
{
int res = 0;
int a = 1;
int b = 2;
res = max(a, b);
return res;
}
int max(int a,int b)
{
do_nothing();
if(a<b)
{
return b;
}
else
{
return a;
}
}
int do_nothing()
{
return 0;
}
序幕:为函数设置环境、
push {r11, lr} /* 序幕开始:保存帧指针和返回地址到堆栈*/
add r11, sp, #0 /* 设置当前栈帧的帧指针,指向栈帧的头部地址,即返回地址的地址 */
sub sp, sp, #16 /* 序幕结束:会根据参数和局部变量分配相应的堆栈空间 */
函数的正文:实现函数的逻辑,并将返回结果赋值给r0
mov r0, #1 /*传入参数 */
mov r1, #2 /*传入参数 */
bl max
结尾:恢复到初始状态,以便从父函数离开的地方继续执行
sub sp, r11, #0 /* 调整栈指针 */
pop {r11, pc} /* 恢复帧指针,并将返回地址放入pc中,实现直接跳转到返回地址处。在此步骤中,栈帧会被销毁 */