CRC32 自包含退化现象分析

我的好友 fuzhe 在阅读 LevelDB 源码时,发现了一个有趣的细节:系统在存储 CRC 校验码时,并不直接使用计算出的值,而是要先做一个看似"多余"的 mask 操作。这个操作包括右旋转 15 位和加上一个神秘的常数 0xa282ead8ul
。
代码注释提到这是为了解决"对包含嵌入式 CRC 的字符串计算 CRC 是有问题的",但到底什么问题?为什么位操作能解决它?这个设计背后隐藏着什么原理?
今天我们带着这些疑问,开始一场 CRC 的探索之旅。一步步揭开 CRC 的数学本质,理解"自包含退化"现象,最终看到 LevelDB 工程师们如何用优雅的数学技巧解决了这个深刻的问题。
什么是 CRC:从自然数除法到校验码
传输数字 1234 时,如何检测错误?用除法取余数:
余数 70
就是 1234
的指纹。
校验原理:
发送:
数据=1234, 校验=70
接收:重新计算 received % 97 的余数
余数不是 70 → 检测到错误
判断收到的消息正确性的方式是:
这里有个重要的点:余数相等不能 100% 保证数据正确(可能有碰撞),但余数不等就可以 100% 确定数据有错误。
例子:
另外,为了防止消息体和校验值一样,CRC 在定义的时候还提出了一点,就是先要乘以一个比较大的数字,来保证所得余数跟消息本身不一样:对于所有输入数字,先放大:
碰撞概率:使用质数 97 作除数,碰撞概率约 1/97 ≈ 1%。
以上就是 CRC 校验的核心思想:
用固定除数计算余数作为"指纹"
这些原理和标准 CRC 完全一致。唯一区别是 CRC 使用二进制多项式代替十进制数字,使用异或代替普通加减法法。理解了这个基础,接下来看看如何正式的定义 CRC 算法.
从自然数四则运算到 GF(2)多项式的四则运算
GF(2) 运算:二进制世界的数学
在二进制世界里,我们就只有两个数字:0 和 1, 即 GF(2), 或 Galois Field of 2 elements. 因为只有 0 和 1, 这里的运算规则可以很简单, 但又和我们日常使用的加减乘除非常相似(符合交换律分配律结合律等):
01 的加法规则(相当于异或 XOR):
01 的减法规则(同样是异或):
可以看出, 01 的世界里加法和减法相同: x+1 == x-1
, 都对应异或操作:a + b → a ^ b
01 的乘法规则:
可以看出 01 世界中的乘法就是 AND
操作:a × b = a & b
01 的除法规则:
看到这里我们发现:GF(2) 也是四则运算,和整数的计算规则基本一样,就是加法变成了异或。
还有一个有趣的地方:GF(2) 计算是封闭的 - 不管怎么算,结果永远只有 0 或 1,绝不会蹦出别的数字。一个完美的封闭系统。
多项式的四则运算
而一个关于 x 的多项式, 它的加减乘除运算是定义为:
加减法: 等幂项的系数相加相减;
乘法: 一个多项式的每一项跟另一个多项式的每一项相乘, 再把结果中的等幂项合并;
除法是乘法的逆运算;
从这里我们看出, 要让多项式的四则运算成立, 前提是它的系数必须可以进行。加减法和乘法运算, 换句话说, 任何满足加减乘的东西都可以作为多项式的系数。GF(2)就可以。
不论是整数还是 GF(2) 做多项式的系数, 多项式的四则运算都满足下面这些要求:
加法交换律:P(x) + Q(x) = Q(x) + P(x)
乘法交换律:P(x) × Q(x) = Q(x) × P(x)
加法结合律:(P + Q) + R = P + (Q + R)
乘法结合律:(P × Q) × R = P × (Q × R)
分配律:P × (Q + R) = P×Q + P×R
GF(2) 多项式运算示例:
到这里你可能已经注意到:GF(2) 多项式和二进制数之间有着完美的一一对应关系。这意味着什么呢?我们可以把二进制数当作多项式来算!说白了,我们是借用多项式的四则运算,给二进制数定义了一套全新的运算规则。
多位运算示例:
进一步, 我们之前说的 CRC 计算,其实我们不需要基于整数的除法来定义 CRC,我们真正需要的是: 四则运算.它压根不在乎你用的是整数、自然数还是别的什么。只要满足这些运算规律,CRC 就能工作。
使用 GF(2) 多项式的四则运算是为了充分利用值域空间
这里, 我们把二进制数看作一个 GF(2) 为系数的多项式,然后按照多项式的四则运算来定义这个二进制数的四则运算。
这样做是因为能完全使用二进制数的值空间。
例如, 如果我们还用整数定义的四则运算, 那么要找一个质数作为 CRC 的除数,例如 97, 那么每个校验值的值域是[0, 97)
,要是用一个 byte 来存, 那么 97 到 255 的数字用不到, 校验能力就降低了。
但是 GF(2) 的多项式的 prime 多项式, 例如 x³ + x + 1 (对应二进制 1011), 那么用它做除数多项式,余数多项式可以是全部 2³ 个 2 次多项式, 增加了校验值的值域空间,也就是最大化了检测能力. 标准的 CRC32 则是全部利用了 2³² 个值。
二进制数和多项式的对应关系
每个二进制数都能对应一个多项式:
多项式表示:
现在我们熟悉的除法思想就可以用到二进制数据上了!
CRC 定义为:
就像我们选择质数 97 作为除数一样,CRC-32 使用的生成多项式也必须是 不可约的多项式(irreducible polynomial)。这保证了最好的错误检测性能。
让我们用一个 4 位的不可约多项式来演示 CRC 计算:
这和我们第一章介绍的用整数除法在概念上完全一致,只是现在用的是多项式除法和异或运算。
基于整数除法的 CRC 是 CRC32 的核心原理,在此基础上再把四则运算替换为 GF(2) 多项式的四则运算,则充分利用了值的空间(配合计算机中 2⁸ 宽的场景)。
CRC 在 LevelDB 中一个看似多余的设计
LevelDB 的 Mask 操作
在 LevelDB 的代码中(源码链接),系统计算出 CRC-32 校验值后,并不直接使用,而是要做一个变换:
这个操作包含两个步骤:
右旋 15 位:
(crc >> 15) | (crc << 17)
加上常数:
+ kMaskDelta
问题
代码注释指出了关键问题:
"it is problematic to compute the CRC of a string that contains embedded CRCs"对包含嵌入式 CRC 的字符串计算 CRC 是有问题的
Yes, But, 到底什么东西 problematic 了?
实际上它所指的这个问题出现在一个常见场景中:当 CRC 校验码本身也成为被校验数据的一部分时。
考虑这样的嵌套数据结构, 我们先有了消息 M, 然后为 M 做一次 CRC 得到CRC(M)
,然后把 M 和CRC(M)
拼到一起, 然后传输协议的下一层又对整个M | CRC(M)
,当做一个消息, 又做一次 CRC:
这里问题出现了:如果两层使用相同的 CRC 算法,那么不管消息 M 是什么内容,第二层的 CRC 值都会是同一个固定常数!
这意味着外层 CRC 完全失去了检错能力 - 无论内层数据如何变化,外层看到的校验结果永远相同。
LevelDB 的 mask 操作正是为了解决这个问题而设计的。通过对 CRC 值进行可逆的变换,避免了退化现象的发生,保证了多层校验系统的有效性。
CRC 的"自包含退化"现象
用简单 CRC 推导退化现象
为了清楚展示退化过程,我们定义一个简化的 CRC 算法, 除数多项式是111₂
:
用二进制表示:
1000₂ = 8
(即 2³,对应多项式 x³)111₂ = 7
(对应多项式 x² + x + 1,这是一个 3 次不可约多项式)
这相当于 3 位的 CRC。
完整的数学推导
∵ CRC(M) = (M × 1000₂) % 111₂
∴ M × 1000₂ = k × 111₂ + CRC(M) (其中 k 为某个数)
二进制连接操作: 在二进制中,要把 CRC 追加到消息 M 后面,需要先为 CRC 空出位置。由于我们的 CRC 是 3 位(111₂ 有 3 位),所以需要将 M 左移 3 位:
然后把 CRC 值放入这 3 位中:
(注意实际使用时, CRC32 是 4 个 byte,所以把 CRC32 追加到消息 M 的操作是: M 后面空出 4 byte, 即M * 2³²
,然后追加 M 的 CRC32)
例如:M = 101₂,CRC(M) = 101₂
M × 1000₂ = 101₂ × 1000₂ = 101000₂ (为 CRC 空出 3 个位置) M | CRC(M) = 101000₂ + 101₂ = 101101₂ (把 CRC 填入空出的位置)
然后我们将关系代入到追加后的数据得到:
在 GF(2) 域中,任何数与自己相加都等于 0:
因此:
于是我们发现:M | CRC(M)
正好是 111₂ 的倍数!
计算第二层 CRC
然后计算 CRC(M | CRC(M)),利用 M | CRC(M) = k × 111₂:
这就是数学上退化现象的本质:无论 M 是什么值,CRC(M | CRC(M)) 总是等于同一个固定常数 (0)!
实际系统中的表现
在真实的 CRC 实现中,由于存在初始值、最终异或等处理步骤,这个固定值通常不是 0,而是某个特定常数(称为 residue)。但核心问题不变:
Mask 机制的影响分析
所有 LeveldDB 中的注释就是在说这个问题,并且试图用 Mask 对 CRC 值做一个变换来消除这种退化特性.显然, ((crc >> 15) | (crc << 17)) + kMaskDelta
操作之后就破坏了上面的数学关系, 使得外层 CRC 不再是一个消息无关的常数.
然后我们来分析下 Mask 能提供哪些特性:
对随机数据损坏的防护能力:无变化
从数学角度分析,mask 机制对随机 bit 错误的检测能力既不会提升,也不会降低。
我们来看看这两种方案的区别。设原始消息为 M,两种方案的映射关系是:
无 mask 方案:M → (CRC₁, CRC₂) = (CRC(M), 固定常数)
有 mask 方案:M → (CRC₁, CRC₂') = (CRC(M), CRC(M | mask(CRC(M))))
从这个推导可以看出,有 mask 的方案中,最终结果也只与 CRC(M)
相关。由于 CRC(M)
的取值只有 2³ 个,所有的消息 M 最终映射到平面上的 2³ 个点,纠错能力也只有 1/2³。
有意思的是,两种方案都将任意消息 M 映射到一个 2 维空间中的特定点。虽然无 mask 时外层 CRC 是固定常数,但实际上这并不减少整体的错误检测能力:
两种方案的值域大小相同(都是 2³ 个可能的组合)
对于随机的 bit 翻转,两种方案检测错误的概率完全相等
外层 CRC 的"退化"不影响内层 CRC 的有效性
对人为攻击的防护能力:显著提升
Mask 机制的真正价值在于防范恶意攻击,而非随机错误。
我们来比较一下两种情况下攻击者面临的难度:
如果没有 mask:攻击者知道外层 CRC 永远是固定常数,就可以任意构造 (M', CRC(M')) 对。只要保证 CRC 计算正确,外层校验必然通过,攻击成本很低。
如果有了 mask:攻击者不知道 mask 后的外层 CRC 值,无法直接构造能通过外层校验的伪造数据,必须破解 mask 算法或进行暴力搜索,攻击成本大大增加。
这里有个重要的区别:
抗损坏:防御随机的 bit 变化,主要看值域大小。
抗攻击:防御人为的 bit 变化,主要看构造难度。
Mask 机制巧妙地提升了攻击的构造难度,让系统更安全,同时对随机错误的检测能力完全不受影响。这就是 LevelDB 为什么要加入这个看起来"多余"的 mask 操作的原因。
实践验证:用代码重现退化现象
Python 实现
Python 示例代码地址 -- https://gist.github.com/drmingdrmer/2b49106b225ca7bf361fd21454f693da
让我们用 CRC-32C(LevelDB 使用的算法)来验证自包含退化现象:
验证嵌套 CRC 退化现象
运行结果
我们看到, 尽管:
每个 LevelDB 数据块的内容完全不同
每个数据块的内层 CRC 也完全不同(0x12AB34CD, 0x56EF78AB 等)
但是所有的外层 CRC 都是同一个值:
0x48674BC7
这意味着外层协议完全失去了检错能力!不管 LevelDB 内部的数据如何变化,外层看到的"校验码"永远是同一个常数。
LevelDB 的 Mask 方案
现在让我们看看 mask 如何打破这种退化:
输出:
我们也看到了区别. 使用 mask 后,每个 LevelDB 数据块都产生了不同的外层 CRC 值,外层协议重新获得了检错能力!
现实应用:分层存储系统中的校验挑战
存储系统的分层结构
现代存储系统通常采用多层架构,每层都有自己的完整性校验:
在这种架构中,上层数据经常包含下层的校验码,形成嵌套结构。
问题场景举例
场景 1:数据库备份
如果 backup_crc 采用相同的 CRC 算法,就会遇到自包含退化。
场景 2:网络传输
LevelDB 的 mask 确保了 packet_crc 不会退化。
场景 3:RAID 系统
其他系统的应对策略
第一种方案是使用不同的 CRC 多项式,让各层使用不同的生成多项式。这种方法简单直接,但需要在系统各层之间进行协调,增加了实现复杂度。第二种是加入层标识,在数据前加入层标识符,例如 [layer_id | data | crc]
,这样能够明确分层,但会增加存储开销。第三种方案是使用不同的校验算法,让不同层使用 CRC、MD5、SHA 等不同算法,虽然能完全避免算法层面的冲突,但带来了性能和复杂度的开销。第四种就是 LevelDB 式的 mask 方案,对存储的校验码进行可逆变换,这种方法开销最小,效果最好,但需要理解数学原理才能正确实现。
设计选择的权衡
从这个表格可以看出,LevelDB 选择 mask 方案是有道理的 - 既保持了高性能,又解决了根本问题。
适用范围和限制
mask 方案特别适用于高性能存储系统、嵌套数据结构频繁的场景以及需要保持算法一致性的系统。不过在一些场景下不太适用,比如对校验码有加密要求的场景,因为 mask 操作是可逆的,不提供额外的安全保护。在极端安全敏感的环境中,可能需要更复杂的校验方案。另外,如果系统已有成熟的分层校验方案,引入 mask 可能得不偿失。
实施建议
如果你的系统也遇到了类似问题,建议可以这样考虑:先确认问题确实存在,检查是否真的有嵌套 CRC 的场景。然后选择合适的 mask 参数,可以参考 LevelDB 的做法,选择旋转位数和常数。别忘了实现 unmask 函数,读取时需要能还原出原始 CRC。充分测试验证很重要,要确保 mask 真的解决了退化问题。最后记录设计决策,给未来的维护者留个说明,解释为什么要这么做。
所以下次看到这种看似"多余"的代码时,不妨先想想是否有什么深层的原因。很多时候,这些操作都是经过深思熟虑的。
结论:从数学到工程的完美结合
我们从一个简单的观察开始:LevelDB 在存储 CRC 时要做一个看似多余的 mask 操作。通过一步步分析,我们发现这个操作其实解决了一个很深刻的数学问题。从数学层面看,CRC 基于 GF(2) 多项式除法,当余数被附加回原消息时,形成的码字能被生成多项式整除,导致对这种"自包含"结构再次计算 CRC 时总是得到固定常数。从工程层面看,在分层存储系统中,这种退化会导致上层校验失效,无法检测到下层数据的损坏或篡改。
LevelDB 的 mask 方案体现了优秀的工程设计思路。它采用最小干预原则,不改变 CRC 算法本身,只在存储时做简单变换,对性能影响微乎其微。同时保持数学严密性,右旋转提供线性置换,加法常数引入非线性变换,彻底破坏 GF(2) 域的代数结构。在工程实用性方面,操作可逆保证数据完整性,实现简单减少出错概率,向后兼容便于系统演进。
这个案例让我们看到系统软件设计的深层道理。数学基础的重要性在于,只有深入理解 CRC 的数学原理,才能发现问题的根源,进而设计出优雅的解决方案。细节往往是关键,一个看起来微不足道的操作,可能就是系统完整性的关键保护。简单往往最有效,面对复杂的数学问题,最好的工程解决方案通常是最简单的那个。
这种思路可以应用到其他地方。Hash 函数的嵌套使用中,其他校验算法可能也存在类似的退化问题。在密码学协议设计中,要小心协议消息的自包含可能带来的安全隐患。在编码理论应用中,纠错码分层使用时也要注意避免类似的陷阱。
作为开发者,这个案例给了我们启发:别急着删"多余"的代码,先搞清楚为什么要这样写,再考虑要不要"优化"。数学基础真的有用,很多看起来是工程问题的,其实根源都在数学。测试要考虑边界情况,特别是一些嵌套、组合的场景。好的注释很重要,给后来的人解释清楚"为什么这样做"。
LevelDB 的这个 mask 操作,表面上看就是几行简单的代码,但背后其实体现了对数学原理的深刻理解和对工程质量的严格要求。这也许就是优秀系统软件的特点吧:用最简单的方法解决最复杂的问题,在正确性和性能之间找到最佳的平衡。所以下次当你在代码中看到一些"奇怪"的操作时,不妨先停下来想想:这里是不是隐藏着什么有趣的设计想法呢?
参考资料
源码资料
原文链接 -- https://blog.openacid.com/algo/crc
Python 示例代码地址 -- https://gist.github.com/drmingdrmer/2b49106b225ca7bf361fd21454f693da
fuzhe at x -- https://x.com/fuzhe19
LevelDB CRC 实现:
LevelDB CRC32C 头文件 - Mask 函数的完整实现
LevelDB CRC32C 实现文件 - CRC-32C 计算的完整代码
LevelDB Block 格式文档 - 数据块存储格式说明
数学理论基础
CRC 算法原理:
Wikipedia: Cyclic redundancy check - CRC 算法的数学基础
Wikipedia: Finite field arithmetic - GF(2) 有限域运算
Wikipedia: Irreducible polynomial - 不可约多项式的数学定义
多项式运算:
Wikipedia: Polynomial long division - 多项式长除法
Wikipedia: Galois field - 伽罗华域 GF(2) 的详细解释
CRC 标准和应用
CRC-32C (Castagnoli):
RFC 3720 - Internet Small Computer Systems Interface (iSCSI) - CRC-32C 在 iSCSI 中的应用
Wikipedia: Cyclic redundancy check - CRC-32C - CRC-32C 算法详细说明
其他 CRC 实现:
zlib CRC-32 实现 - 广泛使用的 CRC-32 实现
系统设计相关
存储系统设计:
LevelDB 设计文档 - LevelDB 整体架构设计
LSM-Tree 论文 - Log-Structured Merge Trees 原理
数据完整性:
End-to-end data integrity - 端到端数据完整性保护
工程实践案例
类似的 Mask 技术:
PostgreSQL CRC implementation - PostgreSQL 的 CRC 实现
ZFS checksum algorithms - ZFS 文件系统的校验算法
错误检测技术:
Reed-Solomon codes - 纠错码理论
Hamming codes - 汉明码原理
实用工具
CRC 计算工具:
Online CRC Calculator - 在线 CRC 计算器,支持多种 CRC 算法
RevEng CRC Catalogue - 各种 CRC 算法的参数表
Python 实现:
crcmod Python library - Python CRC 计算库
zlib.crc32 文档 - Python 内置 CRC-32 函数
学术资料
密码学相关:
Handbook of Applied Cryptography - 应用密码学手册
关于 Databend
Databend 是一款开源、弹性、低成本,基于对象存储也可以做实时分析的新式湖仓。期待您的关注,一起探索云原生数仓解决方案,打造新一代开源 Data Cloud。
👨💻 Databend Cloud:databend.cn
📖 Databend 文档:docs.databend.cn
💻 Wechat:Databend
✨ GitHub:github.com/databendlab…
版权声明: 本文为 InfoQ 作者【Databend】的原创文章。
原文链接:【http://xie.infoq.cn/article/728b6998be9f0d9691b22897f】。文章转载请联系作者。
评论