<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>HardFault on Zero Day Notes</title>
        <link>https://blog.5772447.xyz/tags/hardfault/</link>
        <description>Recent content in HardFault on Zero Day Notes</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh</language>
        <lastBuildDate>Sun, 18 Oct 2026 09:00:00 +0800</lastBuildDate><atom:link href="https://blog.5772447.xyz/tags/hardfault/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>任务栈溢出致随机死机：HardFault 现场取证</title>
            <link>https://blog.5772447.xyz/posts/3e0783d8/</link>
            <pubDate>Sun, 18 Oct 2026 09:00:00 +0800</pubDate>
            <guid>https://blog.5772447.xyz/posts/3e0783d8/</guid>
            <description>&lt;h2 id=&#34;问题背景&#34;&gt;&lt;a href=&#34;#%e9%97%ae%e9%a2%98%e8%83%8c%e6%99%af&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;问题背景&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;公司在全国客户现场部署了约 200 台环境数据采集终端：STM32F407 + FreeRTOS，通过 4G 或以太网把传感器数据上报到平台。设备装在客户机房和户外机柜里，没有调试器、没有维护通道，只能靠远程升级和日志排障。&lt;/p&gt;&#xA;&lt;p&gt;从两个月前开始，各地陆续反馈同一类问题：设备会&amp;quot;随机重启&amp;quot;，重启后自己恢复正常，业务表现是上报数据出现一段几分钟到几十分钟的缺口。现场人员的第一反应通常是&amp;quot;断电了&amp;quot;或&amp;quot;信号不好&amp;quot;，换电源、重新上电、加强散热，折腾一圈，问题照旧。&lt;/p&gt;&#xA;&lt;h2 id=&#34;故障现象&#34;&gt;&lt;a href=&#34;#%e6%95%85%e9%9a%9c%e7%8e%b0%e8%b1%a1&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;故障现象&#xD;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;设备运行 2~6 周后随机死机，随后看门狗复位、自动恢复，时间点毫无规律。&lt;/li&gt;&#xA;&lt;li&gt;掉电保持的复位原因记录显示：绝大多数是独立看门狗（IWDG）复位。&lt;/li&gt;&#xA;&lt;li&gt;死机前串口和日志没有任何错误输出——死机瞬间根本来不及打印。&lt;/li&gt;&#xA;&lt;li&gt;故障率与数据上报频率呈正相关：上报越频繁的站点，重启越密。&lt;/li&gt;&#xA;&lt;li&gt;更换电源模块、检查晶振、加装散热之后，复现率没有变化。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;排查过程&#34;&gt;&lt;a href=&#34;#%e6%8e%92%e6%9f%a5%e8%bf%87%e7%a8%8b&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;排查过程&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;第一步，先查看门狗。&lt;/strong&gt; 把 IWDG 超时从 8 秒放宽到 30 秒，逐任务核对喂狗逻辑——复发时间没变。看门狗只是&amp;quot;收尸&amp;quot;的，不是&amp;quot;凶手&amp;quot;，排除。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;第二步，查硬件。&lt;/strong&gt; 换电源、测晶振、加散热，故障率纹丝不动；而&amp;quot;上报越频繁越容易死&amp;quot;这个相关性越来越明显——信号指向软件时序问题：负载越重，某个边界越容易被踩到。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;第三步，给死机装&amp;quot;黑匣子&amp;quot;。&lt;/strong&gt; 设备死机时没有任何输出，等于每次案发现场都被看门狗复位冲掉了。我们在固件里加入 HardFault 处理：把故障状态寄存器（CFSR、HFSR、MMFAR、BFAR）和异常自动压栈的寄存器帧（R0~R3、R12、LR、PC、xPSR）写入不参与启动清零的 noinit 内存段——看门狗复位不断电，RAM 内容可以保留；下次上电由启动代码读出并上报。同时记录 RCC_CSR 的复位标志，确认复位来源。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;第四步，等现场&amp;quot;抓到一次&amp;quot;。&lt;/strong&gt; 两周后一台高频上报的终端触发了取证，读出的数据非常明确：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;复位标志：IWDG 复位，说明确实是看门狗兜底；&lt;/li&gt;&#xA;&lt;li&gt;CFSR = &lt;code&gt;0x00020000&lt;/code&gt;，即 INVSTATE（无效执行状态）——典型的返回地址被破坏后跳到非法地址；&lt;/li&gt;&#xA;&lt;li&gt;保存的 PC 落在上报处理函数的中间，而 LR 指向的地址不在任何已知函数入口附近——栈被踩了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;strong&gt;第五步，验证栈溢出假设。&lt;/strong&gt; 打开 FreeRTOS 的栈溢出检测（&lt;code&gt;configCHECK_FOR_STACK_OVERFLOW = 2&lt;/code&gt;），并在任务创建时用固定模式填充栈空间；随后在实验室把上报频率拉到正常值的 5 倍做压力测试。结果：上报任务的 &lt;code&gt;uxTaskGetStackHighWaterMark&lt;/code&gt; 长期只剩十几个字节的余量，压力测试下栈溢出钩子直接命中，崩溃时的 PC 与现场抓到的完全一致。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;第六步，找到具体是哪个函数。&lt;/strong&gt; 用 &lt;code&gt;arm-none-eabi-addr2line&lt;/code&gt; 对照固件 ELF，把保存的 PC 翻译到源码行：问题出在上一版固件新增的&amp;quot;报文组装&amp;quot;函数——函数内声明了一个约 600 字节的局部数组，叠加原有调用链，在这个任务 1KB 的栈里几乎没有余量；一旦上报队列积压或中断嵌套加深，最后一点空间被压垮，返回地址被覆盖，异常返回跳到非法地址，HardFault 触发，看门狗复位收场。&lt;/p&gt;&#xA;&lt;h2 id=&#34;解决方案&#34;&gt;&lt;a href=&#34;#%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;解决方案&#xD;&#xA;&lt;/h2&gt;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;立即处理&lt;/strong&gt;：把上报任务栈从 1KB 增加到 2KB，留足余量。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;根治问题&lt;/strong&gt;：报文组装的大局部数组改为静态缓冲区（启动时一次性分配），函数内部不再放大栈帧；同时排查其他任务的局部大变量。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;保留黑匣子&lt;/strong&gt;：HardFault 取证代码保留在正式固件里，故障信息纳入平台日志上报；现场设备远程升级新固件。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;加监控&lt;/strong&gt;：把上报任务的栈高水位随心跳上报，低于阈值即告警，让&amp;quot;栈快满了&amp;quot;在死机之前就被看见。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;验证：现场 20 台设备升级后连续运行 8 周零死机；实验室 10 倍上报频率压力测试 72 小时无异常。&lt;/p&gt;&#xA;&lt;h2 id=&#34;根因分析&#34;&gt;&lt;a href=&#34;#%e6%a0%b9%e5%9b%a0%e5%88%86%e6%9e%90&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;根因分析&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;直接根因是&lt;strong&gt;任务栈空间不足叠加新增函数的大局部数组&lt;/strong&gt;：平时栈余量就只剩十几字节，遇到上报队列积压或中断嵌套加深，栈溢出覆盖返回地址，异常返回跳向非法地址触发 HardFault，看门狗兜底复位。&lt;/p&gt;&#xA;&lt;p&gt;深层根因有三层：开发阶段没有做栈用量评估，栈大小是随手填的；新增功能只测&amp;quot;功能对不对&amp;quot;，没测&amp;quot;资源够不够&amp;quot;；设备部署在现场，没有调试器和日志，缺少故障取证手段——前两个问题注定会发生，第三个问题让定位拖了整整两个月。&lt;/p&gt;&#xA;&lt;h2 id=&#34;预防措施&#34;&gt;&lt;a href=&#34;#%e9%a2%84%e9%98%b2%e6%8e%aa%e6%96%bd&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;预防措施&#xD;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;任务栈按实测高水位的 1.5~2 倍预留，关键任务启用栈溢出检测与填充模式校验；&lt;/li&gt;&#xA;&lt;li&gt;禁止在任务函数里声明大局部数组，统一用静态缓冲区或启动时一次性分配的堆内存；&lt;/li&gt;&#xA;&lt;li&gt;新固件上线前必须跑满负载压力测试（高频率上报、弱网重传、队列积压），把资源边界打出来；&lt;/li&gt;&#xA;&lt;li&gt;所有现场设备固件内置 HardFault 取证（寄存器加栈帧保存、复位原因上报），把&amp;quot;黑匣子&amp;quot;做成标准能力；&lt;/li&gt;&#xA;&lt;li&gt;排查纪律：先拿到证据再动手，避免&amp;quot;换电源、放宽看门狗&amp;quot;这类盲目试错。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;总结&#34;&gt;&lt;a href=&#34;#%e6%80%bb%e7%bb%93&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;总结&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;嵌入式设备的随机死机，难的不是修，而是&amp;quot;看见&amp;quot;——设备在客户现场，没有调试器、没有串口，死机瞬间什么都不会留下。这次能破案，靠的是把 Cortex-M 的异常机制用起来：让 HardFault 在死之前把寄存器现场写进不掉电的内存，等下一条命开始时&amp;quot;开口说话&amp;quot;。栈溢出是嵌入式世界最古老的死因之一，但只有当证据链补齐，它才会从玄学变成一行可以改的代码。&lt;/p&gt;&#xA;&lt;h2 id=&#34;延伸阅读&#34;&gt;&lt;a href=&#34;#%e5%bb%b6%e4%bc%b8%e9%98%85%e8%af%bb&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;延伸阅读&#xD;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://blog.5772447.xyz/posts/10494fcc/&#34; &gt;RS485 总线干扰致采集网关每隔几小时集体掉线&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://blog.5772447.xyz/posts/7fec59ab/&#34; &gt;OTA 固件升级断电变砖：双分区方案缺位&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;</description>
        </item></channel>
</rss>
