修正 CubeMX 重生成防丢失机制:审计脚本实测有效,恢复 5 处被覆盖定制

第二次重生成验证了审计机制,并证伪了上一轮的一处假设:

- 审计脚本 check_cubemx_survival.py 准确检出全部 5 处覆盖(16 项中 5 FAIL),
  机制有效;Src/main.c 复位原因锁存(USER CODE BEGIN Init 区)完好,
  是"USER CODE 区有效"的正向证据。
- 关键发现:CubeMX 本次换了一代中断函数模板(旧 HardFault_Handler /
  HardFault_Report 自造区 → 新函数体内 HardFault_IRQn 0 / W1_HardFault_IRQn 0),
  "生成区之外的自造标记区"同样不被保留,旧假设被证伪。

恢复方案(第二次升级):

- stm32f4xx_it.c:报告实现迁入 USER CODE BEGIN 0(区名从未变过的标准保护位);
  Handler 本体改为生成区整函数替换,用 #define HardFault_Handler
  HardFault_Handler_Gen 把生成实体改名保留,链接器自动移除(.map 实证
  Removing ... (2 bytes)),向量表指向手工 __asm 版(0x080001a1,18 字节)。
- iwdg.c:去掉 HAL_IWDG_Init() 调用,补 IWDG-DEFER-START-ANCHOR 锚点注释。
- FreeRTOSConfig.h:恢复 FREE_RTOS_CONFIG_HARDENING 块(两个加固宏)。
- freertos.c:补回两处任务栈中文注释(栈值未丢,.ioc 已同步)。

配套:

- check_cubemx_survival.py:hardfault-handler 改 wholefile + 锚点检查;
  UNPROTECTED_LEGACY 清单更新。
- AGENTS.md:新增"CubeMX 换模板"与"ARMCC 内联汇编语法"两条铁律,
  以及"uvprojx 被 Keil 自动改写不入库"条目。
- 看门狗设计说明.md:§10.1 补第二次重生成验证结论与模板换代发现,
  §10.2 L2/L3 全面改写(自造区名不可依赖)。

验证:@build 0 错误 0 警告;pre-commit 13 钩子全通过;审计 16 通过 / 2 警告 / 0 失败。
This commit is contained in:
2026-09-29 07:52:39 +08:00
parent 7a18d2ce28
commit 67d26ac37a
7 changed files with 271 additions and 129 deletions
+89 -25
View File
@@ -759,21 +759,62 @@ external pin → "pin reset" (新增)
| 级别 | 文件 | 被覆盖内容 | 恢复方式 |
|---|---|---|---|
| **P0** | `Src/iwdg.c`(新文件) | CubeMX 生成的 `MX_IWDG_Init()` 直接调 `HAL_IWDG_Init()` → 开机死循环 | 改为只填参数;插 `USER CODE BEGIN IWDG_DEFER_START` 注释块说明(**唯一必需的手工改动**) |
| **P1** | `Src/stm32f4xx_it.c` | `HardFault_Handler` 崩溃现场采集(91 行:PC/LR/xPSR/R0-R3/CFSR/HFSR/BFAR/MMFAR)被打回空 `while(1)` | 整块换回手工版(`__asm void` 汇编取 MSP/PSP + `hard_fault_report`) |
| **P0** | `Src/iwdg.c`(新文件) | CubeMX 生成的 `MX_IWDG_Init()` 直接调 `HAL_IWDG_Init()` → 开机死循环 | 改为只填参数(**此改动在生成区,无 USER CODE 位可用**),插 `IWDG-DEFER-START-ANCHOR` 锚点注释供审计检出 |
| **P1** | `Src/stm32f4xx_it.c` | `HardFault_Handler` 崩溃现场采集(PC/LR/xPSR/R0-R3/CFSR/HFSR/BFAR/MMFAR)被打回空 `while(1)` | 整函数替换为 `__asm` 版;生成实体用 `#define` 改名保留(链接器自动移除);**报告实现迁入 `USER CODE BEGIN 0`** |
| **P1** | `Inc/FreeRTOSConfig.h` | `configCHECK_FOR_STACK_OVERFLOW 2` + `configUSE_MALLOC_FAILED_HOOK 1` **整个块被删除** | 插回 `USER CODE BEGIN FREE_RTOS_CONFIG_HARDENING` 块 |
| **P2** | `Src/freertos.c` | 三处中文注释丢失(任务栈大小的实测记录) | 恢复注释(栈**数值**未丢,`.ioc` 同步到位) |
| **P2** | `Src/freertos.c` | 两处中文注释丢失(任务栈大小的实测记录) | 恢复注释(栈**数值**未丢,`.ioc` 同步到位) |
> ⚠️ **P1 两处的教训**:它们都在 `USER CODE` 标记区**之外**(`stm32f4xx_it.c` 的
> `HardFault_Handler` 由 CubeMX 直接生成;`FreeRTOSConfig.h` 的加固宏原本写在
> 非 `USER CODE` 区)。**P1 的丢失不会立刻暴露**——栈溢出检测与堆失败钩子只在
> 真发生溢出/耗尽时才起作用,且 `HardFault_Handler` 被打回空死循环后
> **崩溃时连现场都打不出来**(只剩 IWDG 兜底复位,但归因信息全丢)。
> 故本次已把这三个宏搬进 `USER CODE BEGIN 1` 区,并加注释
> "⚠ 本段为手工添加,CubeMX 重生成会删除(2026-09-29 实测),改完须核对本块存在"。
> 故本次已把这三个宏搬进 `USER CODE BEGIN FREE_RTOS_CONFIG_HARDENING` 区。
> **每步改完必须 `@build` 0 错误 0 警告**(`AGENTS.md` 铁律)。第 0~6 步已一次性
> 验证通过:`BUILD_OK: 0 errors, 0 warnings`。
> **每步改完必须 `@build` 0 错误 0 警告**(`AGENTS.md` 铁律)。
#### ⚠️ 第 0 步复现(2026-09-29 第二次重生成):审计脚本有效,但机制假设需修正
第一次重生成后我们建立了审计脚本并"加固"了恢复方案,随后**用户又重生成了一次**,
据此验证机制:
- ✅ **审计脚本准确检出全部 5 处覆盖**(16 项清单中 5 项 FAIL)——L3 兜底手段有效
- ✅ **`Src/main.c` 的复位原因锁存完好**(在 `USER CODE BEGIN Init` 区)——L2 有效性的**正向证据**
- ✅ `MDK-ARM/*.uvprojx` 中 `task_watchdog.c` 挂载保住
但暴露出 **L2 的一个致命前提错误**:
> **🔴 关键发现:CubeMX 会"换一代"中断函数模板 ——
> "生成区之外的自造标记区"同样不被保留。**
>
> - **旧模板**:在 `HardFault_Handler` 函数**前后**生成自造区
> `/* USER CODE BEGIN HardFault_Handler */` 与 `/* USER CODE BEGIN HardFault_Report */`
> → 上一轮我们把手写实现放进这两个区,**当时确实生效了**。
> - **新模板**:改成函数体**内部**的
> `/* USER CODE BEGIN HardFault_IRQn 0 */` 与 `/* USER CODE BEGIN W1_HardFault_IRQn 0 */`
> → 旧的 `HardFault_Handler` / `HardFault_Report` 区**整段消失**,放在里面的代码一并丢失。
>
> **教训**:`USER CODE` 区有效的前提是"**该区由 CubeMX 生成且名字仍被当前模板使用**"。
> CubeMX 换模板(或改区名)时,**依赖旧区名的内容会连同区一起消失**,
> 而它看起来仍像"我们遵守了规则"。**不要假设自造区名的稳定性——只信 CubeMX 当前
> 模板真实生成的区名**(用一次重生成后的文件核对,而非记忆)。
据此对 `stm32f4xx_it.c` 的恢复方案做了**第二次升级**:
| 组件 | 上一轮落点 | **本轮最终落点** | 稳定性 |
|---|---|---|---|
| 崩溃报告实现(`hard_fault_report` 等) | 自造区 `HardFault_Report` | **`USER CODE BEGIN 0`**(文件头部,CubeMX 标准保护位,名字从未变过) | ⭐⭐ L2 真有效 |
| `HardFault_Handler` 实体 | 自造区 `HardFault_Handler` | **生成区内整函数替换** + `HARD-FAULT-ANCHOR` 锚点;生成实体经 `#define HardFault_Handler HardFault_Handler_Gen` 改名保留,链接器自动移除(`.map` 实测 `Removing ... (2 bytes)`) | ⭐ L3 审计兜底 |
> **为什么 Handler 只能放生成区**:CubeMX 本代模板把整个函数体(含 `HardFault_IRQn 0` /
> `W1_HardFault_IRQn 0`)连同函数一起重写;函数体之外的自造区不保留。查询 `.map`
> 确认向量表指向我们的 `__asm` 版(`0x080001a1`,`.emb_text`,18 字节),
> 生成版被链接器丢弃 —— **零代码开销,且未删除生成区任何一行**。
> **ARMCC 语法坑(本轮踩过)**:`__asm volatile ("MRS %0, lr" : "=r" (x))` 是 GCC 内联
> 汇编语法,ARMCC(MDK)报 `#18: expected a ")"`;`__asm { MRS x, lr }` 块内联会把
> `lr` 当 C 标识符报 `#20: identifier "lr" is undefined`。**唯一可行写法**是
> 整个函数用 `__asm void f(void) { ... }`(旧代码即此写法,已验证可行)。
### 10.2 让定制"重生成不丢"的三层手段(**通用方法论**)
@@ -819,29 +860,41 @@ external pin → "pin reset" (新增)
| 文件 | 可用标记区 | 已用于 |
|---|---|---|
| `Inc/FreeRTOSConfig.h` | `Header` / `Includes` / `FREE_RTOS_CONFIG_HARDENING` / `MESSAGE_BUFFER_LENGTH_TYPE` / `1` / `Defines` | 加固宏(HARDENING)、`configASSERT`(1) |
| `Src/freertos.c` | 共 25 个,含 `Includes` / `Application` / `Variables` / `PD` / `PTD` / … | 钩子实现(`Application`) |
| `Src/stm32f4xx_it.c` | 共 52 个,**每个中断函数都有专属区**(`HardFault_Handler` / `DMA1_Stream3_IRQn` / …) | 崩溃采集(`HardFault_Handler` + `HardFault_Report`) |
| `Src/freertos.c` | 共 25 个,含 `Includes` / `Application` / `Variables` / `PD` / `PTD` / … | 钩子实现(`Application`)、`timeSyncTask` 属性定义(`Variables`) |
| `Src/stm32f4xx_it.c` | 共 52 个,含文件头 `0`(标准保护位)+ 各中断函数**内部**的 `<Func>_IRQn 0` / `W1_<Func>_IRQn 0` | 崩溃报告实现(`0`) |
| `Src/main.c` | 共 19 个,含 `Init` / `SysInit` / `2` / `PV` / `PFP` | 复位原因锁存(`Init`) |
| `Src/iwdg.c` | 共 7 个,含 `IWDG_Init 0/1/2` / `IWDG_DEFER_START`(我方新增) | 阻止自动启动(`IWDG_DEFER_START`) |
| `Src/iwdg.c` | 共 7 个,含 `IWDG_Init 0/1/2` | ❌ **本次改动用不上**(见下方特例) |
| `Inc/main.h` | 共 7 个 | 引脚宏等 |
> ⚠️ **`Src/iwdg.c` 的特例**:本次需要的"不自动启动"改动**必须落在生成区**
> (`hiwdg.Init.*` 赋值语句与 `HAL_IWDG_Init()` 调用之间的位置),
> 而 CubeMX **不为这两句之间生成标记区**。于是采取"**在生成区里插入一个
> 自定义命名的 USER CODE 块**"——实测 CubeMX 会保留**任何**形态的
> `USER CODE BEGIN/END` 注释对,即使区名不是它预定义的。这是 L2 的
> 一个有用变体:**可以自己造标记区**。
> 🔴 **两条硬教训(2026-09-29 实测,务必先读)**
>
> **① 不要依赖"自造标记区"**。上一轮我们按"CubeMX 保留任何形态的 `USER CODE` 注释对"
> 的结论,在 `stm32f4xx_it.c` 里自造了 `HardFault_Handler` / `HardFault_Report` 区,
> 当时**确实生效**。但第二次重生成时 CubeMX **换了中断函数模板**,区名从
> `HardFault_Handler` 变成函数体内的 `HardFault_IRQn 0` / `W1_HardFault_IRQn 0`,
> **旧区连同内容一起消失**。
> → **只信 CubeMX 当前模板真实生成的区名**,用重生成后的文件核对,别凭记忆。
> → **宁可用文件头 `USER CODE BEGIN 0` 这类"区名从未变过"的标准位**。
>
> **② 生成区内无标记位可用的改动,只能 L3 兜底**。`Src/iwdg.c` 需要改的是
> `hiwdg.Init.*` 赋值与 `HAL_IWDG_Init()` 调用之间那一句,而 CubeMX 只在调用**之前**
> (`IWDG_Init 0/1`)和**之后**(`IWDG_Init 2`)生成标记区——**正中间没有**。
> 塞进 `IWDG_Init 1` 区也不能阻止区外那行被覆盖(HAL 调用在区外)。
> → 该改动**只能在生成区**,靠 `IWDG-DEFER-START-ANCHOR` 锚点注释 + 审计脚本检出。
> 同理 `HardFault_Handler` 本体(本代模板整函数重写,函数体外无区)。
#### L3:存量清单(标记区外,只能靠人记 + 脚本盯)
| 文件 | 内容 | 为什么无法 L1/L2 |
|---|---|---|
| `Src/stm32f4xx_it.c` | `MemManage_Handler` / `BusFault_Handler` / `UsageFault_Handler` 的空 `while(1)`(**建议后续加崩溃采集**,目前 `HardFault_Handler` 已加) | CubeMX 不为这三个函数生成 `USER CODE` 区(与 `HardFault_Handler` 不同,后者有);可仿照 `HardFault_Handler` **自造标记区**(见 L2 变体) → **可升级为 L2** |
| `Src/iwdg.c` | 不调 `HAL_IWDG_Init()`(**P0:开机死循环防线**) | 生成区内无标记位可用(见上方硬教训②)。锚点 `IWDG-DEFER-START-ANCHOR` + 审计 FAIL 项 |
| `Src/stm32f4xx_it.c` | `HardFault_Handler` 实体 | 本代模板整函数重写、函数体外无区。锚点 `HARD-FAULT-ANCHOR` + 审计 FAIL 项 |
| `Src/stm32f4xx_it.c` | `MemManage_Handler` / `BusFault_Handler` / `UsageFault_Handler` 的空 `while(1)`(**建议后续加崩溃采集**) | CubeMX 为这三者生成的是函数体**内部**的区,函数体被整段重写 → 只能整函数替换 + 审计。目前仅 WARN |
| `Src/freertos.c` | `.stack_size` 行内的中文注释 | 该行由 CubeMX 按 `.ioc` 生成,"值"受保护、"注释"不受。**注释是锦上添花**,丢失不影响功能(值在 `.ioc` 里) |
> **结论**:真正必须靠 L3 盯的只有 3 个 Fault 处理函数;其余要么已在 `.ioc`(L1),
> 要么已进 `USER CODE` 区(L2)。**建议后续把 3 个 Fault 处理函数也改成自造标记区版**,
> 那样 L3 清单可清空。
> **结论**:L3 清单已从"3 个 Fault 处理函数"扩展为"3 项生成区改动 + 3 个 Fault 函数"。
> 前 3 项已挂审计 FAIL(丢了一定知道),后 3 项建议后续一并升级为崩溃采集 + 审计。
> **L3 的定位是兜底而非根治**——只要改动落在生成区,就不可能 L1/L2。
#### L3 落地:审计脚本 + 基线
@@ -851,17 +904,26 @@ python scripts/check_cubemx_survival.py --check # 门禁模式(pre-commit
python scripts/check_cubemx_survival.py --write # 重建基线(确认改动无误后)
```
- **清单**:16 项覆盖 P0/P1 全部防线(iwdg 延后启动、崩溃采集、两个加固宏、
断言改造、看门狗模块与工程挂载、复位原因锁存、HAL 驱动与使能宏、`.ioc` 关键项)
- **漂移检测**:把各项内容哈希存 `.cubemx_survival_baseline.json`,`--check` 比对
- **清单**:16 项覆盖 P0/P1 全部防线(iwdg 延后启动、崩溃采集 ×2、两个加固宏、
断言改造 ×2、看门狗模块与工程挂载、复位原因锁存、HAL 驱动与使能宏、`.ioc` 关键项)
- **已实测有效**:2026-09-29 第二次重生成时,脚本**准确检出全部 5 项被覆盖**(5 FAIL)
- **漂移检测**:把各项内容哈希存 `.cubemx_survival_baseline.json`,`--check` 比对。
⚠️ **`MDK-ARM/*.uvprojx` 排除在漂移检测之外**(15 项入基线,16 项参与断言)——
该文件每次 `@build` 后被 Keil UV4 自动改写(XML 格式归一化 + `PackID` 降级到
本机 DFP 版本),内容每次构建都不同;若参与检测则**每次构建后提交都会被拦**。
脚本内以 `no_baseline=True` 标记,只保留"源文件仍挂载"断言
- **已挂 pre-commit**(`cubemx-survival` 钩子):改动 `.ioc` / `Src/` / `Inc/` /
`task_watchdog` / `.uvprojx` 时自动触发
- **注释过滤**:断言前先剥掉 C 注释,避免"注释里提到 `HAL_IWDG_Init`"被误判
(本项目多处注释刻意引用历史代码,不做过滤会恒定误报)
(本项目多处注释刻意引用历史代码,不做过滤会恒定误报);
但**正项检查(`must_contain`)在 `wholefile` 模式下不剥注释**,
因为锚点本身就是注释(`IWDG-DEFER-START-ANCHOR` / `HARD-FAULT-ANCHOR`)。
脚本内以 `probe`(正项)/ `probe_neg`(禁项)分离处理
> **推荐工作流**:**每次 CubeMX 重生成后,立刻跑一次
> `python scripts/check_cubemx_survival.py`**——它会把"被覆盖了哪几处"
> 直接列出来(含修复提示),比事后靠故障现象反推快得多。
> 本项目的两次重生成都是靠它把损失锁定在"5 处、含修复提示"的范围内。
---
@@ -871,7 +933,7 @@ python scripts/check_cubemx_survival.py --write # 重建基线(确认改动
|---|---|
| **选型** | **IWDG**(独立 LSI),非 WWDG(依赖 APB1 + 中断,在关中断/HardFault 场景失效) |
| **实施路径** | **CubeMX 勾选 IWDG 生成**(`.ioc` = 配置单一事实源,与其他外设一致);唯一手工改动 = `Src/iwdg.c` 中阻止 `HAL_IWDG_Init()` 立即启动(§2.2 / §4.5) |
| **防丢失** | **L1 写 `.ioc` > L2 搬进 `USER CODE` 区 > L3 审计兜底**(§10.2);已落 `scripts/check_cubemx_survival.py` + pre-commit 钩子 |
| **防丢失** | **L1 写 `.ioc` > L2 搬进 `USER CODE` 区 > L3 审计兜底**(§10.2);⚠️ **自造区名不可依赖**(CubeMX 换模板会连区带内容一起删,2026-09-29 实测);已落 `scripts/check_cubemx_survival.py` + pre-commit 钩子 |
| **架构** | **任务心跳位图(各自置位,无锁单字)+ defaultTask 单一喂狗者**——避免"有人跑就喂狗"的假保护 |
| **超时** | 2498 ms(PR=4 / RLR=1249);最坏(LSI 47 kHz)≈1701 ms;中间留 800 ms 停喂判定裕度 |
| **启动时机** | **defaultTask bring-up 完成后**——绕开初始化深链超时陷阱,代价是启动段无保护 |
@@ -930,3 +992,5 @@ python scripts/check_cubemx_survival.py --write # 重建基线(确认改动
| 2026-09-29 | 创建 | 调研 + 设计方案:IWDG + 任务心跳位图;含选型、九项设计决策、验证计划与风险清单 |
| 2026-09-29 | 修正实施路径 | 实测发现 HAL IWDG 驱动源码不在仓库且未编入 MDK 工程 → 初稿改为**直接寄存器操作**(§2.2 路径 C);超时数值按公式核算修正;补 `RCC_CSR`/`IWDG` 寄存器定义的实测行号 |
| 2026-09-29 | **路径再变更 + 实施落地** | 决策改走 **CubeMX 图形配置**(§2.2 路径 A):`HAL_IWDG_MODULE_ENABLED` 改为经 CubeMX 生成(避免"取消注释即编译失败")。同时落地 §5 骨架到 `App/util/task_watchdog.{c,h}`,各任务埋点(default/net/ui/time_sync),`RCC_CSR` 复位原因锁存(`Src/main.c` + `App/app/app_main.c`),`configASSERT` 改为 `vApplicationAssertFailed()`。实测 CubeMX 重生成覆盖 4 处既有定制(P0×1 + P1×2 + P2×1),已全部恢复,`@build` 0 错误 0 警告 |
| 2026-09-29 | **新增 §10.2 三层手段 + 审计脚本** | 沉淀通用方法论(L1 `.ioc` / L2 `USER CODE` 区 / L3 审计+基线);新建 `scripts/check_cubemx_survival.py`(16 项清单 + 漂移检测)+ `.cubemx_survival_baseline.json` + pre-commit `cubemx-survival` 钩子;`AGENTS.md` 与 `docs/代码规范/pre-commit使用说明.md` 同步 |
| 2026-09-29 | **第二次重生成验证 + 机制修正** | 审计脚本**实测有效**(准确检出 5 处覆盖)。⚠️ **关键发现**:CubeMX 换了一代中断函数模板(旧 `HardFault_Handler`/`HardFault_Report` 自造区 → 新函数体内 `HardFault_IRQn 0`/`W1_HardFault_IRQn 0`),**"生成区外的自造标记区"同样不被保留**——上一轮假设被证伪。据此升级恢复方案:报告实现迁入 `USER CODE BEGIN 0`(标准保护位);Handler 本体改为生成区整函数替换(`#define` 改名保留生成实体,链接器自动移除,`.map` 已验证)+ `HARD-FAULT-ANCHOR` 锚点。同步修正审计脚本(`hardfault-handler` 改 `wholefile` + 锚点)、`UNPROTECTED_LEGACY` 清单、§10.1/§10.2 全文。`@build` 0 错误 0 警告 |