要实现函数调用,有至少要有四步:
- 压栈,保护当前函数现场。
- 跳转,实现函数跳转。
a. 读参数
b. 运算、运行
c. 返回结果 - 跳转,返回父级函数。
- 出栈,恢复现场。
用实例,来对应感受一下。
对应的汇编
可见,套路既是:
5. 压栈,保护当前函数现场。 { PUSH 或者 LDR } sp寄存器,Rx寄存器
6. 跳转,实现函数跳转。{B、BL、BX},对应给PC寄存器赋值
a. 读参数 (出栈【从栈读取】、或者R0、R1)
b. 运算、运行 (ADD等)
c. 返回结果(压栈【从栈保存】,或者R0、R1)
7. 跳转,返回父级函数。{B、BL、BX ,但对应LR}, 对应给PC寄存器赋值
8. 出栈,恢复现场。{POP 或者LDR}, sp寄存器,Rx寄存器
【注意】:
9. 跳转前保存下一条指令地址至LR寄存器
不带返回的跳转指令B, 不会保存地址至LR寄存器
带返回的跳转指令BL,会自动保存地址至LR寄存器
10. 在子函数不会自动跳回父函数,需要手动将LR寄存器的值赋给PC寄存器才能跳转回来。
11. 有一个官方规范AAPCS(Procedure Call Standard for the ARM® Architecture),详细描述了进行函数调用时如何进行参数的传递和调用路径的记录等。
12. 大多数的函数调用通过BL语句实现,对应着LR寄存器 和 PC寄存器。因此依次找到栈中LR的数值,就能找到调用路径中各个函数的地址。最后根据map文件翻译出各函数的名称,就可以得到函数的调用路径了。【栈回溯】
13. 寄存器概述
- 通用寄存器
通用寄存器中可供挖掘的信息并不多,通常情况下r0-r3寄存器保存着函数的前四个参数(其余的参数在栈中保存),需要注意的是:这四个寄存器的数值仅在函数开始执行的时候是可靠的,在函数执行的过程中可能被改变。在函数返回时,寄存器r0和r1用于保存返回值(根据返回数据的大小,决定仅使用r0还是同时使用r0和r1)。同样这两个寄存器仅在子函数刚返回时数值才是可靠的。
- 特殊功能寄存器【PC、LR和SP】
SP指向当前的栈顶,在知晓栈的结构时,可以根据SP访问栈中的数据。
在中断处理函数中LR有特殊用法,其中保存了返回被中断地点的方法,而不是通常情况下的返回地址。因此在Hardfault处理函数中寄存器LR和PC的值没有太多参考意义,被处理器自动压栈的LR和PC最有用,PC记录了被中断打断前正在执行的指令地址(也是正在执行的函数地址),LR记录了被中断打断前,正在执行的函数的父函数的地址。根据这两个地址,可以找到引发Hardfault异常的函数和语句,以及其父函数(如果辅以汇编代码继续对栈的内容进行分析,则可以回溯整个调用路径)。
- SCB寄存器
在M3/M4处理器标准外设中,有一个叫做SCB(System Control Block)的部分,其中有6个寄存器记录了发生Hardfault异常的原因。
--此部分转载至 【作者:电工王大爷,链接:https://www.jianshu.com/p/e766c2fba1cc】
具体参考官方文档《HardFault的诊断》,或者电工王大爷的帖子。