函数调用栈空间以及fp寄存器

本文探讨了ARM架构中函数调用栈的空间分配及其管理机制。重点介绍了fp和sp寄存器的功能,以及它们在函数调用过程中的作用。通过实际代码示例,展示了GCC编译器对于函数现场保存的具体实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Arm汇编学习笔记(六)——函数调用栈空间以及fp寄存器

原创 2016年08月17日 12:44:12

Arm上函数调用的规则在ARM System Developer's Guide文档中的ATPCS部分有详细的定义,这里主要通过函数调用过程中函数栈的情况来说明fp和sp等寄存器的作用。有关ATPCS的详细内容可以去文档中看。


fp叫做frame pointer寄存器,即栈帧指针寄存器;sp叫做stack pointer寄存器,即栈指针寄存器。那么它们具体的作用是什么呢?

首先,大家知道每个进程都有自己独立的栈空间,进程中有千千万万的函数调用,这些函数共享进程的这个栈空间,那么问题就来了,函数运行过程中会有非常多的入栈出栈的过程,当函数返回backtrace的时候怎样能精确定位到返回地址呢?还有子函数所保存的一些寄存器的内容?这样就有了栈帧的概念,即每个函数所使用的栈空间是一个栈帧,所有的栈帧就组成了这个进程完整的栈。而fp就是栈基址寄存器,指向当前函数栈帧的栈底,sp则指向当前函数栈帧的栈顶。通过sp和fp所指出的栈帧可以恢复出母函数的栈帧,以此类推就可以backtrace出所有函数的调用顺序。

那一个函数的栈帧具体的范围什么呢?fp和sp具体应该指向什么位置呢?请看下图:


图1

上图描述的是ARM的栈帧布局方式,main stack frame为调用函数的栈帧,func1 stack frame为当前函数(被调用者)的栈帧,栈底在高地址,栈向下增长。此图是网上的图,理论上应该是上图的格式,fp、sp、lr和pc这四个寄存器是非常特殊的寄存器,它们记录了当前正在运行的函数一些重要信息,在刚进入一个新的函数开始执行的时候,它们保存的是上个函数的信息,需要将它们入栈保存起来,这很重要!这些并没有定义在ATPCS中,ATPCS规定的是函数调用的时候参数如何传递,以及函数返回值的保存等。上面的这些个人觉得是一种默契,定义函数现场的保存及恢复,这些默契包括ATPCS都是人为的一种约束,目的是为了保证程序运行中不会出错,具体怎样实现应该是不同的编译器不尽相同。

下面我为了验证arm的gcc编译器的实现,自己写了个小实验程序:

  1. main.c:  
  2.   
  3.   
  4.   
  5. #include <stdio.h>  
  6.   
  7. int func(int i);  
  8.   
  9. int main(void)  
  10. {  
  11.   int i = 25;  
  12.   func(i);  
  13.   return 0;  
  14. }  

  1. func.c  
  2.   
  3. int func(int i)  
  4. {  
  5.   int a = 2;  
  6.   return a * i;  
  7. }  

main.c中调用了func函数,而func函数的实现在func.c文件中。下面是用arm-linux-androideabi-gcc编译后的执行文件反汇编出来的代码:
[plain] view plain copy
  1. Disassembly of section .text:  
  2.   
  3. 0000822c <func>:  
  4.     822c:   e52db004    push    {fp}        ; (str fp, [sp, #-4]!)  
  5.     8230:   e28db000    add fp, sp, #0  
  6.     8234:   e24dd014    sub sp, sp, #20  
  7.     8238:   e50b0010    str r0, [fp, #-16]  
  8.     823c:   e3a03002    mov r3, #2  
  9.     8240:   e50b3008    str r3, [fp, #-8]  
  10.     8244:   e51b3008    ldr r3, [fp, #-8]  
  11.     8248:   e51b2010    ldr r2, [fp, #-16]  
  12.     824c:   e0030392    mul r3, r2, r3  
  13.     8250:   e1a00003    mov r0, r3  
  14.     8254:   e24bd000    sub sp, fp, #0  
  15.     8258:   e49db004    pop {fp}        ; (ldr fp, [sp], #4)  
  16.     825c:   e12fff1e    bx  lr  
  17.   
  18. 00008260 <main>:  
  19.     8260:   e92d4800    push    {fp, lr}  
  20.     8264:   e28db004    add fp, sp, #4  
  21.     8268:   e24dd008    sub sp, sp, #8  
  22.     826c:   e3a03019    mov r3, #25  
  23.     8270:   e50b3008    str r3, [fp, #-8]  
  24.     8274:   e51b0008    ldr r0, [fp, #-8]  
  25.     8278:   ebffffeb    bl  822c <func>  
  26.     827c:   e3a03000    mov r3, #0  
  27.     8280:   e1a00003    mov r0, r3  
  28.     8284:   e24bd004    sub sp, fp, #4  
  29.     8288:   e8bd8800    pop {fp, pc}  

上面的汇编代码可以看到,并没有想上面图中所画的,将fp, sp, lr, pc全部都入栈,而是只入栈这四个寄存器中有改动的。fp是肯定要保存的,它指向的是每个函数栈帧的栈基址,而sp一般不用入栈,因为它的值一般保存在fp中,因为刚进入一个函数的时候,将上个函数的fp入栈保存以后,当前函数的栈空间应该是空的,fp应该指向与sp相同的位置,然后才会对sp做减法来分配栈空间保存临时变量。而如果当前函数中没有对其它函数的调用的时候,是不会对lr寄存器做修改的,所以也就不用保存了。

但是上面对main函数和func函数的fp指针所指向的位置也不完全相同,main函数中fp指向的是上个fp保存的内存地址,而func中的fp指向的是sp相同的位置。但是只要恢复的时候相对应不出错就可以了,上面也说过ATPCS这些规定都是人为的一种约束,保证backtrace的时候可以把正确的内容恢复到寄存器中,具体怎么实现并没有特别死板的定义。


另外一个比较重要的东西就是出入栈的顺序,在ARM指令系统中是地址递减栈,入栈操作的参数入栈顺序是从右到左依次入栈,而参数的出栈顺序则是从左到右的你操作。包括push/pop和LDMFD/STMFD等。

比如指令 push {fp, sp, lr, pc}执行的结果就是图1中栈的样子,pc被首先入栈存在高地址,从右到左依次入栈,fp存在低地址。

这些是比较细节和基础的东西,同时也是需要搞清楚的。



参考链接:

1. http://www.linuxidc.com/Linux/2013-03/81247.htm

2. http://www.cnblogs.com/chyl411/p/4579053.html

3. http://www.cnblogs.com/fanzhidongyzby/p/5250116.html


<think>我们正在讨论ARM架构下函数调用的工作过程。根据ARM架构的特点,函数调用是通过指针(SP)和链接寄存器(LR)来管理的。当一个函数调用另一个函数,它会将返回地址存入LR,同调整指针来为局部变量和保存的寄存器分配空间。 在ARM中,函数调用遵循一定的约定,称为AAPCS(ARM Architecture Procedure Call Standard)。根据这个标准,函数调用,参数通过寄存器R0-R3传递,如果参数多于4个,则多余的参数通过传递。函数返回,返回值通常存放在R0中。 函数调用的关键操作包括: 1. 调用函数(使用BL指令),将返回地址(当前PC+4)存入LR(R14)。 2. 被调用函数负责保存它将要修改的寄存器(包括LR,因为LR在函数执行过程中可能会被覆盖),以及为局部变量分配栈空间。 3. 被调用函数通常以“POP {PC}”或类似的指令返回,这将从中恢复LR到PC,实现返回。 具体步骤: a) 函数入口(prologue): - 将LR和需要保存的寄存器压入(通常使用PUSH指令)。 - 调整指针(SP)为局部变量分配空间(如果需要)。 b) 函数体执行。 c) 函数出口(epilogue): - 释放局部变量占用的栈空间(如果需要)。 - 从中恢复保存的寄存器,包括将返回地址弹出到PC(通常使用POP指令,或者直接恢复LR然后使用MOV PC, LR)。 需要注意的是,在ARM中,通常是从高地址向低地址增长的(满递减)。 下面我们用一个简单的例子来说明: 假设有两个函数:func1和func2,func1调用func2。 func1的代码片段: ... BL func2 ; 调用func2,LR设置为返回地址(下一条指令地址) ... ; 返回点 func2的代码片段(假设需要保存R4-R6和LR): PUSH {R4-R6, LR} ; 保存寄存器,包括LR SUB SP, SP, #8 ; 为局部变量分配8字节空间 ... ; func2的函数体 ADD SP, SP, #8 ; 释放局部变量空间 POP {R4-R6, PC} ; 恢复寄存器,并将LR的值弹出到PC,实现返回 注意:在POP指令中,我们将LR弹出到PC,这样函数就直接返回到调用处。 另外,在ARM Thumb模式下,POP指令可以直接弹出到PC,这是常见的返回方式。 引用[1]中提到了ARM架构的机器描述结构,其中包含了架构编号、名称、设备树兼容字符串等信息,虽然不直接涉及调用,但说明ARM架构有不同的变体,而调用机制在核心上是一致的。 引用[2]提到主要针对x86/ARM Linux二进制文件,说明我们的讨论同样适用于ARM Linux环境。 引用[3]和[4]与当前问题关联不大,因此不参考。 因此,ARM架构下函数调用的工作过程可以总结为: - 使用来保存返回地址寄存器的值。 - 遵循标准的调用约定(AAPCS)。 - 通过BL指令调用函数并设置LR,通过POP {PC}或等效指令返回。 相关问题: 1. ARM架构中,当函数参数多于4个,如何传递额外的参数? 2. 在ARM函数调用中,哪些寄存器是调用者保存的(caller-saved),哪些是被调用者保存的(callee-saved)? 3. ARM的中通常包含哪些内容? 4. 在异常处理(如中断),ARM的操作有何不同?</think>### ARM架构函数调用工作过程详解 在ARM架构中,函数调用通过寄存器协作和内存管理实现,核心过程如下: #### 1. **关键寄存器作用** - **SP(Stack Pointer, R13)**:指向当前顶位置 - **LR(Link Register, R14)**:保存函数返回地址 - **PC(Program Counter, R15)**:存储下一条指令地址 - **FP(Frame Pointer, R11)**:可选,指向当前基址(用于调试和回溯) #### 2. **函数调用过程(以函数A调用函数B为例)** **步骤1:调用准备(Caller)** - 参数传递:前4个参数通过$R0-R3$传递,多余参数压入 - 返回地址保存:执行`BL func_B`指令,当前PC+4自动存入$LR$ ```assembly MOV R0, #5 ; 参数1 → R0 MOV R1, #10 ; 参数2 → R1 BL func_B ; 调用func_B (LR = PC+4) ``` **步骤2:被调用函数开场(Callee Prologue)** - 保存上下文:将$LR$和需保留的寄存器 - 栈空间分配:调整$SP$为局部变量预留空间 ```assembly func_B: PUSH {R4-R6, LR} ; 保存寄存器 & 返回地址 SUB SP, SP, #8 ; 分配8字节局部变量空间 ``` **步骤3:函数执行** - 局部变量存储中(通过$SP$偏移访问) - 函数体执行期间可修改$R0-R3$ **步骤4:函数返回(Callee Epilogue)** - 释放栈空间:恢复$SP$到开场前位置 - 恢复上下文:弹出寄存器及返回地址到$PC$ ```assembly ADD SP, SP, #8 ; 释放局部变量空间 POP {R4-R6, PC} ; 恢复寄存器并直接返回 (PC ← LR) ``` #### 3. **结构示例 ``` 高地址 +-----------------+ | 调用者保存的寄存器 | ← 旧SP +-----------------+ | 参数5~n | (若有) +-----------------+ | 返回地址(PC) | ← 新FP (可选) +-----------------+ | 保存的LR | +-----------------+ | 保存的R4-R6 | +-----------------+ | 局部变量空间 | ← SP地址 ``` #### 4. **关键特性** 1. **增长方向**:ARM采用**满递减**(FD, Full Descending),向低地址增长 2. **寄存器保存规则**: - 调用者保存:$R0-R3$(参数/临寄存器) - 被调用者保存:$R4-R11$(需长期保留的寄存器) 3. **返回机制**:通过`POP {..., PC}`直接将返回地址载入$PC$,等效于`BX LR` #### 5. **异常处理差异** 在中断/异常场景下: - 处理器自动保存$PC$和$CPSR$到中 - 使用专用异常指针(如ARMv7的$SP_irq$) - 需手动保存其他寄存器[^1][^2] > 此过程遵循ARM AAPCS标准(ARM Architecture Procedure Call Standard),确保二进制兼容性[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值