ARM体系函数调用过程分析

一、背景:

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 
您的支持是对博主最大的鼓励,感谢您的认真阅读。 
本文无所谓版权,欢迎转载。 

猜你喜欢

转载自blog.csdn.net/frank_zyp/article/details/88202347