写点什么

CRC32 自包含退化现象分析

作者:Databend
  • 2025-09-09
    福建
  • 本文字数:10841 字

    阅读完需:约 36 分钟

CRC32 自包含退化现象分析

我的好友 fuzhe 在阅读 LevelDB 源码时,发现了一个有趣的细节:系统在存储 CRC 校验码时,并不直接使用计算出的值,而是要先做一个看似"多余"的 mask 操作。这个操作包括右旋转 15 位和加上一个神秘的常数 0xa282ead8ul


代码注释提到这是为了解决"对包含嵌入式 CRC 的字符串计算 CRC 是有问题的",但到底什么问题?为什么位操作能解决它?这个设计背后隐藏着什么原理?


今天我们带着这些疑问,开始一场 CRC 的探索之旅。一步步揭开 CRC 的数学本质,理解"自包含退化"现象,最终看到 LevelDB 工程师们如何用优雅的数学技巧解决了这个深刻的问题。

什么是 CRC:从自然数除法到校验码

传输数字 1234 时,如何检测错误?用除法取余数:


1234 ÷ 97 = 12 余 70
复制代码


余数 70 就是 1234指纹


校验原理


  • 发送:数据=1234, 校验=70

  • 接收:重新计算 received % 97 的余数

  • 余数不是 70 → 检测到错误


判断收到的消息正确性的方式是


接收到:数据=1234, 校验=70
1. 计算 1234 % 97 = 702. 比较:计算余数(70) == 收到的校验码(70) ?3. 相等 → 数据很可能正确4. 不等 → 数据肯定有错误
复制代码


这里有个重要的点:余数相等不能 100% 保证数据正确(可能有碰撞),但余数不等就可以 100% 确定数据有错误。


例子


1234 % 97 = 701235 % 97 = 71  ← 数据变了,余数也变了
复制代码


另外,为了防止消息体和校验值一样,CRC 在定义的时候还提出了一点,就是先要乘以一个比较大的数字,来保证所得余数跟消息本身不一样:对于所有输入数字,先放大:


先乘 100:42 → 42004200 % 97 = 29  → 发送:数据=42, 校验=29
复制代码


碰撞概率:使用质数 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):


0 + 0 = 00 + 1 = 11 + 0 = 11 + 1 = 0  ← 没有进位!
复制代码


01 的减法规则(同样是异或):


0 - 0 = 01 - 1 = 01 - 0 = 10 - 1 = 1  ← 没有借位!
复制代码


可以看出, 01 的世界里加法和减法相同: x+1 == x-1, 都对应异或操作:a + b → a ^ b


01 的乘法规则


0 × 0 = 00 × 1 = 01 × 0 = 01 × 1 = 1  ← 和普通乘法一样
复制代码


可以看出 01 世界中的乘法就是 AND 操作:a × b = a & b


01 的除法规则


0 ÷ 1 = 01 ÷ 1 = 1  ← 和普通除法一样
复制代码


看到这里我们发现: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) 多项式运算示例


分配律验证:(x² + 1) × (x + 1) = (x² + 1)×x + (x² + 1)×1                   = x³ + x + x² + 1    ← 展开后有重复项
体现 GF(2)特性的例子:(x + 1) + (x + 1) = x + 1 + x + 1 = (x + x) + (1 + 1) = 0 + 0 = 0 ← 相同项抵消!
多项式加法:(x² + x + 1) + (x² + 1) = x² + x + 1 + x² + 1 = (x² + x²) + x + (1 + 1) = 0 + x + 0 = x ← 相同系数的项抵消
复制代码


到这里你可能已经注意到:GF(2) 多项式和二进制数之间有着完美的一一对应关系。这意味着什么呢?我们可以把二进制数当作多项式来算!说白了,我们是借用多项式的四则运算,给二进制数定义了一套全新的运算规则。


多位运算示例


加法/减法:  1011⊕ 1101------  0110
乘法(多项式乘法): 101 × 11 = 101 × (10 + 1) = 101 × 10 + 101 × 1 = 1010 ⊕ 101 = 1111
复制代码


进一步, 我们之前说的 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³² 个值。

二进制数和多项式的对应关系

每个二进制数都能对应一个多项式:


多项式表示


二进制:1011 → 多项式:1·x³ + 0·x² + 1·x¹ + 1·x⁰ = x³ + x + 1二进制:1101 → 多项式:1·x³ + 1·x² + 0·x¹ + 1·x⁰ = x³ + x² + 1
复制代码


现在我们熟悉的除法思想就可以用到二进制数据上了!


CRC 定义为


数据多项式 * 100..000  % 生成多项式 = 余数(CRC 值)
复制代码


就像我们选择质数 97 作为除数一样,CRC-32 使用的生成多项式也必须是 不可约的多项式(irreducible polynomial)。这保证了最好的错误检测性能。


让我们用一个 4 位的不可约多项式来演示 CRC 计算:


数据多项式:x² + 1 (对应二进制 101)生成多项式:x³ + x + 1 (对应二进制 1011,这是一个不可约多项式)
为了计算 CRC,我们需要计算:(x² + 1) · x³ % (x³ + x + 1) 的余数数据后面补 3 个 0(相当于乘以 x³,补 0 个数=生成多项式度数)
多项式除法过程:被除数:(x² + 1) · x³ = x⁵ + x³ (对应二进制 101000)
___________________________________ x³ + x + 1 √ x⁵ + 0·x⁴ + x³ + 0·x² + 0·x + 0 x⁵ + 0·x⁴ + x³ + x² ---------------------------------- x² + 0·x + 0 ← 余数
结果:数据 (x² + 1) 的 CRC 是 100发送 消息M | CRC(M):101 | 100
复制代码


这和我们第一章介绍的用整数除法在概念上完全一致,只是现在用的是多项式除法和异或运算。


基于整数除法的 CRC 是 CRC32 的核心原理,在此基础上再把四则运算替换为 GF(2) 多项式的四则运算,则充分利用了值的空间(配合计算机中 2⁸ 宽的场景)。

CRC 在 LevelDB 中一个看似多余的设计

LevelDB 的 Mask 操作

在 LevelDB 的代码中(源码链接),系统计算出 CRC-32 校验值后,并不直接使用,而是要做一个变换:


// https://github.com/google/leveldb/blob/main/util/crc32c.h#L26static const uint32_t kMaskDelta = 0xa282ead8ul;
// Return a masked representation of crc.//// Motivation: it is problematic to compute the CRC of a string that// contains embedded CRCs. Therefore we recommend that CRCs stored// somewhere (e.g., in files) should be masked before being stored.inline uint32_t Mask(uint32_t crc) { // Rotate right by 15 bits and add a constant. return ((crc >> 15) | (crc << 17)) + kMaskDelta;}
复制代码


这个操作包含两个步骤:


  1. 右旋 15 位(crc >> 15) | (crc << 17)

  2. 加上常数+ 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:


第一层:消息 M  →  M | CRC(M)
第二层:M | CRC(M) → M | CRC(M) | CRC(M | CRC(M))
复制代码


这里问题出现了:如果两层使用相同的 CRC 算法,那么不管消息 M 是什么内容,第二层的 CRC 值都会是同一个固定常数!


这意味着外层 CRC 完全失去了检错能力 - 无论内层数据如何变化,外层看到的校验结果永远相同。


LevelDB 的 mask 操作正是为了解决这个问题而设计的。通过对 CRC 值进行可逆的变换,避免了退化现象的发生,保证了多层校验系统的有效性。

CRC 的"自包含退化"现象

第一层:消息 M  →  M | CRC(M)
第二层:M | CRC(M) → M | CRC(M) | CRC(M | CRC(M))
复制代码

用简单 CRC 推导退化现象

为了清楚展示退化过程,我们定义一个简化的 CRC 算法, 除数多项式是111₂


CRC = (消息 M × 1000₂) % 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 位:


M × 1000₂   (左移 3 位,为 3 位 CRC 空出位置)
复制代码


然后把 CRC 值放入这 3 位中:


M | CRC(M) = M × 1000₂ + CRC(M)
复制代码


(注意实际使用时, 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 填入空出的位置)


然后我们将关系代入到追加后的数据得到:


M | CRC(M)  =  M × 1000₂ + CRC(M)            =  (k × 111₂ + CRC(M)) + CRC(M)            =  k × 111₂ + CRC(M) + CRC(M)
复制代码


在 GF(2) 域中,任何数与自己相加都等于 0:


CRC(M) + CRC(M) = 0  (GF(2)加法性质)
复制代码


因此:


M | CRC(M)  =  k × 111₂
复制代码


于是我们发现:M | CRC(M) 正好是 111₂ 的倍数!


计算第二层 CRC


然后计算 CRC(M | CRC(M)),利用 M | CRC(M) = k × 111₂:


CRC(M | CRC(M))  =  CRC(k × 111₂)                 =  (k × 111₂ × 1000₂) % 111₂                 =  (k × 111₂ × 1000₂) % 111₂                 =  0   // 任何 111₂的倍数模 111₂都等于 0
复制代码


这就是数学上退化现象的本质:无论 M 是什么值,CRC(M | CRC(M)) 总是等于同一个固定常数 (0)!

实际系统中的表现

在真实的 CRC 实现中,由于存在初始值、最终异或等处理步骤,这个固定值通常不是 0,而是某个特定常数(称为 residue)。但核心问题不变:


CRC(任意消息 | 该消息的 CRC) = 固定常数(与消息内容无关)
复制代码

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))))


   CRC(M | mask(CRC(M)))=  CRC( M * 1000₂         + mask(CRC(M)) )=  CRC( k * 111₂ + CRC(M) + mask(CRC(M)) )=     ( k * 111₂ + CRC(M) + mask(CRC(M)) ) * 1000₂ % 111₂=     ( CRC(M) + mask(CRC(M)) ) * 1000₂ % 111₂
复制代码


从这个推导可以看出,有 mask 的方案中,最终结果也只与 CRC(M) 相关。由于 CRC(M) 的取值只有 2³ 个,所有的消息 M 最终映射到平面上的 2³ 个点,纠错能力也只有 1/2³。


有意思的是,两种方案都将任意消息 M 映射到一个 2 维空间中的特定点。虽然无 mask 时外层 CRC 是固定常数,但实际上这并不减少整体的错误检测能力:


  1. 两种方案的值域大小相同(都是 2³ 个可能的组合)

  2. 对于随机的 bit 翻转,两种方案检测错误的概率完全相等

  3. 外层 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 使用的算法)来验证自包含退化现象:


import osimport random
# CRC-32C (Castagnoli) 参数POLY_REV = 0x82F63B78
def _make_crc32c_table(): tbl = [] for i in range(256): crc = i for _ in range(8): if crc & 1: crc = (crc >> 1) ^ POLY_REV else: crc >>= 1 tbl.append(crc & 0xFFFFFFFF) return tbl
_CRC32C_TABLE = _make_crc32c_table()
def crc32c(data: bytes) -> int: """标准 CRC-32C 实现""" crc = 0xFFFFFFFF for b in data: crc = _CRC32C_TABLE[(crc ^ b) & 0xFF] ^ (crc >> 8) return (crc ^ 0xFFFFFFFF) & 0xFFFFFFFF
复制代码

验证嵌套 CRC 退化现象

def demonstrate_nested_crc_degradation():    # 模拟不同的LevelDB数据块    leveldb_blocks = [        b"user_data_1",        b"user_data_2",        b"important_record_12345",        b"transaction_log_abcdef",        bytes(range(50)),  # 二进制数据    ]
# 添加一些随机数据块 for _ in range(5): leveldb_blocks.append(os.urandom(random.randint(10, 100)))
print("=== 嵌套CRC退化验证 ===") outer_crcs = []
for i, block_data in enumerate(leveldb_blocks): # 第一层:LevelDB内部CRC inner_crc = crc32c(block_data) leveldb_block = block_data + inner_crc.to_bytes(4, "little")
# 第二层:外层协议CRC(如网络传输) outer_crc = crc32c(leveldb_block) complete_packet = leveldb_block + outer_crc.to_bytes(4, "little")
outer_crcs.append(outer_crc)
print(f"块 #{i+1:2d}: 数据长度={len(block_data):3d}, " f"内层CRC=0x{inner_crc:08X}, " f"外层CRC=0x{outer_crc:08X}")
# 检查外层CRC是否都相同 unique_outer_crcs = set(outer_crcs) print(f"\n不同外层CRC的数量: {len(unique_outer_crcs)}") print(f"所有外层CRC都是: 0x{list(unique_outer_crcs)[0]:08X}") print(">>> 外层CRC完全退化!无论内层数据如何变化,外层CRC都是固定值")
复制代码

运行结果

=== 嵌套CRC退化验证 ===块 # 1: 数据长度= 11, 内层CRC=0x12AB34CD, 外层CRC=0x48674BC7块 # 2: 数据长度= 11, 内层CRC=0x56EF78AB, 外层CRC=0x48674BC7块 # 3: 数据长度= 21, 内层CRC=0x9A2B3C4D, 外层CRC=0x48674BC7块 # 4: 数据长度= 22, 内层CRC=0xDEF01234, 外层CRC=0x48674BC7块 # 5: 数据长度= 50, 内层CRC=0x567890AB, 外层CRC=0x48674BC7...
不同外层CRC的数量: 1所有外层CRC都是: 0x48674BC7>>> 外层CRC完全退化!无论内层数据如何变化,外层CRC都是固定值
复制代码


我们看到, 尽管:


  • 每个 LevelDB 数据块的内容完全不同

  • 每个数据块的内层 CRC 也完全不同(0x12AB34CD, 0x56EF78AB 等)

  • 但是所有的外层 CRC 都是同一个值:0x48674BC7


这意味着外层协议完全失去了检错能力!不管 LevelDB 内部的数据如何变化,外层看到的"校验码"永远是同一个常数。

LevelDB 的 Mask 方案

现在让我们看看 mask 如何打破这种退化:


# LevelDB的mask函数K_MASK_DELTA = 0xA282EAD8
def mask(crc: int) -> int: # 右旋15位 + 加常数 rotated = ((crc >> 15) | (crc << 17)) & 0xFFFFFFFF return (rotated + K_MASK_DELTA) & 0xFFFFFFFF
def demonstrate_mask_effect(): # 使用前面相同的LevelDB数据块 leveldb_blocks = [ b"user_data_1", b"user_data_2", b"important_record_12345", b"transaction_log_abcdef", ]
print("\n=== 使用LevelDB mask后的结果 ===") outer_crcs_masked = []
for i, block_data in enumerate(leveldb_blocks): # 第一层:LevelDB内部CRC + mask inner_crc = crc32c(block_data) masked_crc = mask(inner_crc) # 关键:存储前mask leveldb_block_masked = block_data + masked_crc.to_bytes(4, "little")
# 第二层:外层协议CRC outer_crc = crc32c(leveldb_block_masked)
outer_crcs_masked.append(outer_crc)
print(f"块 #{i+1}: 原CRC=0x{inner_crc:08X}, " f"Mask后=0x{masked_crc:08X}, " f"外层CRC=0x{outer_crc:08X}")
unique_masked = set(outer_crcs_masked) print(f"\n使用mask后,不同外层CRC的数量: {len(unique_masked)}") print(">>> Mask成功!外层CRC现在随数据内容变化")
复制代码


输出:


=== 使用LevelDB mask后的结果 ===块 #1: 原CRC=0x12AB34CD, Mask后=0xE5F2A891, 外层CRC=0x3C7B2A15块 #2: 原CRC=0x56EF78AB, Mask后=0xC4D8B672, 外层CRC=0x8F4E1B29块 #3: 原CRC=0x9A2B3C4D, Mask后=0x7B3EA254, 外层CRC=0x1D6C9F38块 #4: 原CRC=0xDEF01234, Mask后=0x2A8C5E91, 外层CRC=0x94E7C2B6
使用mask后,不同外层CRC的数量: 4>>> Mask成功!外层CRC现在随数据内容变化
复制代码


我们也看到了区别. 使用 mask 后,每个 LevelDB 数据块都产生了不同的外层 CRC 值,外层协议重新获得了检错能力!

现实应用:分层存储系统中的校验挑战

存储系统的分层结构

现代存储系统通常采用多层架构,每层都有自己的完整性校验:


应用层数据├── 应用层 CRC (例:数据库记录校验)├── 存储引擎层 CRC (例:LevelDB block 校验)├── 文件系统层 CRC (例:ext4/ZFS 校验)└── 硬件层 CRC (例:磁盘扇区校验)
复制代码


在这种架构中,上层数据经常包含下层的校验码,形成嵌套结构。

问题场景举例

场景 1:数据库备份


原始记录: [user_data | record_crc]备份文件: [backup_header | [user_data | record_crc] | backup_crc]
复制代码


如果 backup_crc 采用相同的 CRC 算法,就会遇到自包含退化。


场景 2:网络传输


LevelDB block: [block_data | block_crc(masked)]网络包:        [packet_header | [block_data | block_crc] | packet_crc]
复制代码


LevelDB 的 mask 确保了 packet_crc 不会退化。


场景 3:RAID 系统


磁盘扇区: [user_data | user_crc | sector_crc]RAID 校验: [扇区 1 | 扇区 2 | ... | raid_parity]
复制代码

其他系统的应对策略

第一种方案是使用不同的 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 实现


数学理论基础

CRC 算法原理



多项式运算


CRC 标准和应用

CRC-32C (Castagnoli)



其他 CRC 实现


系统设计相关

存储系统设计



数据完整性


工程实践案例

类似的 Mask 技术



错误检测技术


实用工具

CRC 计算工具



Python 实现


学术资料

密码学相关


关于 Databend

Databend 是一款开源、弹性、低成本,基于对象存储也可以做实时分析的新式湖仓。期待您的关注,一起探索云原生数仓解决方案,打造新一代开源 Data Cloud。


👨‍💻‍ Databend Cloud:databend.cn


📖 Databend 文档:docs.databend.cn


💻 Wechat:Databend


✨ GitHub:github.com/databendlab…

发布于: 刚刚阅读数: 6
用户头像

Databend

关注

还未添加个人签名 2022-08-25 加入

还未添加个人简介

评论

发布
暂无评论
CRC32 自包含退化现象分析_Databend_InfoQ写作社区