以Cortex-M3为例,RTThread在运行过程如果产生fault,会进人HardFault_Handler中断,RTThread对HardFault_Handler进行了重定义,HardFault_Handler函数在context_rvds.S文件中。函数内部做了一些工作对进入异常之前的处理器内部寄存器状态、工作模式、线程信息(如果异常是线程模式下产生)以及错误类型进行了输出。
HardFault_Handler大致工作流程入下:
HardFault_Handler PROC
; get current context
TST lr, #0x04 ;(1-1)
MRSNE r0, msp ;(1-2)
MRSEQ r0, psp ;(1-3)
STMFD r0!, {r4 - r11} ;(2)
STMFD r0!, {lr} ;(3)
MSRNE msp, r0 ;(1-4)
MSREQ psp, r0 ;(1-5)
PUSH {lr} ;(4)
BL rt_hw_hard_fault_exception;(5)
POP {lr} ;(6)
ORR lr, lr, #0x04 ;(7)
BX lr ;(8)
ENDP
ALIGN 4 ;(9)
END ;(10)
-
(1)(1-1)到(1-3)实现的操作可以概括为:当EXC_RETURN[2]是否为0判断进入异常之前使用的是msp还是psp,
然后将此sp的值保持到r0中。(怀疑RTT此处有bug ,正好与分析的逻辑是相反的,个人觉得MRSN应该对应psp, r0 ),下面分析下具体实现过程。
关于TST指令 : 执行按位与操作,直接结果更新到状态寄存标志位Z
这个指令通常与EQ、EN这些条件码来组合使用,必须注意测试后的结果全部位为0时候,标志位Z=1,此时EQ成立,反之Z= 0,NE成立。
关于EXC_RETURN:Cortex-M3权威指南中对EXC_RETURN[2]的描述如下
在进入异常服务程序后, LR的值被自动更新为特殊的EXC_RETURN, 当 EXC_RETURN[2]为0时候异常返回后从主堆栈做出栈操作(因为进入异常之前使用的是MSP),返回后使用MSP,当EXC_RETURN[2]为1时候异常返回后从进程堆栈做出栈操作(因为进入异常之前使用的是PSP),返回后使用PSP。
(1-1)是判断EXC_RETURN[2]是否为0 ,为EXC_RETURN[2]为0时候与操作以后的结果为0则标志位Z=1,此时EQ成立执行(1-3)和(1-5)指令;EXC_RETURN[2]为1时与操作以后的结果为1则标志位Z=0,此时EN执行(1-2)和(1-4)指令。
(1-2)更新msp到r0;
(1-3)更新psp到r0;
(1-4)更新r0到msp;
(1-5)更新r0到psp;
-
(2)把r4 - r11压入堆栈
关于STDF指令
格式: STDF Rn{!},{reglist}{^}
其中Rn为基础寄存器,装有传递数据的初始地址,Rn不可以为R15;后缀!表示最后 的地址写回到Rn中;寄存器列表reglist包含一个或多个寄存器范围,使用“,”分开,如{R1,R2,R6-R9},寄存器排列由小到大;“^”后缀不允许在用户模式呈系统模式下使用。操作后会将Rn地址开始的数据依次更新到R1,R2,R6-R9中,同时将Rn里面装的地址更新为Rn-4*7 。
如果Rn寄存器为SP 上面过程等价于push {reglist}
第(1)步的操作已经把SP加载到r0中了,此时R0中存放的就是当前堆栈顶地址。假设此时的SP地址为0x20005818,即r0 = 0x20005818;执行第(2)步操作后r0=r0-4*8 (r4 - r11一共8个寄存器)r0 = 0x200057F8;整个过程其实就是完成r4 - r11的压栈操作。
上面用先用r0进行压栈操作,然后在在把r0值传给sp,绕了一圈看似跟直接push操作效果无异。这样做其实有两个目的:
- 第一因为在异常状态下处理工作在handler模式,此时的SP=MSP,而上面操作的目的是将r4-r11压入进入异常之前系统所使用的堆栈里,如果进入异常前使用的也是MSP那么上诉操作可以用push指令完成,将r4-r11压入主堆栈里。但是如果进入异常之前系统使用的是进程栈即SP = PSP,此时我们需要对PSP进行操作因此这里就不能使用push了。
- 将r0指向了栈底,在下面rt_hw_hard_fault_exception函数调用中要作为函数的形参进行传递。
-
(3)此时的程序运行在异常模式下, lr的值被自动更新为特殊的EXC_RETURN,此处实际上是将EXC_RETURN的值压入了进入异常之前的堆栈
-
(4)在调用分支程序之前先lr压入当前堆栈,
-
(5)调用rt_hw_hard_fault_exception函数
经过第(1)到(3)步的操作之后,此时进入异常之前的堆栈已经被更新为如下内容
由于R0的值为更新之后的堆栈栈底地址,此时我们调用rt_hw_hard_fault_exception函数就可通过R0把栈底地址传入函数内部进行进行数据分析,来判断进入导致硬件flault的原因以程序及故障点的定位。
下面我们对rt_hw_hard_fault_exception函数进行分析函数原型如下:
void rt_hw_hard_fault_exception(struct exception_info * exception_info);
函数参数是exception_info
结构体类型指针,
struct exception_info
{
rt_uint32_t exc_return;
struct stack_frame stack_frame;
};
我们对exception_info结构体进行展开,展开后数据分布如下所示
{
rt_uint32_t exc_return;//小端模式低地址在前
{
rt_uint32_t r4;
rt_uint32_t r5;
rt_uint32_t r6;
rt_uint32_t r7;
rt_uint32_t r8;
rt_uint32_t r9;
rt_uint32_t r10;
rt_uint32_t r11;
{
rt_uint32_t r0;
rt_uint32_t r1;
rt_uint32_t r2;
rt_uint32_t r3;
rt_uint32_t r12;
rt_uint32_t lr;
rt_uint32_t pc;
rt_uint32_t psr;//高地址在后
}
}
}
由于处理器工作在小端模式下,RTThread对exception_info结构体的定义各个寄存器变量的内存分布与堆栈中寄存器的内存分布是一一对应的,这样很方便后续的指针操作。
/*
* fault exception handler
*/
void rt_hw_hard_fault_exception(struct exception_info * exception_info)
{
extern long list_thread(void);
/*定义context指针指向寄存器堆栈空间*/
struct stack_frame* context = &exception_info->stack_frame;
/*如果定义了外部异常处理钩子函数则执行定义的函数,否则进行后续处理*/
if (rt_exception_hook != RT_NULL)
{
rt_err_t result;
result = rt_exception_hook(exception_info);
if (result == RT_EOK)
return;
}
/*打印状态寄存器信息*/
rt_kprintf("psr: 0x%08x\n", context->exception_stack_frame.psr);
/*打印通用寄存器组信息*/
rt_kprintf("r00: 0x%08x\n", context->exception_stack_frame.r0);
rt_kprintf("r01: 0x%08x\n", context->exception_stack_frame.r1);
rt_kprintf("r02: 0x%08x\n", context->exception_stack_frame.r2);
rt_kprintf("r03: 0x%08x\n", context->exception_stack_frame.r3);
rt_kprintf("r04: 0x%08x\n", context->r4);
rt_kprintf("r05: 0x%08x\n", context->r5);
rt_kprintf("r06: 0x%08x\n", context->r6);
rt_kprintf("r07: 0x%08x\n", context->r7);
rt_kprintf("r08: 0x%08x\n", context->r8);
rt_kprintf("r09: 0x%08x\n", context->r9);
rt_kprintf("r10: 0x%08x\n", context->r10);
rt_kprintf("r11: 0x%08x\n", context->r11);
rt_kprintf("r12: 0x%08x\n", context->exception_stack_frame.r12);
rt_kprintf(" lr: 0x%08x\n", context->exception_stack_frame.lr);
rt_kprintf(" pc: 0x%08x\n", context->exception_stack_frame.pc);
/*如果EXC_RETURN[2] 为1则进入异常之前为线程模式*/
if(exception_info->exc_return & (1 << 2) )
{
rt_kprintf("hard fault on thread: %s\r\n\r\n", rt_thread_self()->name);
#ifdef RT_USING_FINSH
list_thread();/*打印线程信息*/
#endif /* RT_USING_FINSH */
}
else/*否则为handler模式*/
{
rt_kprintf("hard fault on handler\r\n\r\n");
}
#ifdef RT_USING_FINSH
hard_fault_track();/*错误跟踪*/
#endif /* RT_USING_FINSH */
while (1);
}
hard_fault_track();函数内部根据当前错误各个错误寄存器的状态对错误详细信息进行了进一步的解析,如感兴趣可自行研究。
-
(6)在调用分支程序之后先lr弹出当前堆栈。如果第5步调用的是c函数,可以省略(4),(6)操作,因为在进入函数之前和退出函数之后编译器会自动完成对lr的入栈和出栈操作。
-
(7)置位EXC_RETURN[2]使异常结束后进入线程模式,且使用PSP。
-
(8)异常返回。