LV055-CRC32校验原理
前置阅读:LV050-app在线升级原理与流程设计 交代了升级链路的整体骨架。本篇讲清楚骨架里第一道防线——CRC32——是什么、为什么每个环节都离不开它,以及固件里那十几行逐位代码到底在算什么。下一篇 LV060-Ed25519签名校验原理 会说明 CRC32 挡不住的那类威胁。
一、 CRC32 是什么
1. 一句话定义
CRC(Cyclic Redundancy Check,循环冗余校验)是一种基于除法求余数的检错码:发送方把数据看作一个巨大的二进制数,用双方约定的"除数"(生成多项式)去做模 2 除法,得到的 32 位余数就是 CRC32,把它附在数据后面一起传输;接收方对收到的"数据 + 余数"用同一个除数再除一次,余数不为零就说明传输出错了。
CRC32 的名字里两个关键词各管一件事:
- Cyclic / Redundancy:余数会随数据的每一比特变化而"循环滚动",相当于把整段数据的信息压缩进 32 位——这 32 位是数据的高度冗余摘要;
- Check:它只负责"发现错误",不负责"纠正错误",更不负责"防人篡改"(这是它与签名最本质的区别,见第五节)。
2. IAP 升级链路里的三处用点
在本工程的升级链路中,CRC32 出现在三个层次,全部使用同一套参数:
| 使用点 | 位置 | 保护对象 |
|---|---|---|
| 帧内载荷 CRC | app.img 帧头偏移 12 | 挡住 WiFi / 串口 / TCP 传输过程中的位翻转 |
| 升级记录整帧 CRC | W25Q128 扇区 2/3 的 upd 记录 | 挡住镜像在外部 Flash 槽里的存储损坏 |
| 参数 / 公钥记录 CRC | 扇区 0/1 与 132/133 的 64B 记录信封 | 挡住记录内容损坏导致的误读 |
二、 为什么需要 CRC32
1. 误码从哪来
升级镜像要走的路并不干净:
(1)传输链路:ESP8266 把 56KB 镜像切成 512 字节一块经 WiFi 吐给 UART3,再由串口中断逐字节搬进 MCU——电磁干扰、信号完整性、缓冲竞争都可能让某个字节翻转;
(2)存储介质:镜像落在外部 SPI Flash 上,掉电瞬间可能写入半截,Flash 比特单元长期使用也可能位衰减;
(3)拷贝路径:go flash 时数据还要从外部 SRAM 搬进内部 Flash,任何一次搬运都多一次出错机会。
一个 3.7 万字节的镜像里哪怕只错 1 个比特,落到代码段上就可能是一条变义的指令——升级失败的表象千奇百怪,根因往往就是这一位。所以每个环节都必须当场验,不能攒到最后。
2. 与奇偶校验、累加和的对比
| 方案 | 原理 | 检不出什么 | 适用场景 |
|---|---|---|---|
| 奇偶校验 | 让 1 的个数为奇/偶 | 任何偶数个位同时出错 | 串口单字节 |
| 累加和 | 所有字节求和取低位 | 两个字节的错误互相补偿、字节换序 | 简单短报文 |
| CRC32 | 模 2 除法余数 | 漏检概率约 2^-32 量级 | 长数据块完整性 |
累加和的致命弱点在于"补偿":一个字节加了 3、另一个字节减了 3,累加和纹丝不动。CRC 的除法把每个字节的影响扩散到全部 32 位上,位置不同的错误会得到完全不同的余数,补偿巧合在数学上被压到 2^-32 量级。
3. CRC32 的检错能力
对于 CRC-32 这一类生成多项式,可以证明它必然能检出:
(1)所有 1 位错误;
(2)所有 2 位错误(在一定长度范围内);
(3)所有长度 ≤ 32 位的连续突发错误;
(4)所有奇数个位错误。
更长的突发错误漏检概率约为 2^-32——对一次升级传输来说足够了。它检得住"误码",但注意:这些性质只对随机错误成立,对"知道算法、蓄意伪造"的攻击者无效,这正是下一篇要解决的问题。
三、 计算原理
1. 模 2 除法:CRC 的数学模型
把消息比特流 M(x) 左移 32 位(相当于腾出余数的位置),除以双方约定的 32 位生成多项式 G(x),所得 32 位余数即为 CRC:
这里的"除法"是模 2 除法:不借位、不进位,加减法全部退化为异或。它不需要真的做除法——硬件上就是一排移位寄存器加几个异或门,软件上就是"移位 + 条件异或"。
2. 反射多项式:固件里的 0xEDB88320
教科书上的 CRC-32 多项式写作 0x04C11DB7(最高位在前)。但工程实现普遍采用它的反射(bit-reversed)形式 0xEDB88320,配套把"左移"改成"右移":
这样做的好处是直接适配"低字节在前"的实际传输顺序,省掉逐字节的位序翻转。固件里那行 0xEDB88320 与 PC 侧 zlib.crc32 使用的多项式是同一个,只是写法不同。
3. 初值与终值取反
还有两个容易被忽略的参数:
(1)初值 0xFFFFFFFF:全 0 初值有个缺陷——开头连续多个 0 字节不会改变余数,前导零等于没校验;全 1 初值让每一段前导数据都参与运算;
(2)终值取反 ^= 0xFFFFFFFF:与初值配对,保证"全 0 数据"和"无数据"算出不同的 CRC,也避免了余数全 0 的退化情形。
这四个参数(多项式、初值、终值取反、反射)合起来俗称 zlib 参数。通信两端只要有一处参数不同,算出来的 CRC 就完全对不上——所以参数一致性是第一件要对齐的事,见下一节。
四、 工程实现解析
1. upd_crc32()
逐位版 CRC32 的完整实现如下,该函数在 flash-app/Drivers/BSP/Update/upd.h 文件中声明:
// flash-app/Drivers/BSP/Update/upd.c
uint32_t upd_crc32(const uint8_t *data, uint32_t len)
{
uint32_t crc = 0xFFFFFFFFU; /* 初值:全 1 */
uint32_t i;
for (i = 0U; i < len; i++)
{
uint32_t bit;
uint8_t b = data[i];
crc ^= b; /* 字节异或进低 8 位 */
for (bit = 0U; bit < 8U; bit++) /* 每字节 8 轮移位 */
{
crc = ((crc & 1U) != 0U)
? ((crc >> 1U) ^ 0xEDB88320U) /* 移出的 1 回头咬多项式 */
: (crc >> 1U); /* 移出的 0:只移位 */
}
}
return crc ^ 0xFFFFFFFFU; /* 终值取反 */
}【函数作用】
对任意长度数据计算 zlib 参数的 CRC32。逐位实现不占查表空间——128KB 的镜像表驱动要 1KB 表项,逐位只要十几行代码,对 ROM 紧张的 bootloader 是更合适的取舍。
【参数含义】
data:数据首地址,可以是 Flash 指针也可以是 RAM 缓冲;len:字节数。
【返回值】
32 位 CRC 校验值。相同内容必得相同值;内容任何一比特不同,值几乎必然不同。
对照第三节的流程图看,crc ^= b 对应"异或进低 8 位",内循环 8 次对应"每字节的 8 轮移位判断",最后一行对应"终值取反"——图与代码一一对应。逐位版处理 56KB 镜像约需几十毫秒,放在主循环里完全可接受。
2. 三处实现,一套参数
参数一致性靠"约定 + 对表"维持,本工程四个 CRC 使用点的参数完全相同:
| 实现位置 | 写法 | 用途 |
|---|---|---|
tools/pack_iap.py | zlib.crc32()(标准库) | 打包时算载荷 CRC |
atk-f103/Drivers/BSP/Iap/iap_crc32.c | 逐位实现 | bootloader 侧收帧与搬运校验 |
flash-app/Drivers/BSP/Update/upd.c | 逐位实现(上面这份) | app 侧下载校验与记账 |
两侧 param.c / upd.c 记录信封 | 复用上述实现 | 记录自校验 |
任何一侧改了参数(比如换了初值),另一侧的校验会立刻全部失败——这是"帧格式常量两侧严格一致"约束的一部分。
3. 双层防线:帧内 CRC 与记录 CRC
CRC 在链路上出现了两次,覆盖范围刻意不同:
- **层 1(帧内载荷 CRC)**只覆盖 app.bin 本体,随帧传输,在下载完成那一刻比对——它回答"从 PC 到 SRAM 这段路上有没有出错";
- **层 2(整帧 CRC)**在镜像落进外部 Flash 槽时算好存进升级记录,
go flash搬运前重新计算比对——它回答"在槽里躺的这段时间有没有坏"。
两层各司其职:只做层 1,传输后到搬运前的存储窗口就是裸奔;只做层 2,传输误码会被误判成"存储坏了",排查方向都会带偏。
五、 CRC32 的能力边界
最后把边界说清楚,避免对它抱不切实际的期待:
(1)只检错,不纠错:CRC 发现余数不为零只能整体丢弃或要求重传,定位不了错在哪一位;
(2)不防蓄意篡改:CRC 没有密钥,算法和参数完全公开。攻击者改动载荷后用同一个多项式重算一遍,把新余数填回帧头,接收端校验照样通过——CRC 证明的是"字节没变错",证明不了"字节是谁给的";
(3)不是摘要:32 位余数会被哈希碰撞式的构造方法定向命中,它从来就不是为对抗"聪明对手"设计的。
一句话总结:CRC32 管"意外",不管"故意"。当升级镜像要经 WiFi 从局域网飞过来时,"故意"就成了必须面对的威胁——下一篇文章的 Ed25519 签名校验接手的就是这一段。
本文档由 markdowncli 技能辅助生成