ARM二进制程序的函数调用过程栈的变化详解
概要
本篇博客主要包括两个方面的内容:
- 整理栈涉及到的一些基本概念、ARM架构下栈相关的操作指令;
- 分析一个函数调用实例。
* 栈的基本知识
栈的概念
- 栈
首先,栈是一种先进后出(FILO)的数据结构,栈底是第一个进栈数据所在的位置,栈顶是最后一个进栈数据所在的位置。
其次,栈也是内存中的一段特殊空间,用于存放函数参数、函数上下文(寄存器)、函数返回地址、局部变量等。
Ps. 返回值一般是在R0寄存器中返回,不使用栈。
-
栈帧
系统在运行过程中,会为每个进程分配一个栈空间(互不干扰),而进程中的每个函数在被调用时会分配栈上的一块连续的空间区域,这个空间区域就是栈帧,函数在执行过程中可以随意使用属于自己栈帧上的空间,当函数返回时,栈帧空间将被收回,后续可能会分配给其他函数使用。每个函数的栈帧使用栈顶指针和栈底指针来界定。如下图所示:
-
栈的相关寄存器和指针
寄存器 | 名称 | 功能 |
---|---|---|
R11 | frame pointer,FP寄存器 | 用于保存FP指针,即栈底指针 |
R12 | IP寄存器 | 用于暂存SP,即栈顶 |
R13 | stack pointer,SP寄存器 | 用于保存SP指针,即栈顶指针 |
R14 | link register,LR寄存器 | 用于保存函数的返回地址 |
R15 | PC寄存器 | 用于保存程序计数器的值 |
栈的分类
-
满/空栈【Full/Empty】
根据SP指针指向的位置,栈可分为满栈和空栈,如下图:
SP指向最后入栈的数据时为满栈(左图);SP指向下一个将要放入数据的空位置时为空栈(右图)。形象的理解为,SP指针指向最后一个数据,这个栈没有空位置,那就是满的;而当SP指针指向一个空位置,那么这个栈就不满,就是空的。
-
升/降栈【Ascending/Descending】
根据SP指针的移动方向,栈可以分为升栈和降栈,如下图:
当数据入栈时,SP指针从低地址向高地址移动(即栈的生长方向为Low->High),此为升栈(左图);相反的,SP指针从高地址向低地址移动(即栈的生长方向为High->Low),此为降栈(右图)。
-
栈的四种情况
综上,两两结合可以得到四种类型的栈,即:
满降栈FD | 满升栈FA | 空降栈ED | 空升栈EA |
---|---|---|---|
Ps. ARM为满降栈。
栈相关指令(存取指令)
##以32位ARM为例,ARM64中栈的指令有所改变##
首先需要明确的一点是,存取指令有很多,但实际的使用需要结合栈的种类。如果是满栈的话,肯定是需要先改变SP指针再压栈;出栈则相反,需要先出栈,再改变SP指针。而如果是空栈,压栈时先压入数据,再改变SP指针;出栈时,先移动SP指针,再弹出数据。
批量存取指令为:LDM(取)和STM(存),即load multiple & store multiple。
结合四种类型的栈,那么就会存在以下四对存取指令:
😃 | 满降栈 | 满升栈 | 空降栈 | 空升栈 |
---|---|---|---|---|
批量存 | STMFD | STMFA | STMED | STMEA |
批量取 | LDMFD | LDMFA | LDMED | LDMEA |
ARM是满降栈,因此使用STMFD和LDMFD指令。
STMFD指令即向栈中压入多个数据,采用事先递减方式(DB,before decrease),先将SP指针减小,再压入数据;
LDMFD指令即弹出栈中的多个数据,采用事后递增方式(IA,after increase),先弹出数据,再将SP指针增大。
其他指令类似,在这里不做过多讨论。重点是要理解不同类型栈的压栈和出栈规律。
Ps. 此外还有事后递减(DA)和事先递增(IB)方式。
* 函数调用实例分析栈的变化
下面看一个实例,如下图,main函数调用test_b函数,在test_b函数中首先是函数序言(Prologue,一般用于在栈中保存一些环境变量等),然后是test_b函数的函数体(完成相应功能),最后是函数结语(Epilogue,一般用于恢复栈中保存的变量,准备好返回main函数的环境)。
函数序言
执行STMFD指令,向栈中压入R11和LR,即main函数的FP指针和返回地址,确保test_b函数执行完毕后可以正确返回main函数中的相应位置继续执行后续指令以及可以正确恢复main函数的栈帧。随着数据的压入,SP = SP - 8。
执行ADD指令,更新当前的栈底指针(R11寄存器,FP指针),指向test_b函数栈帧的栈底。
执行SUB指令,抬高栈顶,为test_b函数创建栈帧,后续代码的执行可以用到这块栈空间。
以上过程示意图如下:
函数结语
执行SUB指令,更新SP = FP - 4,即指向保存的R11值。
执行LDMFD指令,弹出之前保存的main函数栈底指针,以及返回地址赋值给PC寄存器,这样的话main函数的执行流和栈帧全部恢复了,然后SP = SP + 8。
以上过程示意图如下:
参考链接
- ARM汇编之栈与函数 函数调用栈示例分析
- ARM-栈 栈的基本概念和作用
- ARM下C语言栈帧机制
- [arm-汇编stmdb、ldmia、stmfd、ldmfd]