1.发生中断时处理器的行为
不考虑其他细节,M3内核在发生中断时首先自动将如下8个寄存器压栈。因此在中断处理函数中,发生中断时正常执行时的寄存器数值已经被压入了堆栈中。在中断处理函数开始执行时,除了PC,LR,SP等控制寄存器,从r0-r12等这些通用寄存器的数据是没有变化的。下图描述了M3内核将寄存器压栈的顺序:
2、编译器通过栈来实现函数调用
C编译器通过栈来实现函数的调用,即在栈中记录程序执行的轨迹并辅助寄存器进行参数传递。具体如何实现C函数的调用,历史上有很多的规范,这些规范叫做调用惯例。对于ARM处理器来说,有一个官方的规范AAPCS(Procedure Call Standard for the
ARM® Architecture)详细描述了进行函数调用时如何进行参数的传递和调用路径的记录等。如下仅对使用栈记录调用路径的行为进行简单描述:
查看编译器生成的汇编代码可以得知,大多数的函数调用通过BL语句实现,BL语句将当前程序下一条指令的地址存入LR寄存器,并跳转到指定的地方(子函数开始的地方)开始执行。子函数中如果还需要调用孙子函数,就会在函数的入口处将LR的值压栈,以便函数执行结束后能够返回父函数。因此依次找到栈中LR的数值,就能找到调用路径中各个函数的地址。最后根据map文件翻译出各函数的名称,就可以得到函数的调用路径了。
如下是一个简单函数汇编代码的例子,函数OnPowerOff调用了函数FS_Deinit,函数FS_Deinit调用了SPIFFS_unmount。可以看出OnPowerOff函数的入口如将LR压入栈中(此时LR中保存的是函数OnPowerOff的返回地址,也就是调用OnPowerOff的父函数中的某条指令的地址),然后调用了FS_Deinit。同样FS_Deinit也在入口处将LR压入栈中(此时LR中保存的是OnPowerOff函数中POP指令的地址),然后再调用SPIFFS_unmount。返回的过程,依次将栈中保存的返回地址直接出栈到PC寄存器,完成函数的返回。这样,如果某个函数将栈中的返回地址写坏,则函数在返回时就会跳转到某个随机的地方,这就是常说的“程序跑飞了”。
1.通用寄存器
通用寄存器中可供挖掘的信息并不多,通常情况下r0-r3寄存器保存着函数的前四个参数(其余的参数在栈中保存),需要注意的是:这四个寄存器的数值仅在函数开始执行的时候是可靠的,在函数执行的过程中可能被改变。在函数返回时,寄存器r0和r1用于保存返回值(根据返回数据的大小,决定仅使用r0还是同时使用r0和r1)。同样这两个寄存器仅在子函数刚返回时数值才是可靠的。
2.特殊功能寄存器
特殊功能寄存器就是PC、LR和SP了。
SP指向当前的栈顶,在知晓栈的结构时,可以根据SP访问栈中的数据。
在中断处理函数中LR有特殊用法,其中保存了返回被中断地点的方法,而不是通常情况下的返回地址。因此在Hardfault处理函数中寄存器LR和PC的值没有太多参考意义,被处理器自动压栈的LR和PC最有用,PC记录了被中断打断前正在执行的指令地址(也是正在执行的函数地址),LR记录了被中断打断前,正在执行的函数的父函数的地址。根据这两个地址,可以找到引发Hardfault异常的函数和语句,以及其父函数(如果辅以汇编代码继续对栈的内容进行分析,则可以回溯整个调用路径)。
而具体引发Hardfault异常的原因,可以根据下面章节介绍的SCB寄存器来查看。
3.SCB寄存器
在M3/M4处理器标准外设中,有一个叫做SCB(System Control Block)的部分,其中有6个寄存器记录了发生Hardfault异常的原因。
CMSIS规范中对SCB寄存器的定义:
高亮的几个寄存器CFSR、HFSR、MMFAR、BFAR是我们需要关注的,AFSR是平台相关的暂时忽略。上述寄存器中CFSR又可以分为三个寄存器分别是:UFSR,BFSR,MFSR。上述寄存器的内存分布如下图所示:
解读SCB寄存器时应首先根据HFSR寄存器判断产生Hardfault的原因,如果确认是fault上访的情况,则依次检查BFSR、UFSR和HFSR确定具体的错误原因和地址。
附加一段Hardfault Handler中断响应函数的示例,该函数中打印所有通用寄存器,特殊功能寄存器和SCB寄存器,并对SCB寄存器的内容进行了基本的解读,配合上述文字,可以更精确的定位发生Hardfault的原因和位置。
Keil在生成程序的时候,可以生成两个辅助文件非常有帮助,他们分别是map文件和list文件。打开他们的方法如下图所示:
这两个文件我仅能大概读懂,下面介绍一下如何根据PC和LR寄存器中的地址数据,通过map文件找到该指令所在的函数,并根据list文件找到出错的代码行号。抛砖引玉,欢迎大牛拍砖:
map文件是连接器生成的二进制文件的信息,其中描述了各符号的交叉引用、函数和数据排布的顺序和大小等各种有用信息。要根据指令地址查找函数主要关注map文件中的Image Symbol Table这一部分内容。
随意取出其中的一部分,对于每行共有5列数据,每行代表一个符号(函数和全局变量)的数据。分别是:“符号名,符号地址,类型,长度,所在的.o文件”。我们主要关注“符号名,符号地址和长度”这部分内容。
根据地址查找函数名,就是检索MAP文件的“Image Symbol Table”部分,查找到函数地址和所查询地址最接近,且函数地址比所查询地址小的函数。并根据函数的长度确认是否在函数的范围内。
举一个真实的例子:
比如说在发生Hardfault错误时,使用上述Hardfault Handler示例代码打印出的错误信息如下:
错误类型为“非精确的数据访问违例”。处理器自动压入堆栈的PC寄存器的数据为0x00010642,在map文件中查找字符串“0x000106”(故意抹去末尾数据),找到的两个匹配数据是如下黄色表示的内容,但是这里两个地址都比出现错误的地址0x00010642大,错误并非发生在这两个函数中。
其中和0x00010642最为接近且小于0x00010642的为函数_uartInit,且函数地址0x000105e1+146大于0x00010642,,所以确认发生错误的指令在函数_uartInit中。
下面轮到List文件出场了,List文件实际上是编译器生成的C代码和汇编的对照表,其中_uartInit的部分内容如下:
出错的地址是0x00010642,相对于函数起始地址0x000105e1的偏移为0x61,根据List文件中的内容,_UartInit中第一条指令在文件中的偏移是0xb0,因此出错代码的偏移应该是0x61+0xb0 = 0x111。因此可以定位出发生错误的语句在uart7816.c中的157行。上图中黄色加亮部分所在的语句。
找到产生错误的语句后,在有针对性的查找CPU手册中串口寄存器WP7816B的部分,发现用错了头文件,的确是访问了非法的内存区域。至此,问题解决。