使用DeepSeek辅助定位/解决栈溢出导致的Hardfault

本文由群友 黄苇鳽 撰稿于2025年8月11日。DualMono代为投稿。

使用DeepSeek辅助定位/解决栈溢出导致的Hardfault

起因

最近在为STM32L051K8U6(ARM Cortex-M0+ ARMv7)单片机移植开源Wouo GUI框架后,发现每次与spinbox控件交互时,程序都会立即卡死并且进入hardfault_Handler。


1

一、定位触发Hardfault的代码位置

找出产生问题的C语言函数
首先进入MDK的Debug模式进行在线调试,通过与spinbox控件交互来复现上述Hardfault:


2

接着,打开Register栏,查看此时栈指针寄存器的值。由于并未使用OS,所以查看MSP即可


3

可以看到,此时MSP的值为0x20001F08
在Keil的Memory栏中查询地址0x20001F08,第6个字处对应的就是触发hardfault的代码地址0X08006275


4

在反汇编窗口中跳转到该地址对应的代码(Show Dissassembly at Address),即可找到触发hardfault的那一行代码


5

此汇编代码对应的就是下图中定义的OLED_WinFSM函数(以下简称FSM函数


6

对于ARM v7处理器,异常处理机制要在异常入口处自动保存R0~R3、R12、LR、PSR以及异常处理结束后的返回地址,共8个寄存器。它们被称作栈帧。在异常发生时(比如Hardfault中断发生时),被压入栈空间的栈帧如下图所示,栈指针指向栈帧的底部,即被保存的R0寄存器


7

找出产生问题的C语言函数调用位置
使用Ctrl+F全局搜索FSM的函数名并检查代码,发现FSM函数似乎在这个位置被集中调用:


8

它们四个分别代表slideWin、configWin、infoWin与spinWin控件的状态机。
观察代码,显然在与spinbox控件交互后,会调用

OLED_WinFSM(&(p_cur_ui->spinWin.win), p_cur_ui->current_page, op, time);

而通过调试能够佐证,就是在进入这个函数调用后发生了错误
继续细分,寻找产生问题的汇编代码位置
在FSM函数的定义内打断点,之后与spinbox控件交互,发现CPU在FSM函数内中断了8次。也就是说在用户与spinbox控件交互后,CPU一共进入了8次FSM函数,而在第8次进入后发生了HardFault
在第8次进入FSM后,在FSM函数内部开始单步调试,定位到在进入w->react函数时发生了错误(以下简称react函数)
查看此时react函数的反汇编上下文代码:


9

这些汇编代码的意义如下:

0x08005ECC 6A22      LDR      r2,[r4,#0x20]   ; 加载react函数指针到r2
0x08005ECE 4629      MOV      r1,r5           ; sel_item参数
0x08005ED0 4630      MOV      r0,r6           ; bg参数
0x08005ED2 4790      BLX      r2              ; 调用react函数

在反汇编代码中继续进行单步调试,发现在0x08005ED2 处,即“调用react函数”时发生了错误

二、提出问题 解决问题

🤔在0x08005ED2处发生了什么错误?
🤔为什么在第8次调用FSM函数时才会引起错误?
🤔为什么前7次调用FSM函数时没有引起错误?
要回答这三个问题,可以对比一下这8次FSM函数的调用有何异同。
对上述react函数的汇编代码上下文进行分析,发现当错误产生时,即

0x08005ED2 4790      BLX      r2              ; 调用react函数

能够在Register栏中查到被放进R2寄存器里的值为0x41200000,这显然不是一个正常的函数地址,也就是说

0x08005ECC 6A22      LDR      r2,[r4,#0x20]   ; 加载react函数指针到r2

这里并没有把正确的react函数指针放到r2中

而当我尝试与不会引发错误的infoWin、slideWin控件交互,在同样的位置进行调试,发现被放进r2寄存器的值分别为0x08003DD5、0x08004875,他们指向了两个正常的react函数OLED_infoWinReact与OLED_slideValWinReact

至此可以确定,之所以会引发HardFault是因为在发生问题的FSM函数中,在调用react函数前加载进r2寄存器的值是错误的
🤔为什么加载进r2寄存器的值会是错误的?
仔细观察发生问题的react函数反汇编上下文代码

    21:         w->show(sel_item, time); 
0x080059E0 4619      MOV      r1,r3
0x080059E2 4628      MOV      r0,r5
0x080059E4 4790      BLX      r2
0x080059E6 6A22      LDR      r2,[r4,#0x20]
    22:         w->react(bg, sel_item); 
    23:         break; 
    24:     default: 
    25:         break; 
    26:     } 
0x080059E8 4629      MOV      r1,r5
0x080059EA 4630      MOV      r0,r6
0x080059EC 4790      BLX      r2

我记下了这几行汇编代码执行时CPU CoreRegister的部分栈帧,并使用DeepSeek辅助整理分析:

;0x08005A00 4628      MOV      r0,r5 执行前:
R0=0x20000BDC    R1=0x00000014    R2=0x08004909(OLED_SpinWinShow)    R3=0x00000014    R4=0x20000214    R5=0x2000071C

;0x08005A00 4628      MOV      r0,r5 执行后:
R0=0x2000071C    R1=0x00000014    R2=0x08004909(OLED_SpinWinShow)    R3=0x00000014    R4=0x20000214    R5=0x2000071C

;0x08005A02 4790      BLX      r2 执行后:
;即w->show(sel_item, time);调用后
R0=0x40400000    R1=0x00000000    R2=0x00000000   R3=0x41800000   R4=0x20000200(并非原来的0x20000214)    R5=0x2000071C

;0x08005A04 6A22      LDR      r2,[r4,#0x20] 执行后:
R0=0x40400000    R1=0x00000000    R2=0x00000036(错误的值)   R3=0x41800000   R4=0x20000200(并非原来的0x20000214)   R5=0x2000071C

可以发现,问题根源是 R4 寄存器在调用 w->show 后被破坏,导致后续从被破坏的R4寄存器中加载了无效的 w->react 函数指针
此处调用的 w->show 值为0x08004909,指向一个OLED_SpinWinShow 函数
🤔w->show函数如何破坏 r4 寄存器的值?
我截取了OLED_SpinWinShow函数的反汇编代码:

; 以下是OLED_SpinWinShow函数开头
...
 316: void OLED_SpinWinShow(Option *sel_item, uint16_t time) { 
0x08004908 B5F0      PUSH     {r4-r7,lr}
   318:     OLED_Animation(&p_cur_ui->spinWin.win.y, &p_cur_ui->spinWin.win.y_tgt, p_cur_ui->upara->ani_param[WIN_ANI], time); 
0x0800490A 4E7A      LDR      r6,[pc,#488]  ; @0x08004AF4
......
......
......
   367:     } 

; 以下是OLED_SpinWinShow函数结尾
0x08004AEA 62A8      STR      r0,[r5,#0x28]
0x08004AEC 480A      LDR      r0,[pc,#40]  ; @0x08004B18
0x08004AEE 63A8      STR      r0,[r5,#0x38]
   368: } 
0x08004AF0 B005      ADD      sp,sp,#0x14
0x08004AF2 BDF0      POP      {r4-r7,pc}
0x08004AF4 0020      DCW      0x0020
...

把它提供给DeepSeek,让它分析上述问题w->show函数如何破坏 r4 寄存器的值,以下是它的分析内容:

DeepSeek分析原文

根据提供的汇编代码和寄存器变化,HardFault的直接原因是 OLED_SpinWinShow 函数在返回时错误地恢复了 r4 的值。以下是关键分析步骤:

1. 函数栈操作验证
函数开头:PUSH {r4-r7, lr} 压栈了 r4-r7 和 lr(共 5 个寄存器,占用 20 字节,即 0x14)。
函数结尾:
0x08004AF0 ADD sp, sp, #0x14 ; 释放局部变量空间(假设内部分配了 0x14 字节)
0x08004AF2 POP {r4-r7, pc} ; 恢复 r4-r7 并返回
如果函数内部正确分配并释放了 0x14 字节的局部变量,栈指针应指向压栈的 r4 值,恢复正确。
问题现象: 调用 OLED_SpinWinShow 后,r4 从 0x20000214 变为 0x20000200(减少了 0x14)。
这表明 POP {r4-r7, pc} 时从栈中恢复的 r4 值已被覆盖,根本原因是栈指针 (sp) 未正确指向保存的寄存器区域。
2. 栈操作错误定位
关键矛盾: ADD sp, sp, #0x14 的调整量可能错误。 如果函数内部实际分配的局部变量空间小于 0x14 字节(例如分配了 0x10 字节),则:AsmSUB sp, sp, #0x10 ; 分配 16 字节局部变量 … ; 函数逻辑 ADD sp, sp, #0x14 ; 错误!多释放了 4 字节这会导致栈指针 (sp) 在 POP 时指向错误的位置,恢复的 r4 值来自父结构体地址 0x20000200,而非预期的 w 地址 0x20000214。
3. 验证调试建议
修正栈调整量: 如果函数内部实际分配了 0x10 字节局部变量,将结尾的 ADD sp, sp, #0x14 改为 ADD sp, sp, #0x10。

检查 OLED_SpinWinShow 内部的栈分配指令: 在函数内部查找 SUB sp, sp, #N 指令,确认 N 是否等于 0x14。 如果 N

现在,有很大把握可以怀疑OLED_SpinWinShow函数中出现了栈溢出的情况。情况大致像下面这样:

在此函数运行前,栈指针指向在调用show函数之前入栈的寄存器栈帧,这其中包含被篡改的R4寄存器。在此函数运行开始时,编译器认为在函数内需要分配20B的栈空间,于是在它函数的开头加入了:

SUB sp, sp, #0x14

从而让出20B的栈空间共局部变量使用。而在函数运行期间发生了栈溢出,导致越界访问并修改了这20B局部变量以外的栈数据,包括R4寄存器的值


10

至此,问题基本解决。使用DeepSeek继续分析这个函数的内容,它轻而易举找到了一个数组:

// num buffer
char numBuff[8];
sprintf(numBuff, "%+08d", sel_item->val);

在这里,numBuff[8] 看似需要8字节,实际 sprintf(numBuff, "%+08d", …)可能写入超过 8 字节(例如数值为 -12345678 时占 9 字节)而导致栈溢出。扩大numBuff数组的大小:

// num buffer
char numBuff[12];
sprintf(numBuff, "%+08d", sel_item->val);

问题解决


11

发表回复

这篇文章有一个评论

  1. 第 取名困难中页

    墙裂推荐snprintf!