查看: 1356|回复: 1

那个讨厌的HardFault,看你往哪里跑!

[复制链接]
  • TA的每日心情
    开心
    2024-3-26 15:16
  • 签到天数: 266 天

    [LV.8]以坛为家I

    3298

    主题

    6545

    帖子

    0

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    32003
    最后登录
    2024-4-9
    发表于 2021-4-22 08:55:54 | 显示全部楼层 |阅读模式
    那个讨厌的HardFault,看你往哪里跑!
    说到debug,相信大家伙最不想看到的就是程序偷偷的停掉,然后跳进一个叫做HardFault_Handler的函数中。虽说我们意志坚强、不惧难题,无论程序虐我千百遍,我待程序如爱恋;但是还是需要有效的方法来找到问题,解决问题。



    为了顺利定位问题的所在,常见的方法是要向工程中插入额外的代码和变量,这样一来,不仅可能增加代码尺寸,还可能会破坏程序的执行逻辑。
    而且,每次对于代码的修改都需要对工程进行重新编译,效率较低。
    如果说,遇到上述情况的时候,我们能够找来合适的帮手,一款强大的IDE来辅助调试,这缕东风还能送我们一程。当然,倘若我们手里只有一些命令行工具,比如GDB呢,,,,可能只能一声长叹了。
    不过,好在小编最近发现了一个叫做GNU gdb脚本的好东西(.gdbinit),迫不及待要来和大家分享。本篇小编将以调试HardFault为例进行说明,话不多说,开整。


    什么是.gdbinit?

    当GDB启动时,他会查找一个名为.gdbinit的文件;如果该文件存在,则GDB会执行该文件中的所有命令。


    通常,该文件用于简单的配置命令,例如读取/设置寄存器的值,控制GDB行为(设置/查看断点等)。其编写语法遵循如下格式:
    define<command>  // 定义的命令名
    <code>           // 对应的代码
    end
    document<command>// 命令帮助文档
    <helptext>       // 帮助文档内容
    end
    此定义的上半部分(在 define ... end 动词所界定的范围内)构成了在调用该命令时所执行的代码。
    此定义的下半部分(在 document ... end 所界定的范围内)由 GDB 命令解释器使用,用于在键入help <command> 时显示与<command> 命令关联的文本。
    举个简单的例子:
    define cls
    shell clear
    end
    document cls
    Clears the screen with a simple command.
    end


    这里我们定义了一个叫做cls的指令,document是对cls指令的解释,执行help cls后将显示我们所添加的说明信息:
    1.png
    此时,如果我们直接键入cls,当前控制台会被清空。
    征战HardFault
    下面,让我们开始HardFault之旅,搞点复杂的。


    先上一段.gdbinit里的脚本代码:
    1. define armex
    2.   printf "EXEC_RETURN (LR):\n",
    3.   info registers $lr
    4.     if (($lr & 0x4) == 0x4)
    5.       printf "Uses PSP 0x%x return.\n", $psp
    6.       set $armex_base = $psp
    7.     else
    8.       printf "Uses MSP 0x%x return.\n", $msp
    9.       set $armex_base = $msp
    10.     end
    11.   
    12.     printf "xPSR            0x%x\n", *($armex_base+28)
    13.     printf "ReturnAddress   0x%x\n", *($armex_base+24)
    14.     printf "LR (R14)        0x%x\n", *($armex_base+20)
    15.     printf "R12             0x%x\n", *($armex_base+16)
    16.     printf "R3              0x%x\n", *($armex_base+12)
    17.     printf "R2              0x%x\n", *($armex_base+8)
    18.     printf "R1              0x%x\n", *($armex_base+4)
    19.     printf "R0              0x%x\n", *($armex_base)
    20.     printf "Return instruction:\n"
    21.     x/i *($armex_base+24)
    22.     printf "LR instruction:\n"
    23.     x/i *($armex_base+20)
    24. end
    25.   
    26. document armex
    27. ARMv7 Exception entry behavior.
    28. xPSR, ReturnAddress, LR (R14), R12, R3, R2, R1, and R0
    29. end
    复制代码
    在这里,我们定义了一个叫做armex的指令,正如document中所述,这条指令会在进入arm exception时候被调用,并且打印出r0-r3, r12, r14, pc以及xPSR,而这也正是我们所需要参考的信息,比较重要的是r14与PC,他们能够告诉我们,是哪条指令触发的异常。这样,根据这个地址值,我们就可以找到对应的那条指令。


    那么,同样的功能,如果要在我们的工程中添加C代码,需要以下这些语句:
    1. void HardFault_HandlerC(unsigned long *stack_top){
    2.   volatile unsigned long stacked_r0 ;
    3.   volatile unsigned long stacked_r1 ;
    4.   volatile unsigned long stacked_r2 ;
    5.   volatile unsigned long stacked_r3 ;
    6.   volatile unsigned long stacked_r12 ;
    7.   volatile unsigned long stacked_lr ;
    8.   volatile unsigned long stacked_pc ;
    9.   volatile unsigned long stacked_psr ;

    10.   stacked_r0 = ((unsigned long)hardfault_args[0]) ;
    11.   stacked_r1 = ((unsigned long)hardfault_args[1]) ;
    12.   stacked_r2 = ((unsigned long)hardfault_args[2]) ;
    13.   stacked_r3 = ((unsigned long)hardfault_args[3]) ;
    14.   stacked_r12 = ((unsigned long)hardfault_args[4]) ;
    15.   stacked_lr = ((unsigned long)hardfault_args[5]) ;
    16.   stacked_pc = ((unsigned long)hardfault_args[6]) ;
    17.   stacked_psr = ((unsigned long)hardfault_args[7]) ;
    18.   
    19.   printf(“0x%x, 0x%x, 0x%x, 0x%x, 0x%x, 0x%x, 0x%x, 0x%x,”,
    20. stacked_r0, stacked_r1, stacked_r2, stacked_r3,
    21. stacked_r12, stacked_lr, stacked_pc, stacked_psr);
    22. }
    复制代码
    而这,仅仅是最精简的代码实现,多余的代码和变量的引入,导致代码尺寸以及系统栈空间使用量的增加,同时,破坏了原有代码的逻辑。


    相较于利用.gdbinit的方式,不够清爽。还有一个问题,相关寄存器的打印需要借助于printf函数,并且需要初始化并连接板子上的串口到PC上,无疑又多了一些操作。


    而利用.gdbinit中调用命令的方式,直接会打印在gdb的调试窗口,无需任何硬件资源初始化以及硬件连接。


    实际上,这是GDB通过芯片的调试接口,直接从芯片中读取数据,并由GDB直接输出结果的。


    实战演示


    为了这次测试,小编很不情愿的造了一个有BUG的工程,在main函数中定义一个对于空指针的赋值,并通过配置MPU对0x0开始的32B地址进行写保护,这样,对于0x0处地址的访问,会触发一个MemManager Fault,从而体现到HardFault:
    1. MPU->RBAR = ARM_MPU_RBAR(5, 0x00000000U);
    2. MPU->RASR = ARM_MPU_RASR(0, ARM_MPU_AP_RO, 0, 0, 1, 1, 0, ARM_MPU_REGION_SIZE_32B);

    3. int *p = NULL;
    4. *p = 0x1000;
    复制代码
    接下来,就是编译下载,在HardFault_Handler处设置断点,运行,此处略过1万+的操作流程,直接出结果,不出意料,程序停在了我们设置的断点处,尝试键入armex,令人兴奋的事情出现了,我们想要看到的东西被显示出来了:
    2.png


    根据ReturnAddress,0x788,我们就可以直接定位到那行捣乱的代码处:
    3.png


    这里所指向的是一条str指令,正是要将r2寄存器的值,即0x1000存储到指针p所指向的位置,而目标位置由于已经被MPU保护作为一块只读的区域,导致触发了一个MemManager Fault,进而触发为HardFault。


    至此,危害代码的小虫子就这样找到了,调试工作取得了又一重大进展,开启了一天的好心情了。


    总结


    小编今天为大家介绍了一种全新的调试思路,利用自定义GDB所提供的脚本文件来实现一些特定操作,自动触发。


    这样一来,我们可以做到一种非侵入式调试,所谓非侵入调试,我们无需添加任何的代码、变量到工程中,缩减镜像尺寸。
    同时,能够让我们的调试显得更加方便、灵活、可控,而且不必修改每个工程。最重要的是,我们可以更大限度发挥GDB的潜力,让他为我们所用。
    小编在这里仅仅是抛砖引玉,更多的功能要靠大家集思广益了。



    签到签到
    回复

    使用道具 举报

  • TA的每日心情

    4 小时前
  • 签到天数: 557 天

    [LV.9]以坛为家II

    34

    主题

    5913

    帖子

    2

    版主

    Rank: 7Rank: 7Rank: 7

    积分
    5680
    最后登录
    2024-4-19
    发表于 2021-4-22 09:10:21 | 显示全部楼层
    不明觉厉,蒙头转脑的学习
    哎...今天够累的,签到来了~
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 注册/登录

    本版积分规则

    关闭

    站长推荐上一条 /4 下一条

    Archiver|手机版|小黑屋|恩智浦技术社区

    GMT+8, 2024-4-19 14:55 , Processed in 0.119725 second(s), 21 queries , MemCache On.

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表