diff --git a/docs/review/GD32_BOOT_专家评审_2026-10-19.html b/docs/review/GD32_BOOT_专家评审_2026-10-19.html new file mode 100644 index 0000000..0bf3cdf --- /dev/null +++ b/docs/review/GD32_BOOT_专家评审_2026-10-19.html @@ -0,0 +1,103 @@ + +GD32_BOOT 专家评审 2026-10-19 +

GD32_BOOT 专家评审报告

+

仓库:cbb/GD32_BOOT 分支:GD32C103 评审基线提交:05069e3(2026-07-26,“移除CAN DMA相关代码并优化编译配置”) 评审日期:2026-10-19 报告提交:2026-10-20

+
3致命
9严重
11一般
6建议
5.0综合评分 / 10
+ +

1. 项目概述与架构

+

GD32C103CB(Cortex-M4,128 KB Flash / 32 KB SRAM,120 MHz HXTAL PLL)上的 CAN-FD Bootloader,C++(AC6 + microlib,CMSIS csolution 构建)。功能:上电判定标志位 → 直接跳 APP / 10 s 延迟窗口监听 OTA / 从备份区恢复 / 停留 OTA 模式;OTA 采用 AES-128 握手派生会话密钥,48 字节/包加密传输,每包带密钥 CRC32;另含深度睡眠与 PB8 EXTI 唤醒。

+ + + + + + +
Flash 区域地址范围大小用途
BOOT0x08000000-0x08005FFF24 KBBootloader(当前使用 0x3398 ≈ 13 KB)
APP_BACKUP0x08006000-0x08011FFF48 KB恢复镜像(BOOT 只读不写)
APP_LATEST0x08012000-0x0801DFFF48 KB运行镜像,OTA 写入目标
OTA 标志页0x0801E000-0x0801E3FF1 KB6 个 32 位标志:LATEST/BACKUP 的 AVAL/EN/CHECK(0=有效,0xFFFFFFFF=无效)
睡眠配置页0x0801E400-0x0801E7FF1 KB追加式睡眠使能记录
(flash.cpp 预留)0x0801F800-0x0801FFFF2 KBEEPROM 模拟,未参与构建
+
上电 → systick/USART0/CAN-FD 初始化 → check_sleep()(使能则深睡,PB8 唤醒后软复位)
+  ├─ LATEST.CHECK=0 且 EN=0        → jumpToApplication()
+  ├─ LATEST.CHECK=0 且 EN=0xFFFF.. → 10 s 窗口:发 WAIT 心跳(0x103),收到握手则进入 OTA 循环,否则跳 APP
+  └─ 否则:BACKUP.CHECK=0 且 AVAL=0 → move_app_data() 备份→主区 → 跳 APP
+           否则                     → OTA 循环
+OTA 状态机:IDLE --REQ[00 AC]--> WAITING_RANDOM --REQ[16B nonce]--> WAITING_CONFIRM
+  --REQ[16B AES(K_s, nonce^0x55)]--> CHECKING_FIRMWARE --DATA[4B size]--> 擦除 --> RECEIVING
+  --DATA[64B: 会话号4|包号4|密文48|IFEND4|CRC4]...尾帧 IFEND=0xFFFFFFFF--> FINISHED → 跳 APP
+CAN ID(扩展帧):0x100 REQ / 0x101 DATA / 0x102 ACK / 0x103 WAIT / 0x000 CANCEL;1 Mbps 仲裁 + 2 Mbps 数据(BRS)
+ +

2. 评审范围与方法

+ + +

3. 既有评审记录检查

+
全仓(含历史提交)未发现任何 .md/.html 评审、审计或报告文件;Gitea issue 因访问令牌无 read:issue 权限无法读取。本报告是该仓库的第一份评审记录,因此没有可修订的原评审文件。作为替代,第 5 节逐条核对了提交信息中声明的修复/功能在当前代码中是否成立。
+ +

4. 问题清单

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
编号严重度领域文件:行号现象风险修改建议
F-01致命安全Robot_V2/sys/config.h:70-72;sys/OTA.cpp:531-560AES-128 主密钥 K_master 以明文常量写在头文件并提交进仓库,所有设备共用同一密钥;会话密钥 K_session = AES(K_master, nonce_server) 完全由上位机提供的随机数决定,设备侧不贡献随机性。任何拿到仓库或任一固件镜像的人即可伪造合法上位机;设备无法区分新会话与录制的旧会话,可重放旧握手与旧固件(降级攻击)。主密钥移出源码(读保护的 OTP/选项字节或按设备 UID 派生的设备密钥);握手加入设备侧随机数(双向 nonce);开启 Flash 读保护(SPC)。
F-02致命安全/升级可靠性sys/OTA.cpp:423-478固件没有整体签名或整镜像校验:仅有每包 4 字节“带密钥 CRC32”,CRC32 是线性函数,不是 MAC;AES-ECB 逐块加密,相同明文块产生相同密文,可被剪切/拼接/重排。知道密钥即可构造任意固件;即便不知道密钥,ECB 也允许在包内替换整块。被篡改或损坏的镜像会被当成合法固件写入并激活。改为“整镜像 SHA-256 + ECDSA/Ed25519 签名”或至少 AES-CMAC/HMAC;加密改为 AES-CTR/GCM 并带包序号作 nonce;结束时校验总长度与整体摘要后才置可用标志。
F-03致命升级可靠性sys/OTA.cpp:369-373, 453, 466-471① writeApplicationData 写 Flash 失败只 printf,不返回错误、不上报,随后照常回 ACK;② CRC 校验失败的包仍会继续检查尾帧标志 receive_IFEND,若尾帧本身 CRC 错误也照样置 FINISHED 并跳转;③ 结束时不校验 received_size 与 expected_size(received_size 从未累加)。丢包、写失败、尾帧损坏都可能导致残缺镜像被 jumpToApplication 置为“可用+激活”(OTA.cpp:232-233),只做向量表两字检查就跳转,设备可能变砖或运行半截固件。写失败要回错误帧并让上位机重发;尾帧必须 CRC 通过;累加已写字节并在结束时比对 expected_size 与整镜像校验值,全部通过后才写可用标志。
S-01严重Flash/断电安全sys/OTA.cpp:41-124(OTA_SET)6 个 OTA 标志共用 0x0801E000 一页。把某位从 0 改回 1 时整页擦除后再逐项重写,无双页/日志结构,重写结果不检查。擦除后、重写前掉电,所有标志(包括备份可用标志)全部变回 0xFFFFFFFF:主区与备份都被判无效,设备只能停在 OTA 模式等待上位机。改为双页乒乓或追加式记录(序号+CRC),先写新记录再作废旧记录;写后回读校验。
S-02严重Flash 驱动用法sys/OTA.cpp:18-27, 54, 86, 98, 307-309;sys/initial.cpp:208write_ADDRESS 每次写完都 fmc_lock()。OTA_SET 内先调用 write_ADDRESS(会上锁)再 fmc_page_erase;eraseApplicationArea 连续调用 3 次 OTA_SET,第 2、3 次进入时 FMC 已被上锁;EXTI 唤醒中断里调用 OTA_SET 前根本没有解锁。GD32 库在锁定时写 FMC_CTL 无效而 fmc_ready_wait 仍返回 READY。页擦除可能被静默跳过却返回成功,标志位停留在旧值(例如“激活”标志清不掉),状态机与实际 Flash 状态不一致。由 OTA_SET 自己负责一次 unlock/lock,write_ADDRESS 不再加解锁;擦除后回读确认 0xFFFFFFFF;所有 Flash 操作统一入口。
S-03严重通信协议sys/OTA.cpp:623-634getDeviceSerialNumber 把寄存器地址常量(0x1FFFF7E8 等)本身当作 UID 数值,没有解引用。所有设备上报的“UID”完全相同,UID 在握手中失去意义,无法做设备绑定或按设备派生密钥。改为 *(volatile uint32_t*)DEVICE_SERIAL_ADDR_xxx 读取。
S-04严重通信协议sys/config.h:49-53;device/Can.cpp:105-119, 196-222协议没有节点地址:所有设备使用相同的固定 CAN ID(0x100-0x103),硬件过滤器全通(mask=0)。同一总线上多块处于 BOOT 的板子会同时响应握手、同时擦写、同时回 ACK,ACK 冲突/交错,升级结果不可预测;也无法单独升级某一节点。在 ID 或负载中加入节点号(可由 UID 派生),过滤器只收本节点与广播 ID。
S-05严重CAN 接收/实时性device/Can.cpp:196-222, 150-163(communication_check)接收只有单个 receive_message 缓冲,ISR 每收一帧整体覆盖,计数器 can0_receive_count 只是计数;主循环取出时读到的永远是最后一帧,且计数自减在主循环、自增在 ISR,无临界区。ISR 还接收 ACK/WAIT ID——其他 BOOT 节点每 10 ms 发送的 WAIT 心跳会覆盖本该处理的数据帧。连续两帧到达(握手应答+数据、多节点心跳)就会丢帧或同一帧被处理两次;提交 26cf5af 声称的“修复BOOT丢帧”并未根治。改为环形接收队列(深度≥4,ISR 入队/主循环出队,单生产者单消费者),ISR 只接收 REQ/DATA/CANCEL。
S-06严重CAN 接收device/Can.cpp:203-212temp_message 是未初始化的栈变量,can_message_receive 只在扩展帧时写 rx_efid;收到标准帧时 rx_efid 为栈上残值。CANFD_OTA_CANCEL_ID = 0。总线上任意标准帧都可能被判成某个 OTA ID,残值为 0 时会被当成“取消 OTA”,中途终止升级。temp_message 先清零或调用 can_struct_para_init;先判断 rx_ff==CAN_FF_EXTENDED 再比较 ID;取消命令不要用 ID 0。
S-07严重故障保护整个工程(无 fwdgt/wwdgt 使用);device/Can.cpp:73;RTE/.../gd32c10x_it.c:54-98没有启用任何看门狗;CAN auto_bus_off_recovery = DISABLE 且无软件恢复;HardFault/BusFault 等全部是死循环。任何卡死(总线 bus-off、Flash 等待、异常)后设备永久停机,只能人工断电;BOOT 是最后一道防线,不能没有自恢复。启用独立看门狗并在主循环喂狗(跳 APP 前保持启用,APP 接管);开启 bus-off 自动恢复;Fault 处理改为记录原因后系统复位。
S-08严重调试代码进入发布版sys/config.h:17;sys/OTA.cpp:311-337, 324;253-280DEBUG_OTA=1 是当前发布配置。① eraseApplicationArea 在 __disable_irq() 后擦除失败分支调用 delay_1ms(5000),该延时依赖 SysTick 中断 → 永久死等;② 擦除循环每页在关中断状态下 printf;③ jumpToApplication 关闭 USART0 时钟后仍 printf(jumping3/jumping5),fputc 轮询 TBE。擦除失败时设备永久挂死(无看门狗兜底);跳转路径存在挂在 fputc 的风险(与外设时钟关闭后寄存器读值有关,需实测);关中断时间长导致 CAN 溢出。发布版置 DEBUG_OTA=0;关中断区段内禁止 printf/延时;跳转前先输出日志再关时钟。
S-09严重备份恢复sys/OTA.cpp:657-742;sys/flash.cpp:25move_app_data 用硬件 CRC 校验拷贝结果,但 RCU_CRC 时钟只在 FlashEEPROM::init() 中开启,而 flash.cpp 不在构建清单、init() 从未被调用;main.cpp:88 也忽略 move_app_data 返回值,直接跳转。CRC 外设无时钟时读回值恒定,src_crc==dst_crc 恒成立,校验形同虚设;拷贝失败也会跳转到半截镜像。另外拷贝前先擦除主区,拷贝中掉电则主区损坏,只能依赖备份再次恢复。调用前 rcu_periph_clock_enable(RCU_CRC);检查返回值,失败时进入 OTA 模式而不是跳转;备份区自身应有长度+CRC/签名头。
G-01一般升级流程sys/OTA.cpp:140-185;config.h:74超时计数:非 IDLE 状态每次 process() 只 delay_1us(100) 后 ota_time++,OTA_TIMEOUT=10000 → 实际约 1 s,与注释“10 秒”不符;delay_1us 基于空循环估算,随编译优化变化。上位机准备/擦除较慢时容易误超时;行为依赖优化级别。超时统一基于 SysTick 毫秒计数(记录时间戳比较)。
G-02一般升级流程sys/OTA.cpp:142-153, 319-329页擦除失败只发错误帧并 break,process() 仍把状态置为 RECEIVING_FIRMWARE。在擦除不完整的区域继续接收写入,产生 PGERR 或混合镜像。eraseApplicationArea 返回结果,失败则回到 IDLE。
G-03一般升级流程sys/OTA.cpp:456-457offset = receive_packet_num * 48 为 u32 乘法,可溢出回绕后通过 offset+48 ≤ APP_MAX_SIZE 检查。异常包序号会写入意外偏移。先检查 receive_packet_num < APP_MAX_SIZE/48 再计算偏移;同时要求序号单调连续。
G-04一般A/B 设计main.cpp:24-101;sys/OTA.cpp:221-234所谓 A/B 实为“主区 + 恢复区”:BOOT 从不写备份区;新固件的 CHECK 标志只能由 APP 写入(隐含约定,无文档)。若 APP 未写 CHECK,下次复位会走备份分支,用备份覆盖刚升级的新固件。升级后“回退”行为取决于 APP 是否遵守未文档化约定,容易出现“升级后重启又变回旧版”。书面定义 BOOT/APP 标志握手协议(试运行→确认→回滚计数);或真正双槽并只切换启动槽。
G-05一般跳转sys/OTA.cpp:250-271切换到 IRC8M 后未等待 SCSS 生效,也未关闭 PLL/HXTAL、未复位 RCU 寄存器;SysTick 关闭后未清除 SCB->ICSR 挂起位。APP 的 SystemInit 一般能兜底,但时钟残留状态与 APP 预期不一致时会出现难查的启动问题。调用 rcu_deinit() 或按 SystemInit 逆序复位时钟,清 PendSV/SysTick 挂起。
G-06一般CAN 发送device/Can.cpp:376-409;Can.h:5-43;Can.cpp:122CAN0_TX_IRQHandler 未在 extern "C" 中声明,C++ 名字修饰后不会覆盖启动文件的弱符号(map 中无该函数);NVIC 也从未使能 CAN0_TX_IRQn。发送队列只入队不出队:邮箱满时消息进入队列后永远不会发送,提交 d019b41 的“发送队列机制”实际不工作。在 extern "C" 中声明 TX 中断函数并 nvic_irq_enable(CAN0_TX_IRQn);或在主循环轮询出队。
G-07一般CAN-FDdevice/Can.cpp:315-334DLC 长度映射有缺档:分支为 21-23→24、29-32→32,长度恰为 24 以及 25-28 没有任何分支,直接返回失败(应为 21-24→24、25-32→32)。发送 24~28 字节报文会被静默丢弃(当前协议最大 36/64 字节,未来扩展易踩坑)。用查表把长度向上取整到 {12,16,20,24,32,48,64}。
G-08一般睡眠/唤醒sys/initial.cpp:147-177, 199-255唤醒中断中执行 Flash 擦写与 OTA_SET(未解锁,见 S-02);地址循环用 address >= SLEEP_CONFIG_START 递减,起点为 0x0801E400 时边界写法易错;enter_deep_sleep 返回后无处理(醒来靠 EXTI 软复位)。ISR 内长时间 Flash 操作;唤醒后标志写入可能失败。ISR 只置标志,Flash 操作放主流程;睡眠标志使用与 OTA 标志相同的可靠记录格式。
G-09一般代码质量sys/OTA.h:12;OTA.h:18成员 u32 ota_stateota_time(明显拼接笔误,未使用);isActive() 返回 ota_state 非零,WAITING_RANDOM 等早期状态即被视为“进入 OTA”。可读性差;延迟启动期间收到一帧握手就永久停在 OTA 循环,即使之后超时回 IDLE 也不会再跳 APP(main.cpp:61-67)。删除笔误成员;OTA 循环中 IDLE 持续超时后重新走启动判定。
G-10一般代码质量sys/OTA.cpp:18(inline)与 sys/initial.cpp:183write_ADDRESS 在两个翻译单元各定义一次(一个 inline、一个外部链接),违反 ODR。链接器选择哪份实现不确定,后续改动其中一份会产生隐蔽差异。只保留一份,放到 flash 模块并在头文件声明。
G-11一般代码质量sys/OTA.cpp:676, 709, 735 与 config.h:17move_app_data 内用 OTA_DEBUG,而全工程定义的是 DEBUG_OTA,这些日志永远不会输出。恢复失败时没有任何诊断信息。统一宏名。
A-01建议构建sys/flash.h:5-14 / sys/flash.cpp(未加入 cproject.yml)flash.cpp/flash.h 未参与构建,且 FLASH_EEPROM_OK 枚举与 config.h 的 constexpr 同名冲突;EEPROM 地址 0x0801F800 与其他区无文档说明。死代码误导维护者,一旦加入构建即编译失败。删除或整合进统一 Flash 存储模块。
A-02建议内存配置RTE/.../startup_gd32c10x.s:42;regions_GD32C103CB.h:96-97;map启动文件 Stack 0x800 + scatter ARM_LIB_STACK 0x200 + HEAP 0xC00 三套配置并存(map 中均占用 RAM),microlib 下实际 MSP 来自向量表(2 KB)。浪费 ~3.5 KB RAM,且栈大小以哪处为准不明确。只保留一处栈定义;BOOT 不用 malloc 时把 HEAP 设为 0。
A-03建议构建配置Robot_V2.csolution.yml:42-47Debug 为 optimize none,Release 为 size;ROM 使用 0x3398/0x6000(约 54%),余量可接受。BOOT 版本号写死在 config.h,未写入镜像固定位置。上位机无法读取 BOOT 版本;Debug/Release 时序差异(空循环延时)大。在固定偏移放置版本/构建信息结构;延时不依赖优化级别。
A-04建议仓库卫生.gitignore;out/、tmp/(65 个文件)out/、tmp/ 构建产物(axf/hex/map/.o/CMake 缓存等)在 .gitignore 生效前已被跟踪,至今仍在库中,每次提交都产生大量噪声 diff;compile_commands.json 含本机路径 G:/Robot_v2/BOOT。仓库膨胀、评审困难、泄露本机目录结构。git rm -r --cached out tmp(由维护者执行);发布固件改用 Release 附件。
A-05建议仓库卫生Robot_V2.cbuild-idx.yml、*.cbuild-pack.yml、.vscode/*.json、.continue/mcpServers/new-mcp-server.yaml、RTE/*.base@1.0.0生成文件与 IDE 本地配置被跟踪(.gitignore 已声明忽略但未从索引移除);.continue 下是未填写的模板。多人协作时频繁冲突。从索引移除并保持忽略;保留 cbuild-pack.yml(锁定包版本)即可。
A-06建议文档/测试仓库根目录无 README、无协议文档(帧格式、状态机、标志位定义、BOOT/APP 约定)、无主机侧工具说明、无任何测试(AES 可用 FIPS-197 向量做单元测试)。知识只在作者脑中,协议变更无法验证。补 README + docs/protocol.md(状态机图、帧表、Flash 分区表);为 AES/CRC/标志模块加主机侧单元测试。
+ +

5. 提交声明跟踪表(替代既有评审跟踪)

+ + + + + + + +
提交声明当前状态核对说明
2f48255 / 9c7145eOTA 功能测试通过;安全 OTA,AES 加密和 CRC 校验部分成立加解密与 CRC 已实现,但主密钥明文入库、无签名、ECB 模式、CRC 非 MAC(F-01、F-02);UID 读取错误(S-03)。
c7c4451支持 A/B 分区逻辑部分成立实为主区+恢复区,BOOT 不写备份区;确认依赖 APP 写 CHECK 的隐含约定(G-04)。
aefb7d4修复备份 APP 区域恢复功能部分成立恢复路径可执行,但 CRC 外设无时钟导致校验无效、返回值被忽略(S-09)。
26cf5af修复 BOOT 丢帧问题,优化标志位存储未根治单缓冲接收仍会被后续帧/心跳覆盖(S-05);标志页整页擦写断电风险仍在(S-01)。
d019b41CAN FD 协议栈优化和发送队列机制不成立(队列未生效)TX 中断函数名被 C++ 修饰、NVIC 未使能,队列不会出队(G-06);DLC 映射有缺档(G-07)。
1e6d47d完整的系统休眠和唤醒机制基本成立,有缺陷可休眠并通过 PB8 EXTI 唤醒复位;唤醒 ISR 内做 Flash 操作且未解锁(G-08、S-02)。
46d4bfeAPP 延迟启动模式支持 OTA 请求检测成立,有缺陷10 s 窗口工作正常;一旦进入早期握手状态即永久停在 OTA 循环(G-09)。
05069e3移除 CAN DMA、-Oz/ROM 护栏、-fno-exceptions成立DMA 代码已移除;Release 为 size 优化,ROM 占用 0x3398/0x6000;但 jumpToApplication 仍关闭 RCU_DMA0(无害残留)。
+ +

6. 优先级修复路线图

+ + + + +
阶段内容对应问题
P0 立即(发布前必须)发布版关闭 DEBUG_OTA;修 UID 解引用;写失败/尾帧 CRC/长度校验失败一律不置可用标志;OTA_SET 解锁/上锁统一并回读校验;move_app_data 开 CRC 时钟并检查返回值;启用独立看门狗与 bus-off 恢复F-03, S-02, S-03, S-07, S-08, S-09
P1 短期(1-2 周)接收环形队列 + 帧格式判断 + 只收本节点 ID;协议加入节点地址;标志区改双页/追加式记录;修 TX 中断与 DLC 映射;超时改为 SysTick 时间戳S-01, S-04, S-05, S-06, G-01, G-02, G-03, G-06, G-07
P2 中期(安全加固)密钥移出源码并按 UID 派生;双向 nonce 防重放;整镜像签名(ECDSA/Ed25519)+ AES-CTR/GCM;版本号防降级;开启读保护F-01, F-02
P3 持续书面 BOOT/APP 标志协议与回滚策略;清理 out/tmp 与生成文件;README + 协议文档;AES/CRC 单元测试;统一栈堆配置与调试宏G-04, G-05, G-08-G-11, A-01-A-06
+ +

7. 总结评分

+ + + + + + + + +
维度评分 /10说明
架构清晰度6模块划分简单明了,分区与状态机易懂;BOOT/APP 约定未文档化
升级可靠性4有恢复区与延迟启动;写失败/断电/残缺镜像均可能被激活
安全性3有加密握手的意图,但密钥入库、无签名、ECB、可重放
通信与实时性5CAN-FD 配置正确、BRS 生效;单缓冲接收、无寻址、发送队列失效
故障保护3无看门狗、无 bus-off 恢复、Fault 死循环
代码质量6注释充分、可读;存在 ODR 重复定义、宏名不一致、死代码
构建与仓库卫生5csolution 配置规范、ROM 余量足;构建产物与生成文件入库、无 README/测试
综合5.0功能链路已打通、可用于台架;量产前须完成 P0+P1,面向现场设备须完成 P2
+

本报告仅新增于 docs/review/,未修改任何源码。由 Hark 生成。

+ \ No newline at end of file