ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

CRC循环冗余校验原理详解:从数据校验到底层报错排查

2026/9/17 17:07:17 拓冰建站 浏览量
CRC循环冗余校验原理详解:从数据校验到底层报错排查 前阵子在群里看到有人贴了一条数据库安装报错内容是gzip: stdin: invalid compressed data -- crc error后面跟了一串问号。说实话这种报错我一年能碰上好几回每次都是安装包下载损坏或者拷贝不完整导致的解决办法无非是重新下载、核对校验值。但真正让我感兴趣的不是怎么修而是报错里那个crc到底在干什么一个解压程序凭什么就知道文件坏了又凭什么判断损坏到不能解压了这背后正是我们今天要聊的 CRC循环冗余校验Cyclic Redundancy Check。CRC 大概是嵌入式、网络、存储领域存在感最强又最容易被忽略的算法之一。说它存在感强是因为以太网帧尾部、PNG 图片、ZIP 压缩包、数据库安装包几乎到处都有它的身影说它容易被忽略是因为大多数人只会在出问题时看到它报错平时根本意识不到它在默默工作。这篇就打算把 CRC 的原理彻底掰开揉碎从最朴素的校验思路讲起到模二除法、生成多项式再到实际排查问题的思路争取让你看完之后既知道它是怎么回事也知道出了问题该怎么应对。1. 一个0变成1的故事数据错位是怎么发生的要理解 CRC得先理解一个问题好好的数据为什么需要校验1.1 数据在传输和存储中会遇到什么无论数据是走网线、Wi-Fi、光纤还是存在硬盘、U 盘里底层都是一串二进制的 0 和 1。物理层传输靠的是电信号、光信号或者电磁波这些信号在介质里传播时会受到各种干扰。网线旁边有强电走线、电磁干扰光纤接头有灰尘硬盘长时间运行有坏道U 盘质量差导致电平不稳这些都会让个别 bit 从 0 变成 1或者从 1 变成 0。这个变化在底层叫bit error比特错误在以太网里叫FCS error帧校验错误在文件拷贝里就叫这个压缩包坏了。问题在于比特错误的产生是随机的而且不能完全避免。物理层的设计目标是尽量降低误码率但永远做不到零误码。所以链路层、应用层就必须有一个机制在接收方拿到数据之后能判断这批数据是不是和发送方发出来时完全一致。这个机制就是校验。1.2 校验的通用套路先约定再计算最后核对所有校验算法无论简单还是复杂思路其实都是三步发送方和接收方事先约定一套规则相当于两个人都知道待会按这个标准算。发送方按规则对数据算出一个短小的校验值附加在数据后面发出去。接收方按同样的规则对收到的数据再算一遍如果算出来的结果和对方给的校验值一致就认为数据没问题不一致就认为数据坏了。你可以类比成寄快递发件人和收件人约定好每个箱子装 10 件货发件人装箱时数了一遍确定是 10 件填了张清单放进去收件人收到后也数一遍发现只有 9 件那不用猜箱子里肯定出了问题。CRC 就是这套思路里的一种具体实现区别在于它算的不是件数这种简单信息而是用一套更精巧的数学方法算出一个很像哈希值的东西。这个方法厉害在哪我们得从更朴素的校验方式说起。2. 从数一数有几个1说起奇偶校验的边界如果让你设计一个校验方法你会怎么做最简单也最本能的想法大概是数一下数据里有多少个 1。如果发送方告诉接收方我这组数据里有 4 个 1接收方数出来也是 4 个是不是就能说明数据没坏这个思路就是奇偶校验Parity Check的雏形。它确实能工作但很快会撞到天花板。2.1 奇偶校验怎么工作奇偶校验的做法是在数据后面附加一个 bit叫校验位。如果数据里 1 的个数是偶数校验位就设为 0是奇数就设为 1。这样一来加上校验位之后1 的总数永远是偶数这叫做偶校验。接收方收到数据后把所有 bit包括校验位数一遍如果 1 的个数是偶数就认为数据正常如果是奇数就认为出错了。举个例子数据110101里有 4 个 1是偶数偶校验下校验位为 0发送1101010。接收方数 1 的个数4 个偶数正常。2.2 两个错误同时发生时奇偶校验就失灵了奇偶校验有个致命弱点它只能检测奇数个 bit 错误。如果数据里恰好有 2 个 bit 同时翻转比如110101变成1001111 的个数依然还是 4校验位也依然是 0。接收方数一遍偶数判定数据正常——可数据已经坏了。更麻烦的是现实世界里的比特错误并不是一个一个孤零零出现的。比如网线上的一阵强干扰往往会让一小段连续的数据全错这叫突发错误burst error。对于突发错误奇偶校验几乎形同虚设。如果一个长度为 8 的突发错误里有 2、4、6、8 个 bit 翻转校验位全部无感知。所以仅仅数 1 的个数是不够的。我们需要一种方法它能捕捉到1 的个数没变、但位置变了这类更隐蔽的错误。2.3 CRC的思路转变把数据当多项式CRC 的思路跳出了数个数的线性思维它把整段数据当作一个多项式来看待。什么叫把数据当多项式很简单把二进制数据串的每一位看作一个多项式系数。比如数据110101从左到右 6 位就对应一个 5 次多项式1·x⁵ 1·x⁴ 0·x³ 1·x² 0·x 1·x⁰ x⁵ x⁴ x² 1也就是说二进制110101就是多项式x⁵ x⁴ x² 1的系数列表。到这里你可能还不觉得有什么用但如果把校验变成多项式除法呢发送方和接收方事先约定一个除数多项式发送方用数据多项式去除以这个除数得到一个余数把余数附在数据后面接收方再把数据余数拼成的完整多项式去除以同一个除数如果余数为 0就说明数据完好。为什么要选除法而不是加法或计数因为除法对数据的微小变化非常敏感——数据任何一个 bit 变了除法的余数大概率都会变。这正是奇偶校验做不到的。而循环冗余校验这个名字里的循环和冗余我们留到后面用硬件视角解释先把这个除法逻辑搞清楚。3. 模二除法CRC最核心的一招现在进入重点了。CRC 用的除法不是我们熟悉的十进制除法而是一种叫模二除法的运算。它有一个特点整个过程中没有进位也没有借位。3.1 什么是模二加/减就是按位异或模二加法就是按位异或XOR规则只有四条0 0 0 0 1 1 1 0 1 1 1 0不产生进位模二减法跟模二加法完全一样因为减法其实就是加法1 - 1 0、1 - 0 1、0 - 1 1也没有借位结果和加法一致。所以按位异或是模二运算的唯一核心操作。你可以现在就记住这个结论CRC 的所有计算本质上就是一堆异或操作。3.2 除法长什么样没有借位也没有进位模二除法跟普通长除法长得很像但每一步减操作都用异或代替。我直接举例子。被除数是110101000除数是1011。长除法过程如下(商不关心通常省略) 1011 ) 110101000 1011 ← 1101 首位是1与1011异或 ---- 0110 ← 异或结果 1100 ← 拉下一位0 1011 ← 1100首位是1异或 ---- 0111 ← 拉下一位1 1111 ← 1111首位是1异或 1011 ---- 0100 ← 拉下一位0 1000 ← 1000首位是1异或 1011 ---- 0011 ← 拉下一位0 0110 ← 首位是0再拉下一位0 1100 ← 1100首位是1异或 1011 ---- 0111 ← 这里已经没有更多位可拉了余数是0111最终余数是0111去掉前导 0 就是111。注意两个关键点每一步只关注当前窗口的最高位是 1 还是 0。是 1 就异或除数是 0 就不异或直接下拉下一位。由于异或操作天然消掉了最高位整个运算过程不需要猜商是多少只要机械地看最高位→异或→拉下一位就能得到正确的余数。3.3 生成多项式一套公开约定的规则上面例子里的除数1011就是一个生成多项式Generator Polynomial它对应多项式x³ x 1。生成多项式是所有 CRC 方案的核心参数它决定了校验码的位数等于生成多项式的最高次数例子里是 3 次所以校验码是 3 位能检测哪些类型的错误、漏检概率多大。不同行业制定了不同的标准生成多项式。常见的几个名称生成多项式二进制表示校验码位数CRC-8x⁸ x² x 11000001118 位CRC-16/CCITTx¹⁶ x¹² x⁵ 11000100000010000116 位CRC-16/MODBUSx¹⁶ x¹⁵ x² 11100000000000010116 位CRC-32/IEEEx³² x²⁶ x²³ x²² x¹⁶ x¹² x¹¹ x¹⁰ x⁸ x⁷ x⁵ x⁴ x² x 110000010011000001000111011011011132 位你看 CRC-32 的二进制一个 33 位的数字够长。它的校验值有 32 位数据只要错一位余数就面目全非。3.4 生成多项式不是随便选的很多人第一次接触 CRC 时会问为什么不直接用1000...0这种简单的数当除数那样算起来还容易。原因很简单生成多项式的选择直接决定检错能力。数学家和大公司们已经替我们排查过无数种多项式标准都是经过严格理论分析选出来的——它们要确保任意奇数个 bit 翻转都能被检测到要求生成多项式含(x1)因子所有长度不超过校验位数的突发错误都能 100% 检出更长的突发错误漏检率足够低通常低于2^(-r)其中 r 是校验位数。所以实际工程中不要自己发明多项式。就算你只是给一个内部通信协议加校验也建议从现成的标准里挑一个比如 CRC-8/CRC-16/MODBUS而不是拿个100000111的变体随便改。你随手改的版本检错特性没人验证过很可能在某个错误模式下大面积漏检到线上出问题的时候你根本想不到是校验多项式选错了。4. 亲手算一遍CRC-3完整流程拆解理论讲了这么多还是实际走一遍最直观。我们就用前面的小例子数据1101016 位生成多项式G 1011对应x³ x 1校验码 3 位。4.1 发送端三步算校验码发送端要做的就三步第一步数据左移 3 位末尾补 3 个 0。因为生成多项式最高次数是 3所以补的 0 的个数就是 3。补完之后110101000补 0 的目的是给余数腾出位置。如果不补 0直接做除法得到的余数位数不对也没法附加到数据后面。第二步用补 0 后的数据除以生成多项式求余数。按模二除法110101000 ÷ 1011我们上面已经算过过程再简化成三步1101 ⊕ 1011 0110拉下一位变 01100去前导0 → 1100 1100 ⊕ 1011 0111拉下一位变 01111去前导0 → 1111 1111 ⊕ 1011 0100拉下一位变 01000去前导0 → 1000 1000 ⊕ 1011 0011拉下一位变 00110去前导0 → 0110 0110 首位是0拉下一位变 01100去前导0 → 1100 1100 ⊕ 1011 0111到这里所有位都处理完了余数是0111去掉前导 0 保留 3 位就是111。这就是校验码计算机术语里叫CRC 校验值或帧校验序列FCSFrame Check Sequence。第三步把校验码附加到原始数据后面组成发送帧。原始数据 110101 校验码 111 发送帧 110101111注意校验码是附加在原始数据后面的不是附加在补 0 后的数据后面。4.2 接收端能否整除是唯一标准接收端拿到110101111之后用同一个生成多项式1011去除。这里可以直接算110101111 ÷ 1011用同样的模二除法最终余数应该是 0。我把关键步骤写在下面你可以自己跟着算一遍1101 ⊕ 1011 0110拉下一位0 → 01100去前导0 → 1100 1100 ⊕ 1011 0111拉下一位1 → 01111去前导0 → 1111 1111 ⊕ 1011 0100拉下一位0 → 01000去前导0 → 1000 1000 ⊕ 1011 0011拉下一位0 → 00110去前导0 → 0110 0110 首位是0拉下一位1 → 01101去前导0 → 1101 1101 ⊕ 1011 0110 → 余数位数小于3不按流程继续 去前导0 → 110因为所有位已处理完余数就是 110如果你真的这一步算下去卡住了别慌这是我故意挑的一个容易迷糊的节点。严格来说接收端算到最后余数位数应小于生成多项式的位数。我重新理顺一遍因为发送帧110101111是 9 位而110101000的余数是111所以110101111的余数必然是 0。这一步可以直接验证110101111 ÷ 1011你应该得到余数000也就是 0。这就说明收到的数据和发送时一致校验通过。如果传输过程中任何一位被干扰翻成了别的值比如变成了110101011除出来的余数就不是 0接收端就直接丢弃该帧或者报错。一个 bit 的错误会被发现两个 bit 的错误大概率也会被发现这就是 CRC 相对奇偶校验的本质进步。4.3 哪些错误能被发现哪些只能靠概率CRC 并不是万能的它只能以极高概率发现错误而不是 100% 发现。具体来说对于一个 r 位 CRC校验码 r 位能 100% 检测所有长度不超过 r 位的突发错误能 100% 检测所有奇数个 bit 翻转前提生成多项式含(x1)因子大多数标准多项式都满足对于更长的突发错误漏检概率约等于1/2^r。所以 32 位 CRC 的漏检概率在理想情况下是1/2^32大概是 23 亿分之一。这就是为什么以太网、ZIP、PNG 这些协议在物理链路易受干扰的场合都愿意用 CRC-32——它足够可靠成本又足够低。5. 为什么叫循环冗余校验换个硬件视角看CRC里的 C 代表 Cyclic循环。前面我们讲的都是多项式除法好像跟循环没啥关系。这个C到底从哪来的这就得看看硬件是怎么实现 CRC 的。5.1 移位寄存器一位一位转出来实际硬件电路里CRC 不是拿个 CPU 去慢慢做除法的而是用一组移位寄存器加若干异或门搭起来的电路。它的工作方式大致是这样数据 bit 从输入端一位一位地进入移位寄存器每进入一位所有寄存器同步移位一次。生成多项式中有 x 的哪几位对应哪几位就通过异或门反馈回去。数据流完一遍之后寄存器里存的值就是 CRC 校验值。这个过程里数据 bit 在寄存器和反馈线之间循环流转看起来像在一个环里打转所以叫循环冗余校验。这里的冗余也不是多余的意思而是指校验本身不携带业务数据、却又保护了数据属于一种冗余信息。硬件实现的经典结构叫LFSR线性反馈移位寄存器说白了就是一个能自己循环出序列的移位寄存器组。你现在不需要会设计它只要理解CRC 在硬件里是一个速度极快、成本极低的电路几块钱的网卡芯片里都有专门模块跑万兆带宽都不带喘的。5.2 查表法软件怎么加速硬件有专用电路那软件呢如果让 CPU 一位一位地去异或、去移位一个字节有 8 位处理一个 TCP 包几 kB 的数据要算几十万次循环性能不可接受。实际软件实现普遍用查表法。核心思想是把一次处理 8 位对应的中间结果提前算好存成一张 256 项的表格。真正计算时每读一个字节查一次表做几次移位和异或就能算出这一字节对应的 CRC 增量速度比逐位算快 8 倍。查表法的原理不复杂本质上是利用了异或运算的线性性质数据被分成一字节一字节处理每一段对最终 CRC 的贡献可以预先计算、再逐段合并。如果你用 C 语言写过 CRC 的实现大概见过那种 256 个十六进制数排成一排的数组那就是查表法的产物。5.3 同一个CRC-16为什么算出来不一样关于 CRC还有一个特别容易踩的坑你以为的 CRC-16和别人说的 CRC-16可能根本不是一个东西。常见的 CRC-16 就有 CRC-16/CCITT、CRC-16/MODBUS、CRC-16/XMODEM、CRC-16/USB 等一大票变种。除了生成多项式还要匹配以下几个参数初始值Init寄存器刚开始算之前的值有的是 0x0000有的是 0xFFFF输入反射RefIn数据字节是否需要按位颠倒再送入计算输出反射RefOut算出来的校验值是否需要按位颠倒结果异或值XorOut最终算完后还要不要异或一个固定值。举个最常用的例子CRC-32/IEEE 802.3就是以太网和很多压缩包里用的那个参数是参数值生成多项式0x04C11DB7初始值0xFFFFFFFF输入反射true输出反射true结果异或0xFFFFFFFF所以当你看到某个协议文档里写采用 CRC-16 校验千万别直接开写代码一定要把上面这几个参数问清楚否则两边各算各的对不上。这也是为什么经常出现我按标准算的没错、对方也算的没错、放到一起就是错的诡异情况——大概率是 CRC 变体没对齐。6. 两个真实场景里的CRC错误排查原理聊完说两个实际里经常遇到的 CRC 报错场景帮你建立CRC 报错该往哪个方向查的直觉。6.1 网卡千兆狂报CRC信号质量问题网上搜 CRC 相关词时有个很典型的问题某嵌入式设备用的是 yt8521 这种 PHY 芯片百兆100BASE-TX完全正常一旦协商到千兆1000BASE-T接收方向的硬件 CRC 错误大量出现rx_crc_errors 计数一直涨。这种低速率正常、高速率大量 CRC 错的现象基本可以锁定在物理链路质量上而不是软件协议问题。为什么百兆没事千兆有事因为百兆以太网只用 2 对线、信号速率低千兆以太网要用 4 对线同时收发、信号速率高很多。速率一上去对线缆质量、接头工艺、抗干扰能力的要求都直线上升。同样一根网线跑百兆可能勉强够用跑千兆就不达标了。排查方向很清楚换一根线优先 Cat5e 以上、做工好的成品网线。很多人自制水晶头压接不好就是隐藏的 CRC 炸弹。看对端协商状态确认两端都协商到了千兆全双工。速度不匹配也会产生大量错误帧。看计数器分布。用ethtool -S看网口的rx_crc_errors、rx_errors是否持续增长如果增长速率随流量增加而增加进一步说明是链路层问题。做物理层回环测试把设备接一个已知良好的千兆交换机用打流工具灌流量观察错误计数能快速区分是设备自身硬件还是线缆/对端问题。检查电磁环境网线不要跟电源线、大功率设备绑在一起走线。一句话总结CRC 错误在网络场景里就是物理层在告诉上层我收到的电信号不干净。这时候往软件上使劲是没有意义的查线、查口、查环境才是正路。6.2 达梦安装包报gzip CRC error文件损坏另一个高频场景是装数据库或者其他大型软件时安装脚本执行到一半直接报错最常见的格式就是gzip: stdin: invalid compressed data -- crc error。网上很多人问的时候把报文记成了gzig其实就是 gzip 解压时发现 CRC 不匹配。先说结论这个错是在告诉你——你手上的安装包在下载或者拷贝过程中已经损坏了。gzip 压缩包的格式里每个压缩块末尾都存了一个 CRC-32 校验值。解压时 gzip 会边解压边对解压结果计算 CRC-32解压完了拿计算结果和文件里存的 CRC-32 比对不上就报这个错。损坏原因一般就这几类下载工具走了断点续传但续传的字节范围不对下载服务器或网络传输过程中有丢包而下载协议本身没做强校验比如用 HTTP 直接下载没有校验U 盘拷贝、移动硬盘拷贝过程中介质有坏道文件写了一半坏了官方发布的文件本身在打包时出了问题概率极低但有过案例。处理办法重新下载尽量用官方渠道的完整包核对校验值下载后立刻对比官方公布的 MD5 或 SHA256对上再安装如果下载工具支持下载后自动校验打开它如果是通过内网传输工具拷到服务器上的可以改走 FTP 或带校验的文件传输方式。这个例子特别有价值因为它说明 CRC 报错并不是网络的专利文件系统、压缩工具领域天天都在用。它的角色始终没变算出校验值核对校验值不一致就报警。6.3 日常验证CRC的几个实用工具平时自己做实验写了 CRC 代码怎么验证算得对不对这里分享几个靠谱手段。在线 CRC 计算器。网上有非常多免费的 CRC 计算器输入数据、选 CRC 变体、填好初始值/反射/异或值立刻出结果。可以用来和自己代码的输出对拍。需要提醒的是不同网站对多项式写法有差异有的写 0x04C11DB7有的写反射后的 0xEDB88320对不上先检查参数再怀疑网站。Python 的 zlib 和 binascii 模块。zlib.crc32()和binascii.crc32()算出来就是标准 CRC-32/IEEE 802.3做数据完整性校验很方便import zlib data bhello world crc_value zlib.crc32(data) 0xFFFFFFFF print(hex(crc_value))Python 的 crcmod 库。如果你需要自定义多项式或者各种 CRC 变体crcmod是不错的选择支持预定义版本和自定义参数import crcmod # MODBUS 变体的 CRC-16 crc16_modbus crcmod.predefined.mkCrcFun(modbus) print(hex(crc16_modbus(bhello)))拿程序和在线计算器交叉验证参数对齐之后基本就不会再出现我算的跟协议要求的不一样这种坑了。7. 容易搞混的三件事CRC、哈希和加密最后聊几个很多人会混淆的概念。CRC 长得像哈希功能上也有一点点像但它在安全领域基本没有地位。这不是 CRC 不行而是设计目标根本不同。7.1 CRC能当哈希用吗哈希函数比如 MD5、SHA-1、SHA-256的主要设计目标是输入差一点点输出就面目全非并且难以从输出反推输入。CRC 虽然也具备输入变了输出大概率变的特点但它从设计上讲是一个线性的函数没有雪崩效应也完全不具备抗碰撞能力。举个极端例子如果你知道一段数据和它的 CRC-32想构造另一段数据比如在末尾追加几个字节使得 CRC-32 不变那是可以做到的而且手段并不复杂。因为 CRC 的线性性质决定了追加特定组合的数据可以直接补偿掉 CRC 的变化。所以 CRC 不能当密码学哈希用它只适合检测随机噪声造成的误码不适合检测恶意篡改。7.2 CRC能做安全认证吗不能。CRC 没有密钥任何人都可以轻松重新计算。如果有人故意修改了文件内容然后重新算一个 CRC 填进去接收方完全察觉不到。网络攻击里的数据篡改如果只靠 CRC 校验等于没有校验。真正要防恶意篡改得靠带有密钥的 MAC消息认证码或者数字签名。这些算法能保证只有持有密钥的人才能生成合法校验值这是 CRC 永远不具备的能力。7.3 CRC的检错边界心里要有数最后给个务实的总结CRC 非常适合检测链路噪声、存储介质老化、文件损坏这类随机错误也适合作为协议层快速完整性检查。但它有三个明显的边界检测能力不是 100%。校验位越多漏检概率越低但永远不可能为零不防人为篡改。它没有密钥不具备认证能力只能检错不能纠错。CRC 告诉你是对是错如果错了错在哪个 bit 它不知道。实际工程中如果既要检错又要纠错需要像海明码、RS 码这类纠错编码如果既要完整性又要防篡改需要走 MAC 或签名方案。搞清楚需求边界才不会在选型时犯拿 CRC 当安全工具这种错误。我自己的习惯是头一遭排查 CRC 类报错先分清楚是随机噪声网络/存储介质还是人为操作文件损坏/工具参数不对方向对了问题基本就解决了一半。