一、背景:
1、栈描叙:
栈作为一种数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的
原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据
(最后一个数据被第一个读出来)。栈具有记忆作用,对栈的插入与删除操作中,不需要改变栈底指针。
进函数需要压栈操作,保存需要的信息;出函数时需要出栈操作,恢复现场。
2、特殊寄存器:
r0 ~ r3 通常用于传参;
r15 -> pc => 当前程序执行位置;
r14 -> lr => 连接寄存器:跳转指令自动把返回地址放入r14中;
r13 -> sp => 栈指针:指向上一帧的栈底;
r12 -> ip => ip 内部过程调用寄存器Intra-Procedure-call scratch register,其实就是r12;
r11 -> fp => 当前函数栈帧的栈底,也就是栈基地址FP;
BL NEXT ;跳转到子程序NEXT处执行
......... ;
NEXT
..........
MOV PC,LR ;从子程序返回,
这里的BL是跳转的意思,LR(R14)保存了返回地址
PC(R15)是当前地址,把LR给PC就是从子程序返回
二、调用过程:
下图描述的是ARM的栈帧布局方式,main stack frame为调用函数的栈帧,func1 stack frame为
当前函数(被调用者)的栈帧,栈底在高地址,栈向下增长。图中FP就是栈基址,它指向函数的栈帧起始地址;
SP则是函数的栈指针,它指向栈顶的位置。ARM压栈的顺序很是规矩,依次为当前函数指针PC、返回指针LR、
栈指针SP、栈基址FP、传入参数个数及指针、本地变量和临时变量。先压栈的main stack 进入在高地址。
具体函数:
int func(int a, int b, int c, int d)
{
return 1;
}
int main()
{
int i = 1, j = 2;
func(i, j, 3, 4);
return 0;
}
对应的汇编代码:
.text:000083D0 EXPORT main
.text:000083D0 main ; DATA XREF: .text:000082C4o
.text:000083D0 ; .text:off_82DCo
.text:000083D0
.text:000083D0 IP = R12
.text:000083D0 FP = R11
.text:000083D0 MOV IP, SP //保存SP
.text:000083D4 STMFD SP!, {FP,IP,LR,PC} //压栈
.text:000083D8 SUB FP, IP, #4 //取得FP基址,便可访问栈内所有的地址数据
.text:000083DC SUB SP, SP, #8 //局部变量用
.text:000083E0 MOV R3, #1
.text:000083E4 STR R3, [FP,#a]
.text:000083E8 MOV R3, #2
.text:000083EC STR R3, [FP,#b]
.text:000083F0 LDR R0, [FP,#a]
.text:000083F4 LDR R1, [FP,#b]
.text:000083F8 MOV R2, #3
.text:000083FC MOV R3, #4
.text:00008400 BL func
.text:00008404 MOV R3, #0
.text:00008408 MOV R0, R3
.text:0000840C SUB SP, FP, #0xC
.text:00008410 LDMFD SP, {FP,SP,PC} //出栈恢复现场
.text:00008410 ; End of function main
在main函数中,使用IP(R12)暂时保存栈指针sp,然后使用堆栈操作指令stmfd将栈帧(FP)、IP、
程序返回地址(LR)、程序计数器(PC)压栈,以保护现场,然后使用sub fp,ip,#4使fp指向当前函数栈帧
的栈底,sub sp,sp,#8,为当前函数局部变量分配看空间。接下来通过寄存器传递参数r1,r2,r3,r4。使用BL
指令调用函数,BL指令同时也会将当前指令的下一条指令地址赋给LR,以跳转回来。最后使用ldmfd恢复现场。
通过上面的简单程序可以发现如下规律:
每个linux函数的汇编源码开头基本都是如下结构。
ex:Dump of assembler code for function proc_pid_stack:
0xc03240d4 <+0>: mov r12, sp // ①任何一个函数被调用的那一刻,第一步都是把指向父函数(上一层函数)栈底的sp存到r12(ip)中。(注意:SP只有一个,没有父子之说,执行完子函数,SP还会指回父函数的栈的)
0xc03240d8 <+4>: push {r4, r5, r6, r7, r8, r9, r11, r12, lr, pc} // ②:编译器知道到底需要用多少个寄存器,一并入栈准备,push入栈是从pc->lr->r12->r11->...->r5-r4
0xc03240dc <+8>: sub r11, r12, #4 // ③:r11-4,是往低地址(memory low address)走4个字节,r11是用于访问函数中局部变量的指针(非栈指针sp)
0xc03240e0 <+12>: sub sp, sp, #24 // ④:此部分全部给局部变量用,一般需要多少就偏移多少个字节。
作者:frank_zyp
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文无所谓版权,欢迎转载。