rt-thread 中对 hardfault 的处理

1. 背景

​ 当系统进入异常时,通常会进入异常对应的处理函数,当系统中存在非法操作时,比如除0、非对齐访问、爆栈,此时便会触发 hardfault 异常,一般情况下,hardfault 中是个 while(1),此时系统中存在非法操作,就会被这个 while(1) 永远挂着,但是此时需要查找为什么系统崩了,怎么查?从何处查?通常进入到 hardfault 后调用栈也没了,新手玩家会很头疼,不知从何处查起。因此,系统宕机时,必须拥有足够的信息帮助我们分析宕机原因

2. 基础知识

  • cm 架构,响应异常的第一个行动,就是自动保存现场的必要寄存器信息,依次会把 psp、pc、lr、r12、r3-r0 由硬件自动压入适当的栈中
  • cm 架构是双栈指针设计,当上下文环境是线程环境时,栈指针是psp,当上下文是中断环境时,栈指针为 msp,双栈指针设计天生为 os 而来,这样保证了线程环境与中断环境隔离
  • cm 架构的栈是向下生长的
  • 任意时刻,仅存在一个 sp 指针,要么是 psp,要么是 msp

3. rt-thread 构造栈帧

  1. 在 rt-thread 中每个线程拥有独立的栈空间,栈空间可以是静态的,也可以是动态申请的,栈就是一片连续的内存。

    struct exception_stack_frame
    {
        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;
    };
    
    struct stack_frame // 架构不同,栈帧的大小也不同
    {
        /* r4 ~ r11 register */
        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;
    
        struct exception_stack_frame exception_stack_frame;
    };
    
    /* 初始化线程栈,构造栈帧 */
    rt_uint8_t *rt_hw_stack_init(void       *tentry,
                                 void       *parameter,
                                 rt_uint8_t *stack_addr,
                                 void       *texit)
    {
        struct stack_frame *stack_frame;
        rt_uint8_t         *stk;
        unsigned long       i;
    
        stk  = stack_addr + sizeof(rt_uint32_t);// 栈顶,连续空间的最高地址
        stk  = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);
        stk -= sizeof(struct stack_frame);// 向下偏移 sizeof(struct stack_frame) 个大小
    
        stack_frame = (struct stack_frame *)stk;
    
        /* init all register */
        for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)
        {
            ((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;
        }
    
        stack_frame->exception_stack_frame.r0  = (unsigned long)parameter; /* r0 : argument */
        stack_frame->exception_stack_frame.r1  = 0;                        /* r1 */
        stack_frame->exception_stack_frame.r2  = 0;                        /* r2 */
        stack_frame->exception_stack_frame.r3  = 0;                        /* r3 */
        stack_frame->exception_stack_frame.r12 = 0;                        /* r12 */
        stack_frame->exception_stack_frame.lr  = (unsigned long)texit;     /* lr 线程退出时,将执行线程*/
        stack_frame->exception_stack_frame.pc  = (unsigned long)tentry;    /* entry point, pc,线程切换时,进入到用户线程函数 */
        stack_frame->exception_stack_frame.psr = 0x01000000L;              /* PSR */
    
        /* return task's current stack address */
        return stk;
    }
    
  2. 为什么要构造一个栈帧?

    一个线程初始化完成后,怎样才能进入到线程处理函数中?是由调度器来决定的,调度器选择切换到当前时刻已经就绪的最高优先级的线程,便会手动触发一个 Pendsv 异常,进入 Pendsv 异常时,会将被切线程的上下文保存,将要执行的线程的上下文恢复到寄存器中。通过反推法,如果没有上述构造的初始栈上下文,怎么恢复到寄存器中呢?所以,构造出来一个栈帧后,将上述栈帧中的内容出栈到寄存器中,pc 寄存器中被上述恢复动作赋值成 tentry 这个函数指针,此时将要执行的线程的栈指针 sp(指向栈顶,由于该程线程首次执行,又执行了 push 操作) 更新给 psp,当异常结束返回时,就跳到了线程的处理函数中。如果线程返回了,该线程将永远不会被得到执行,因此,还需给线程收尸,也就是将该线程占用的资源给释放掉,执行构造栈帧时的 texit 函数,此函数地址在 Pendsv 异常处理函数中 push 到了 lr 寄存器中,当函数返回时,必然执行该函数。收尸动作,发生在 idle 线程中,系统空闲时,将这些僵尸线程给释放掉。

  3. rt-thread 切换线程细节

    rt-thread 切换线程时,进入PendSv 异常前,psp 此时是被切线程的栈顶,此时硬件自动压入 8 个寄存器的值,还需手动将剩余寄存器压入此线程对应的栈中,由于该线程需要让出处理器的使用权,因此需要将该线程的上下文环境给保存在栈中。

    还有一个关键的步骤,被切线程的上下文已经全部保存在栈中了,此时线程控制块中的 sp 并没有更新,切线程时是根据线程控制块中的 sp 来切的,因此,还需将 psp 的值更新回线程控制块中的 sp 。此时该线程的上下文被保存在栈中了,同时该栈的栈顶也被保存在了线程控制块中,下次恢复上下文时,就有能力恢复了。

4. rt-thread 中 HardFault_Handler

  1. rt-thread 将 HardFault_Handler 重写了,删除掉了毫无意义的 while(1),加入了更多的信息

        IMPORT rt_hw_hard_fault_exception
        EXPORT HardFault_Handler
    HardFault_Handler    PROC
    
        ; get current context
        TST     lr, #0x04               ; if(!EXC_RETURN[2])
        ITE     EQ
        MRSEQ   r0, msp                 ; [2]=0 ==> Z=1, get fault context from handler.
        MRSNE   r0, psp                 ; [2]=1 ==> Z=0, get fault context from thread.
    
        STMFD   r0!, {r4 - r11}         ; push r4 - r11 register
        STMFD   r0!, {lr}               ; push exec_return register # 压入lr的值来判断异常发生在线程中还是在中断中
    
        TST     lr, #0x04               ; if(!EXC_RETURN[2])
        ITE     EQ
        MSREQ   msp, r0                 ; [2]=0 ==> Z=1, update stack pointer to MSP.
        MSRNE   psp, r0                 ; [2]=1 ==> Z=0, update stack pointer to PSP.
    
        PUSH    {lr}
        BL      rt_hw_hard_fault_exception
        POP     {lr}
    
        ORR     lr, lr, #0x04
        BX      lr
        ENDP
    
        ALIGN   4
    
        END
    
  2. 进入异常前,硬件自动压入一些寄存器到栈中,此时的 lr 寄存器中的值代表着进入异常前的环境(线程或中断),这个环境决定着栈指针是 psp 还是 msp。因此,判断这个 lr 寄存器的 bit2 是 0 还是 1 来获取进入异常前使用的是那个栈。然后将不会被硬件压栈的寄存器给压入相应栈(psp 或 msp)中,然后将该栈的栈顶更新到 r0 寄存器中,此时跳入 C 函数中打印相关信息,入参为栈顶地址

    /* 成员位置严格按照压栈顺序摆放 */
    struct exception_info
    {
        rt_uint32_t exc_return; // 进入异常前的环境
        struct stack_frame stack_frame; // 构造的栈帧数据结构
    };
    
    void rt_hw_hard_fault_exception(struct exception_info * exception_info)
    {
    #if defined(RT_USING_FINSH) && defined(MSH_USING_BUILT_IN_COMMANDS)
        extern long list_thread(void);
    #endif
        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);
    
        if(exception_info->exc_return & (1 << 2) ) // bit2 为 1,表示线程环境
        {
            rt_kprintf("hard fault on thread: %s\r\n\r\n", rt_thread_self()->name);
    
    #if defined(RT_USING_FINSH) && defined(MSH_USING_BUILT_IN_COMMANDS)
            list_thread();
    #endif
        }
        else
        {
            rt_kprintf("hard fault on handler\r\n\r\n");
        }
    
    #ifdef RT_USING_FINSH
        hard_fault_track();
    #endif /* RT_USING_FINSH */
    
        while (1);
    }
    
  3. 根据上述打印信息结合相应反汇编,就能够找到 hardfault 前正在执行的那一句代码,同时,函数发生调用时,返回地址也被压入到相应栈中,因此,根据这些基础信息,还有一定的处理空间,可通过大神写的cmbacktrace软件包进行自动化分析函数调用栈。

  4. 掌握系统宕机时的处理思路才是关键点,需要对 cm 架构的处理器有一定的理解

5. 手动分析

​ 首先根据进入异常前的环境,确定是那个 sp,然后将该 sp 的值复制到内存查找窗口,查找 sp 指向的内存中的 pc 值(由于硬件会自动压栈),然后在反汇编中根据这个 pc 的值就能够找到发生异常前执行的汇编指令请添加图片描述
​ 为什么上图中红色下划线处是 pc 呢?需要根据硬件自动压栈的寄存器的数量以及顺序确定,不同处理器不同

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
RT-Thread诞生于2006年,是一款以开源、立、社区化发展起来的物联网操作系统。 RT-Thread主要采用 C 语言编写,浅显易懂,且具有方便移植的特性(可快速移植到多种主流 MCU 及模组芯片上)。RT-Thread把面向对象的设计方法应用到实时系统设计,使得代码风格优雅、架构清晰、系统模块化并且可裁剪性非常好。 RT-Thread有完整版和Nano版,对于资源受限的微控制器(MCU)系统,可通过简单易用的工具,裁剪出仅需要 3KB Flash、1.2KB RAM 内存资源的 NANO 内核版本;而相对资源丰富的物联网设备,可使用RT-Thread完整版,通过在线的软件包管理工具,配合系统配置工具实现直观快速的模块化裁剪,并且可以无缝地导入丰富的软件功能包,实现类似 Android 的图形界面及触摸滑动效果、智能语音交互效果等复杂功能。 RT-Thread架构 RT-Thread是一个集实时操作系统(RTOS)内核、间件组件的物联网操作系统,架构如下: 内核层:RT-Thread内核,是 RT-Thread的核心部分,包括了内核系统对象的实现,例如多线程及其调度、信号量、邮箱、消息队列、内存管理、定时器等;libcpu/BSP(芯片移植相关文件 / 板级支持包)与硬件密切相关,由外设驱动和 CPU 移植构成。 组件与服务层:组件是基于 RT-Thread内核之上的上层软件,例如虚拟文件系统、FinSH命令行界面、网络框架、设备框架等。采用模块化设计,做到组件内部高内聚,组件之间低耦合。 RT-Thread软件包:运行于 RT-Thread物联网操作系统平台上,面向不同应用领域的通用软件组件,由描述信息、源代码或库文件组成。RT-Thread提供了开放的软件包平台,这里存放了官方提供或开发者提供的软件包,该平台为开发者提供了众多可重用软件包的选择,这也是 RT-Thread生态的重要组成部分。软件包生态对于一个操作系统的选择至关重要,因为这些软件包具有很强的可重用性,模块化程度很高,极大的方便应用开发者在最短时间内,打造出自己想要的系统。RT-Thread已经支持的软件包数量已经达到 180+。 RT-Thread的特点: 资源占用极低,超低功耗设计,最小内核(Nano版本)仅需1.2KB RAM,3KB Flash。 组件丰富,繁荣发展的软件包生态 。 简单易用 ,优雅的代码风格,易于阅读、掌握。 高度可伸缩,优质的可伸缩的软件架构,松耦合,模块化,易于裁剪和扩展。 强大,支持高性能应用。 跨平台、芯片支持广泛。
RT-Thread实时操作系统,邮箱(Mailbox)是一种线程间通信的机制,用于在不同线程之间传递消息或数据。RT-Thread的邮箱提供了一种同步的方式,使得一个线程可以向另一个线程发送消息,并等待接收方处理完毕后再继续执行。 RT-Thread的邮箱通过结构体`struct rt_mailbox`来表示,其定义位于`rtdef.h`头文件。下面是一个简单的示例代码,展示了如何使用RT-Thread的邮箱: ```c #include <rtthread.h> #define MAILBOX_SIZE 10 static struct rt_mailbox mailbox; void mailbox_sender_entry(void *parameter) { rt_uint32_t data = 123; while (1) { rt_mb_send(&mailbox, data); rt_kprintf("Sender: Sent data %d\n", data); data++; rt_thread_mdelay(1000); } } void mailbox_receiver_entry(void *parameter) { rt_uint32_t data; while (1) { if (rt_mb_recv(&mailbox, &data, RT_WAITING_FOREVER) == RT_EOK) { rt_kprintf("Receiver: Received data %d\n", data); } } } int mailbox_example(void) { rt_err_t result; result = rt_mb_init(&mailbox, "mailbox", NULL, MAILBOX_SIZE, RT_IPC_FLAG_FIFO); if (result != RT_EOK) { rt_kprintf("Failed to initialize mailbox: %d\n", result); return -1; } rt_thread_t sender_thread = rt_thread_create("sender", mailbox_sender_entry, RT_NULL, 1024, 25, 10); if (sender_thread != RT_NULL) { rt_thread_startup(sender_thread); } rt_thread_t receiver_thread = rt_thread_create("receiver", mailbox_receiver_entry, RT_NULL, 1024, 20, 10); if (receiver_thread != RT_NULL) { rt_thread_startup(receiver_thread); } return 0; } ``` 在上面的示例,我们创建了一个大小为10的邮箱,然后创建了一个发送者线程和一个接收者线程。发送者线程会周期性地向邮箱发送一个递增的数据,接收者线程会从邮箱接收数据并打印出来。 请注意,以上示例仅为演示目的,实际使用时需要根据具体需求进行适当的修改和扩展。更详细的关于RT-Thread邮箱的使用方法和API可以参考RT-Thread官方文档。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值