ARM Cortex M3/M4 异常处理和栈回溯

编程模型和寄存器

操作模式

两种访问等级:

  1. 特权访问等级:可以访问处理器中的所有资源
  2. 非特权访问等级:有些存储器区域是不能访问的,有些操作也是无法使用,非特权访问等级还可被称作“用户”状态。

两种操作状态

  1. 调试状态:处理器被暂停后进入,停止指令执行。如:调试器打断点。
  2. Thumb状态:执行代码的时候就是这个状态,M3/M4只支持 thumb 指令,不支持ARM指令,所以没有ARM状态。

两种模式:

  1. 处理模式:执行中断服务程序(ISR)等异常处理。在处理模式下,处理器总是具有特权访问等级
  2. 线程模式:在执行普通的应用程序代码时,处理器可以处于特权访问等级,也可以处于非特权访问等级。实际的访问等级由特殊寄存器CONTROL控制。
    操作状态和模式
    软件可以将处理从特权线程模式切换到非特权线程模式,但无法将自身从非特权切换到特权模式(操作受限),非要进行这种切换的话,处理器必须得借助异常机制才行。

寄存器

Cortex-M3和Cortex-M4 处理器的寄存器组中有16个寄存器,其中13个为32位通用目的寄存器,其他3个则有特殊用途,如下图。
在这里插入图片描述

通用目的寄存器 R0~R12

寄存器R0 ~ R12为通用目的寄存器,初始值是未定义的。

  1. 前8个(R0 ~ R7)也被称作低寄存器。由于指令中可用的空间有限,许多16位指令只能访问低寄存器。
  2. 高寄存器(R8 ~ R12)则可以用于32位指令和几个16位指令,如MOV。

R13,栈指针(SP)

R13为栈指针, 可通过PUSH和POP操作实现栈存储的访问。

物理上存在两个栈指针:

  1. 主栈指针(MSP)为默认的栈指针,在复位后或处理器处于处理模式时,其会被处理器选择使用。
  2. 另外一个栈指针名为进程栈指针(PSP),其只能用于线程模式。

栈指针的选择由特殊寄存器CONTROL决定,对于一般的程序,这两个寄存器只会有一个可见。

R14,链接寄存器(LR)

R14(LR),用于函数或子程序调用时返回地址的保存。函数结束时将LR加载到PC中实现返回调用程序处并继续执行。执行函数调用后,LR会自动更新。若需要调用另一个函数,需要先将LR保存到栈中,必满执行函数调用后,LR的当前值丢失。

在异常处理期间,LR会被自动更新为特殊的**EXC_RETURN(异常返回)**数值,之后该数值会在异常处理结束时触发异常返回。

R15,程序计数器(PC)

R15(PC),程序计数器,可读可写的。读操作返回当前指令地址加4(由于设计的流水线特性及同ARM7TDMI处理器兼容的需要)。写PC会引起跳转操作。

由于指令必须要对齐到半字或字地址,PC的最低位(LSB)为0。在使用一些跳转/读存储器指令更新PC时,需要将新PC值的LSB置1以表示Thumb状态。

特殊寄存器

除了寄存器组中的寄存器外,处理器中还存在多个特殊寄存器。这些寄存器表示处理器状态、定义了操作状态和中断/异常屏蔽。在使用C等高级编程语言开发简单的应用时,需要访问这些寄存器的情形不多。不过,在开发嵌入式OS或需要高级中断屏蔽特性时,就要访问它们。

在这里插入图片描述
这里只介绍控制寄存器

CONTROL寄存器

在这里插入图片描述
CONTROL寄存器主要作用有:

  1. 栈指针的选择,PSP or MSP
  2. 线程模式的访问等级的定义,特权 or 非特权

CONTROL寄存器只能在特权访问等级进行修改操作,而读取操作则在特权和非特权访问等级都可以。
在这里插入图片描述
复位后,CONTROL寄存器默认为0,处理器此时处于线程模式、具有特权访问权限以及使用主栈指针。通过写CONTROL寄存器,特权线程模式的程序可以切换栈指针的选择或进入非特权访问等级。nPRIV置位后,运行在线程模式的程序就不能访问CONTROL寄存器了。
在这里插入图片描述
进入异常处理后,可修改CONTROL寄存器,可实现栈指针切换和访问等级的修改:
在这里插入图片描述
MRS和MSR 指令可以读写control寄存器

MRS r0,CONTROL  ;将CONTROL寄存器读人RO
MSR CONTROL, r0 ;将RO写人CONTROL寄存器

浮点寄存器

在这里插入图片描述
S0 ~ S31(S)都为32位寄存器,而且每个都可以通过浮点指令访问,也可利用符号D0~D15(D代表双字/双精度)成对访问。例如,S0和S1成对组成D0,而S3和S2则成对组成D1。尽管Cortex-M4中的浮点单元不支持双精度浮点运算,在传输双精度数据时仍可使用浮点指令。

其他控制相关寄存器这里不再过多介绍

EXC_RETURN

处理器进入异常处理或中断服务程序(ISR)时,链接寄存器(LR)的数值会被更新为EXC_RETURN 数值。

EXC_RETURN中的一些位用于提供异常流程的其他信息,在异常返回时至关重要。
在这里插入图片描述

异常处理

基本知识

对于ARM架构,中断是异常的一种。异常是会改变程序流的事件。产生时,会停止当前任务转而执行异常处理程序。异常处理结束后,会继续正常程序执行。中断的异常处理也是中断服务函数
在这里插入图片描述
编号1~15 是异常,16以上位中断。除了NMI之外都可以被使能或禁止。

C实现的异常处理

C语言可以修改R1~ R3,R12,R14和PSR(程序状态寄存器,是否位负数、是否为0等),若需要修改R4~R11,就需要将他们保存到栈空间,然后函数结束前将他们再恢复

R0~R3、R12、LR以及PSR被称作调用者保存寄存器:调用函数(子函数)后还要使用这些寄存器,则应该在进行调用前将这些寄存器的内容保存到内存中(如栈)。若函数调用后不需要使用的寄存器数值则不用保存。换句话说就是,调用函数会修改这些寄存器,调用这需要进行保存。

R4~R11为被调用者保存寄存器:被调用的子程序或函数需要确保这些寄存器在函数结束时不会发生变化(与进入函数时的数值一样)。就是需要在函数退出前将它们恢复为初始值,如果需要修改这些寄存器,需要先将他们保存,函数退出前进行恢复。

对于浮点寄存器,也有这样的分类:

  1. S0~S15为“调用者保存寄存器”​。
  2. S16~S31为“被调用者保存寄存器”​。

一般来说,函数调用将R0~R3作为输入参数,R0则用作返回结果。若返回值为64位,则R1也会用于返回结果。

详情可参考 AAPSC CHA.6(Procedure Call Standard for the Arm®Architecture)

在这里插入图片描述

栈帧

要使C函数可以用作异常处理,异常机制需要在异常入口处自动保存R0~R3、R12、LR以及PSR,并在异常退出时将它们恢复,这些都要由处理器硬件控制。另外,与普通的C函数调用不同,返回地址(PC)的数值并没有存储在LR中(异常机制在进入异常时将EXC_RETURN代码放入了LR中,该数值将会在异常返回时用到),因此,异常流程也需要将返回地址(PC)保存。

在异常入口处被压入栈空间的数据块为栈帧。对于Cortex-M3或不具有浮点单元的Cortex-M4处理器,栈帧都是8个字大小的。对于具有浮点单元的Cortex-M4,栈帧则可能是8或26个字。

AAPCS 的另外一个要求为,栈指针的数值在函数入口和出口处应该是双字对齐的。因此,若在中断产生时栈帧未对齐到双字地址上,Cortex-M3和Cortex-M4处理器会自动插入一个字。这样,可以保证栈指针位于异常处理的开始处。​“双字栈对齐”特性是可编程的,若异常未完全符合AAPCS,则可以将该特性关闭(可以通过系统控制块(SCB)中配置控制寄存器(CCR,地址0xE000ED14)的一个控制位来使能双字栈对齐特性)。
在这里插入图片描述
若使能了双字栈对齐特性,而且栈指针的数值未对齐到双字边界上,栈中会被插入一段空间,栈指针也会被强制对齐到双字地址,而且压栈的xPSR的第9位被置为1 (退出异常后,根据这个bit判断是否需要跳转SP,去掉空位)
在这里插入图片描述
对于具有浮点单元的Cortex-M4,若浮点单元已使能且使用,栈帧还会包括浮点单元寄存器组中的S0~S15
在这里插入图片描述
通用目的寄存器R0~R3的数值位于栈帧底部,可以很方便地由SP相关寻址访问。有些情况下,这些压栈的寄存器可用于往软件触发中断或SVC服务传递信息。

EXC_RETURN

见上文寄存器章节介绍

异常流程

异常进入和压栈

在这里插入图片描述
当异常产生且被处理器接收时,压栈流程会将寄存器压入栈中并组织栈帧,见上图。

Cortex M3/M4具有多个总线接口,处理器在压栈操作(系统总线)的同时,还可以开始取向量(通常I-CODE总线)和取指,二者可以同时操作。

若处理器运行在线程模式且使用MSP(CONTROL寄存器的第0位为0,默认配置),则压栈操作在执行时使用主栈MSP。

若处理器运行在线程模式且使用进程栈(CONTROL寄存器的第1位为1),则压栈操作执行时使用进程栈PSP。在进入处理模式后,处理器必须使用MSP,所有嵌套中断的压栈操作执行时都使用主栈MSP。
在这里插入图片描述

异常返回和出栈

在异常处理结束时,异常入口处生成的EXC_RETURN数值的第2位用于确定提取栈帧时所用的栈指针。

若第2位为0,则处理器会知道之前压栈时使用的是主栈。
在这里插入图片描述
若第2位为1,则表示使用进程栈。
在这里插入图片描述
在每次出栈操作结束时,处理器还会检查出栈xPSR数值的第9位,并且若压栈时插入了额外的空间则会将其去除。
在这里插入图片描述
为了降低出栈所需的时间,处理器会首先取出返回地址(压栈的PC),因此取指可以和剩下的出栈操作同时进行

栈回溯

在实际开发过程中,不是所有的时候都可以连接仿真器做调试,当发生错误异常(HARDFAULT异常)后,需要在没有仿真器的环境下快速找到异常发生位置并获得函数调用过程。

上文对异常处理相关基础知识做了简单介绍,可知异常发生后,会将异常发生时重要寄存器入栈,我们可以根据这些信息完成函数调用过程的还原,即栈回溯。

本文使用trace32 simulator进行相关展示

函数调用过程

在这里插入图片描述
函数调用过程为:

main
func2
func1

根据寄存器进行分析

当前PC地址为0x1054,对应 func1
在这里插入图片描述
LR(R14)地址为0x1088,对应 func2,0x1084(0x1088-4)为BL指令,跳转到func1
在这里插入图片描述
PS(R13)地址为0xFE4,func2 中开始时,将LR和R3压入栈中,因此R13+4即可获得func2函数的返回地址
在这里插入图片描述

*(uint32_t *)(FE4+4) = 0x2034// 0x2034 在main函数中, 0x2030 为BL指令

寄存器分析结果和实际调用结果相符

0x2034
0x1088
0x1054
main
func2
func1

手动完成栈回溯

有些时候,PC和LR无法正确保存(如,使能MPU保护栈顶后,栈溢出会触发异常但是栈帧无法被压入栈中),此时trace32无法自动完成对调用栈的推到,因此手动从栈中找到曾经入栈的LR就成了栈回溯的关键。

几个大原则:

  1. BL 完成函数跳转,会将返回地址保存到LR中,返回地址位BL指令地址+4
  2. 跳转到新函数后,如果还会继续调用下一个函数,则会在该函数开始时将LR压入栈中
  3. 入栈的LR地址在代码段范围,同时 地址-4后为BL指令

手动栈回溯流程:

  1. 找到栈中第一个在代码段范围的地址(下图中为0x2034),同时满足该地址-4为BL指令(下图为0x2030)
  2. 将该地址设置成LR(下图设置LR为0x2034)
  3. 设置SP为LR所在栈地址+4的位置(下图设置PS为0x0FEC)
  4. 设置PC BL指令跳转的目标地址(下图中,0x2030指令为BL 0x1064,设置PC为0x1064)

到此,大概率能在trace32中将调用栈回溯

在这里插入图片描述
由上图可看出,还原结果如下,func1的调用因为PC和LR丢失,无法被还原

main
func2
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值