ARM二进制程序的函数调用过程栈的变化详解

概要

本篇博客主要包括两个方面的内容:

  1. 整理栈涉及到的一些基本概念、ARM架构下栈相关的操作指令;
  2. 分析一个函数调用实例。

* 栈的基本知识

栈的概念


  • 首先,栈是一种先进后出(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。

以上过程示意图如下:

参考链接

  1. ARM汇编之栈与函数 函数调用栈示例分析
  2. ARM-栈 栈的基本概念和作用
  3. ARM下C语言栈帧机制
  4. [arm-汇编stmdb、ldmia、stmfd、ldmfd]
posted @ 2022-04-12 17:33  From_Zero  阅读(1492)  评论(2编辑  收藏  举报