Skip to content

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 出现在三个层次,全部使用同一套参数:

使用点位置保护对象
帧内载荷 CRCapp.img 帧头偏移 12挡住 WiFi / 串口 / TCP 传输过程中的位翻转
升级记录整帧 CRCW25Q128 扇区 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:

CRC32 的本质:模 2 除法求余数

这里的"除法"是模 2 除法:不借位、不进位,加减法全部退化为异或。它不需要真的做除法——硬件上就是一排移位寄存器加几个异或门,软件上就是"移位 + 条件异或"。

2. 反射多项式:固件里的 0xEDB88320 ​

教科书上的 CRC-32 多项式写作 0x04C11DB7(最高位在前)。但工程实现普遍采用它的反射(bit-reversed)形式 0xEDB88320,配套把"左移"改成"右移":

CRC32 逐位算法流程

这样做的好处是直接适配"低字节在前"的实际传输顺序,省掉逐字节的位序翻转。固件里那行 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 文件中声明:

c
// 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.pyzlib.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 在链路上出现了两次,覆盖范围刻意不同:

IAP 链路里的双层 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 技能辅助生成