在ARM Cortex-M4中调试硬件故障。

12

我在尝试排查EFR32BG12处理器上的一个硬错误,但是已经束手无策了。我一直在按照Silicon Labs知识库中的指示进行操作:

https://www.silabs.com/community/mcu/32-bit/knowledge-base.entry.html/2014/05/26/debug_a_hardfault-78gc

我还参考了Keil的应用笔记,以填补一些细节方面的内容:

http://www.keil.com/appnotes/files/apnt209.pdf

我已成功地使硬错误在一个特定位置频繁发生。当硬错误发生时,处理器将以下值(由处理器在调用硬错误处理程序之前推送到堆栈中的代码)给我:

Name     Type        Value               Location
~~~~     ~~~~        ~~~~~               ~~~~~~~~
cfsr     uint32_t    0x20000 (Hex)       0x2000078c    
hfsr     uint32_t    0x40000000 (Hex)    0x20000788    
mmfar    uint32_t    0xe000ed34 (Hex)    0x20000784    
bfar     uint32_t    0xe000ed38 (Hex)    0x20000780    
r0       uint32_t    0x0 (Hex)           0x2000077c    
r1       uint32_t    0x8 (Hex)           0x20000778    
r2       uint32_t    0x0 (Hex)           0x20000774    
r3       uint32_t    0x0 (Hex)           0x20000770    
r12      uint32_t    0x1 (Hex)           0x2000076c    
lr       uint32_t    0xab61 (Hex)        0x20000768    
pc       uint32_t    0x38dc8 (Hex)       0x20000764    
psr      uint32_t    0x0 (Hex)           0x20000760  

根据Keil的应用笔记,我认为CFSR值为0x20000表示使用错误,并设置了INVSTATE位,即:

INVSTATE: 无效状态:0=无无效状态,1=处理器尝试执行一个使用执行程序状态寄存器(EPSR)非法的指令。当此位被设置时,为异常返回推送的PC值指向尝试非法使用EPSR的指令。可能原因:a)将分支目标地址加载到LSB=0的PC上。b)在异常或中断处理期间堆栈化的PSR损坏。c)向量表包含一个LSB=0的向量地址。

异常推送到堆栈上的PC值(由知识库文章提供的代码提供)似乎是0x38dc8。 如果我在Simplicity Studio“反汇编”窗口中转到此地址,则会看到以下内容:

00038db8:   str     r5,[r5,#0x14]
00038dba:   str     r0,[r7,r1]
00038dbc:   str     r4,[r5,#0x14]
00038dbe:   ldr     r4,[pc,#0x1e4] ; 0x38fa0
00038dc0:   strb    r1,[r4,#0x11]
00038dc2:   ldr     r5,[r4,#0x64]
00038dc4:   ldrb    r3,[r4,#0x5]
00038dc6:   movs    r3,r6
00038dc8:   strb    r1,[r4,#0x15]
00038dca:   ldr     r4,[r4,#0x14]
00038dcc:   cmp     r7,#0x6f
00038dce:   cmp     r6,#0x30
00038dd0:   str     r7,[r6,#0x14]
00038dd2:   lsls    r6,r6,#1
00038dd4:   movs    r5,r0
00038dd6:   movs    r0,r0

这个地址似乎远远超出了我的代码范围。如果我在“内存”窗口中查看同一个地址,我能看到如下内容:

0x00038DC8  69647561 2E302F6F 00766177 00000005  audio/0.wav.....
0x00038DD8  00000000 000F4240 00000105 00000000  ....@B..........
0x00038DE8  00000000 00000000 00000005 00000000  ................
0x00038DF8  0001C200 00000500 00001000 00000000  .Â..............
0x00038E08  00000000 F00000F0 02F00001 0003F000  ....ð..ð..ð..ð..
0x00038E18  F00004F0 06010005 01020101 01011201  ð..ð............
0x00038E28  35010121 01010D01 6C363025 2E6E6775  !..5....%06lugn.
0x00038E38  00746164 00000001 000008D0 00038400  dat.....Ð.......

有趣的是,“audio/0.wav”是固件中的一个静态字符串。如果我理解正确,我在这里学到的是PC不知何故被设置为内存中的这个位置,这显然不是有效指令,导致了硬错误。

为了调试这个问题,我需要知道PC如何被设置为这个错误的值。我相信LR寄存器应该能给我一个想法。异常推送到堆栈上的LR寄存器似乎是0xab61。如果我在这个位置查看,我在反汇编窗口中看到以下内容:

1270            dp->sect = clst2sect(fs, clst);
0000ab58:   ldr     r0,[r7,#0x10]
0000ab5a:   ldr     r1,[r7,#0x14]
0000ab5c:   bl      0x00009904
0000ab60:   mov     r2,r0
0000ab62:   ldr     r3,[r7,#0x4]
0000ab64:   str     r2,[r3,#0x18]

在我看来,问题似乎特别出现在这个调用过程中:

0000ab5c:   bl      0x00009904

这让我想到问题是由于一个损坏的堆栈导致的,它使得clst2sect返回到内存中的无效部分而不是0xab60。clst2sect的代码非常无害:

/*-----------------------------------------------------------------------*/
/* Get physical sector number from cluster number                        */
/*-----------------------------------------------------------------------*/

DWORD clst2sect (   /* !=0:Sector number, 0:Failed (invalid cluster#) */
    FATFS* fs,      /* Filesystem object */
    DWORD clst      /* Cluster# to be converted */
)
{
    clst -= 2;      /* Cluster number is origin from 2 */
    if (clst >= fs->n_fatent - 2) return 0;     /* Is it invalid cluster number? */
    return fs->database + fs->csize * clst;     /* Start sector number of the cluster */
}

这个分析听起来大概是正确的吗?

我遇到的问题是我不知道可能会引起这种行为的原因…… 我已经在所有中断处理程序中设置了断点,以查看它们中的一个是否可能破坏堆栈,但似乎没有任何模式 - 有时候,没有调用任何中断处理程序,但问题仍然会发生。

即便如此,我很难看出程序如何尝试执行远超过实际代码结尾位置的代码... 我觉得函数指针可能是一个可能的候选项,但如果是那样,我期望看到问题出现在使用函数指针的地方。 但是,在错误发生的附近,我没有看到任何使用函数指针的地方。

也许有更多的信息可以从上面给出的调试信息中提取?问题是非常可重现的,因此,如果有我尚未尝试过但您认为可能会提供一些见解的内容,我很想听听您的建议。

感谢您提供的任何帮助!


1
这个返回可能是对PC的弹出吗?直到找到修改LR的东西,它可能是导致返回值损坏的那个。 - Sean Houlihane
我找到了一种在0x38dc8处添加断点的方法,但似乎异常在断点停止执行之前就被调用了——断点从未被触发。 - Michael Cooper
感谢解释。所以“任务”实际上是运行到完成,对吗?它们不能在中途停止并稍后恢复,因此整个系统使用单个堆栈?我问的原因是,当我看到像这样的错误时,大约80%的时间它们是由于任务堆栈不够大而导致交叉堆栈损坏。如果只有一个堆栈,则无法选择此选项。 - cooperised
顺便问一下,如果假设只有一个堆栈,我可以假设您已经检查过它的大小并且没有溢出吗? - cooperised
1
如果您尝试过将堆栈大小加倍,但没有帮助,那么这 可能 不是问题所在。检查溢出堆栈的最可靠方法是配置MPU,使分配的堆栈空间底部不可访问; 溢出堆栈将触发数据异常并从那里开始排查。一种更简单但不太可靠的方法是尝试确定堆栈使用量最大的位置,并设置断点以便您可以查看 r13 - cooperised
显示剩余12条评论
1个回答

16

经过大约一个月的追踪,我成功地找到了问题的根源。我希望在这里提供足够的信息,使得这对他人有用。

最终,问题是由于将指向非静态局部变量的指针传递给状态机,并在稍后更改了该内存位置的值所导致的。因为局部变量不再处于作用域内,那个内存位置是堆栈中的一个随机点,而更改那里的值会破坏堆栈。

这个问题很难追踪,原因如下:

  1. 根据代码编译的方式不同,更改的内存位置可能是一些不重要的东西,比如另一个局部变量,这会导致更加微妙的错误。只有当我运气好时,更改才会影响PC寄存器并引起硬故障。

  2. 即使我找到了一个始终生成硬故障的代码版本,实际的硬故障通常发生在调用堆栈的某个地方,当函数返回并将堆栈值弹出到PC时。这使得很难确定问题的原因--我所知道的只是在该函数返回之前某个地方堆栈被破坏了。

在找到问题的根源方面,一些工具非常有帮助:

  1. 一开始我通过GPIO引脚确定了一个代码块,通常在这个代码块中会发生硬件错误。进入该块之前,我会将一个引脚置高,并在退出该块时将其置低。然后进行了多次测试,检查硬件错误发生时该引脚是高电平还是低电平,并使用一种类似二分查找的方式确定了包含所有硬件错误的最小代码块。

  2. 硬件错误会推送一些重要寄存器到堆栈上。这些寄存器帮助我确认了PC寄存器何时变得损坏,并帮助我理解它是由于堆栈损坏导致的。

  3. 从那个代码块之前开始,同时关注本地变量并向前步进,我能够确定一个函数调用正在破坏堆栈。我可以使用Simplicity Studio的内存视图来确认这一点。

  4. 最后,详细地步进有问题的函数时,我意识到当我取消引用存储的指针并写入该内存位置时,问题就会出现。回顾设置该指针值的位置时,我意识到它已被设置为指向一个非静态的本地变量,而该变量现在已超出作用域。

感谢@SeanHoulihane和@cooperised,他们帮助我消除了一些可能的原因,并给予了我在调试工具方面更多的信心。


现在提到这个有点晚了,但是ETM跟踪将是调试这个问题的“理想”工具。不幸的是,除非您可以以约20 MHz的速度运行,否则工具并不便宜。 - Sean Houlihane
1
那看起来很有趣。我正在以38.4 MHz的速度运行,但可能需要降低一些以进行调试。是否有特定的工具我应该了解一下? - Michael Cooper
请查看 https://github.com/orbcode/orbuculum 获取开源工具和FPGA低速捕获。如果时间就是金钱,那么使用ULink-Pro吧。我相信Keil的评估版本支持跟踪,但我从未在仿真之外使用过跟踪... - Sean Houlihane

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接