修正 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
+67 -20
View File
@@ -31,6 +31,13 @@ CubeMX 重生成存活审计(check_cubemx_survival.py)
退出码:0 = 通过;1 = 有 FAIL 项。
设计依据:docs/子系统设计/看门狗设计说明.md §10.1;AGENTS.md「平台约束」。
注意
----
`MDK-ARM/*.uvprojx` **不参与漂移检测**(`no_baseline=True`):该文件每次
`@build` 后被 Keil UV4 自动改写(XML 格式归一化 + `PackID` 降级到本机 DFP
版本),内容每次构建都不同。若参与检测,则**每次构建后提交都会被拦**。
本项只保留 `must_contain` 断言("源文件仍挂载")。
"""
import argparse
@@ -55,32 +62,55 @@ BASELINE = os.path.join(REPO, ".cubemx_survival_baseline.json")
CHECKS = [
# ---------------- P0:开机死循环防线 ----------------
# ⚠ 这一项是**生成区内的手工改动**(无 USER CODE 区可用):
# 需要改的是 `hiwdg.Init.*` 赋值与 `HAL_IWDG_Init()` 调用之间的位置,
# 而 CubeMX 只在该调用**之前**(IWDG_Init 0/1)和**之后**(IWDG_Init 2)
# 生成标记区——正中间没有。塞进 IWDG_Init 1 区也不能阻止区外那行被覆盖
# (另见 `usr init 1` 区的注释)。
# 故本项按"文件整体 + 特征串"检查,并靠一个稳定锚点注释定位。
dict(
id="iwdg-defer-start",
kind="protected",
kind="wholefile",
path="Src/iwdg.c",
region="IWDG_DEFER_START",
must_not_contain=["HAL_IWDG_Init(&hiwdg)"],
must_contain=["IWDG-DEFER-START-ANCHOR"], # 手工锚点注释,重生成后会消失
must_not_contain=["HAL_IWDG_Init(&hiwdg)"], # 真实调用不得存在
severity="FAIL",
hint="MX_IWDG_Init() 不得调 HAL_IWDG_Init()(内部立即启动,"
"而它跑在 app_main_init 之前 → 开机必复位)。启动移交 task_wd_start()。",
"而它跑在 app_main_init 之前 → 开机必复位)。启动移交 task_wd_start()。"
"恢复后补回 `IWDG-DEFER-START-ANCHOR` 锚点注释。",
),
# ---------------- P1:崩溃可观测性 ----------------
# ⚠ 2026-09-29 重要变更:CubeMX 把本文件的中断函数模板**换了一代**——
# 旧模板在函数前后给自造区(HardFault_Handler / HardFault_Report),
# 新模板改成函数体**内部**的 `HardFault_IRQn 0` / `W1_HardFault_IRQn 0`。
# 实测"生成区之外的自造标记区"**不会被保留**。故现在的落点是:
# Handler 实体 → 生成区内整函数替换(无 USER CODE 位可用),
# 靠 `HARD-FAULT-ANCHOR` 锚点注释检出丢失;
# 生成实体用 `#define HardFault_Handler HardFault_Handler_Gen`
# 改名保留,链接器自动移除(map 已验证 2 bytes removed)
# 报告实现 → `USER CODE BEGIN 0`(生成区内的标准保护位,重生成不丢)
dict(
id="hardfault-report",
kind="protected",
id="hardfault-handler",
kind="wholefile",
path="Src/stm32f4xx_it.c",
region="HardFault_Handler",
must_contain=["hard_fault_report"],
hint="HardFault_Handler 须为手工汇编版(取 MSP/PSP 交 hard_fault_report)。"
"被打回空 while(1) 后死机将完全静默。",
must_contain=[
"HARD-FAULT-ANCHOR", # 手工锚点注释,重生成后会消失
"__asm void HardFault_Handler", # 手工汇编 Handler
"hard_fault_report", # 跳转目标
],
must_not_contain=["HardFault_Handler(void)\r\n{\r\n while (1)"],
hint="HardFault_Handler 须为手工汇编版(取 MSP/PSP 交 hard_fault_report),"
"生成实体改名 HardFault_Handler_Gen。被打回空 while(1) 后死机将完全静默。"
"恢复后补回 `HARD-FAULT-ANCHOR` 锚点注释。",
),
dict(
id="hardfault-report-impl",
kind="protected",
path="Src/stm32f4xx_it.c",
region="HardFault_Report",
must_contain=["CFSR"],
hint="HardFault 现场报告实现(打印 PC/LR/xPSR/CFSR/HFSR/BFAR/MMFAR)。",
region="0",
must_contain=["hard_fault_report", "CFSR"],
hint="HardFault 现场报告实现(打印 PC/LR/xPSR/CFSR/HFSR/BFAR/MMFAR)"
"须在 USER CODE BEGIN 0 区内(生成区内的标准保护位)。",
),
# ---------------- P1:FreeRTOS 加固 ----------------
dict(
@@ -138,6 +168,7 @@ CHECKS = [
kind="generated",
path="MDK-ARM/STM32F407-Demo.uvprojx",
must_contain=["task_watchdog.c"],
no_baseline=True, # 见下方说明:本文件每次 @build 后被 Keil 改写
hint="task_watchdog.c 须挂在 MDK 工程 App 组;CubeMX 重生成可能重建 .uvprojx。",
),
dict(
@@ -268,6 +299,10 @@ def evaluate():
continue
# ---- 取待检内容 ----
# `probe_neg`:用于 must_not_contain(禁项)——**总是剥注释**,
# 避免"注释里提到历史代码"被误判为真实调用。
# `probe` :用于 must_contain(正项)——`wholefile` 模式下不剥注释,
# 因为锚点本身可能就是注释(如 IWDG-DEFER-START-ANCHOR)。
if c["kind"] == "protected":
body = region_of(text, c["region"])
if body is None:
@@ -276,16 +311,28 @@ def evaluate():
)
snap[cid] = None
continue
probe = strip_comments(body) # 只对保护区做注释过滤
probe = strip_comments(body)
probe_neg = probe
elif c["kind"] == "wholefile":
body = text
probe = text # 正项:整文件原文(锚点可能是注释)
probe_neg = strip_comments(text) # 禁项:仍剥注释
else:
body = text
probe = text
probe_neg = text
snap[cid] = sha(probe)
# ---- 基线快照 ----
# `no_baseline=True` 的项不参与漂移检测。理由:`MDK-ARM/*.uvprojx`
# 每次 `@build` 后被 Keil UV4 自动改写(XML 格式归一化 + PackID 降级到
# 本机 DFP 版本),内容每次构建都不同 → 若参与漂移检测,则**每次构建后
# 提交都会被拦**。本项只保留 must_contain 断言("源文件仍挂载")。
if not c.get("no_baseline"):
snap[cid] = sha(probe)
# ---- 断言 ----
bad = [n for n in c.get("must_contain", []) if n not in probe]
bad += [n for n in c.get("must_not_contain", []) if n in probe]
bad += [n for n in c.get("must_not_contain", []) if n in probe_neg]
if bad:
results.append(
(level, cid, "%s:%s" % (c["region"] if c["kind"] == "protected" else path,
@@ -318,15 +365,15 @@ UNPROTECTED_LEGACY = [
(
"Src/freertos.c",
[
("任务栈注释(.stack_size 行内中文说明)", r"\.stack_size\s*=\s*\d+\s*\*\s*4"),
("timeSyncTask 手工定义(不在 .ioc FREERTOS.Tasks01 里)", r"timeSyncTask_attributes"),
],
),
(
"Src/stm32f4xx_it.c",
[
("MemManage_Handler 空 while(1)", r"void MemManage_Handler\(void\)"),
("BusFault_Handler 空 while(1)", r"void BusFault_Handler\(void\)"),
("UsageFault_Handler 空 while(1)", r"void UsageFault_Handler\(void\)"),
("MemManage_Handler 仍为空 while(1),死机静默", r"void MemManage_Handler\(void\)"),
("BusFault_Handler 仍为空 while(1),死机静默", r"void BusFault_Handler\(void\)"),
("UsageFault_Handler 仍为空 while(1),死机静默", r"void UsageFault_Handler\(void\)"),
],
),
]