本文由群友 黄苇鳽 撰稿于2025年8月11日。DualMono代为投稿。
使用DeepSeek辅助定位/解决栈溢出导致的Hardfault
起因
最近在为STM32L051K8U6(ARM Cortex-M0+ ARMv7)单片机移植开源Wouo GUI框架后,发现每次与spinbox控件交互时,程序都会立即卡死并且进入hardfault_Handler。

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

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

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

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

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

对于ARM v7处理器,异常处理机制要在异常入口处自动保存R0~R3、R12、LR、PSR以及异常处理结束后的返回地址,共8个寄存器。它们被称作栈帧。在异常发生时(比如Hardfault中断发生时),被压入栈空间的栈帧如下图所示,栈指针指向栈帧的底部,即被保存的R0寄存器
找出产生问题的C语言函数调用位置
使用Ctrl+F全局搜索FSM的函数名并检查代码,发现FSM函数似乎在这个位置被集中调用:

它们四个分别代表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函数的反汇编上下文代码:

这些汇编代码的意义如下:
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寄存器的值
至此,问题基本解决。使用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);
问题解决

墙裂推荐snprintf!