CRC(循环冗余校验)技术文档
面向:协议/固件工程师、FPGA/ASIC 工程师、驱动开发、逆向与安全分析
目标:从数学定义 → 参数模型 → 常见标准 → 正确实现方式 → 并行/反向/工程坑点
1. CRC 的本质与数学模型
CRC(Cyclic Redundancy Check)不是“简单校验和”,而是定义在有限域 GF(2) 上的线性分组码。
1.1 比特串与多项式
在 GF(2) 中,每一比特是一个系数(0 或 1),比特串可映射为一元多项式:
- 比特串从最高位到最低位,对应从最高次幂到 0 次幂。
- 示例:比特串
1101011011(共 10 位),从左到右为 bit_9…bit_0:
为 1 的比特在:9, 8, 6, 4, 3, 1, 0
对应多项式:
x9+x8+x6+x4+x3+x+1 x^9 + x^8 + x^6 + x^4 + x^3 + x + 1 x9+x8+x6+x4+x3+x+1
1.2 CRC 编码的标准流程(概念版)
给定:
- 生成多项式 (G(x)),阶为 (n)(CRC 宽度 = n)
- 原始消息多项式 (M(x))
编码过程(最经典定义):
- 将消息左移 n 位:M(x)⋅xnM(x) \cdot x^nM(x)⋅xn
- 计算模 2 多项式除法的余数:
R(x)=(M(x)xn) mod G(x) R(x) = (M(x)x^n) \bmod G(x) R(x)=(M(x)xn)modG(x) - 发送多项式:
T(x)=M(x)xn+R(x) T(x) = M(x)x^n + R(x) T(x)=M(x)xn+R(x)
满足 T(x) mod G(x)=0T(x) \bmod G(x) = 0T(x)modG(x)=0。
工程实现里常通过寄存器迭代 + 一堆约定参数来实现这一过程。
2. CRC 参数模型:工程中要记住的是这 6 个
任何一个「CRC 算法」都应由以下参数唯一描述(CRC model):
- Width:CRC 位宽,如 4/8/16/32/64。
- Poly:生成多项式(除最高位隐含的 xWidthx^WidthxWidth 外的部分),用二进制/十六进制表示系数。
- Init:初始寄存器值。
- RefIn:输入每个字节时是否按位反转(LSB-first)。
- RefOut:输出 CRC 值前是否按位反转。
- XorOut:最终 CRC 输出前额外异或的常数。
同一个 Poly,不同 Init/RefIn/RefOut/XorOut → 完全不同的 CRC 变种。
90% 的工程 bug 都是因为这几个没对齐协议。
3. CRC-32 详解
3.1 标准 CRC-32(Ethernet / ZIP 等)
常说的“CRC32”通常指 CRC-32/ADCCP(或称 CRC-32/IEEE 802.3),参数:
-
Width:32
-
Poly:0x04C11DB7
对应多项式:x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1x^{32} + x^{26} + x^{23} + x^{22} + x^{16} + x^{12} + x^{11} + x^{10} + x^8 + x^7+x^5 + x^4 + x^2 + x + 1x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1
-
Init:0xFFFFFFFF
-
RefIn:True
-
RefOut:True
-
XorOut:0xFFFFFFFF
-
测试向量:“123456789” → 0xCBF43926
在实现层面,当采用 LSB-first + 右移实现时,使用的是 0x04C11DB7 的反射形式:0xEDB88320,这是 Poly 在实现中的另一种写法,不是“换了 CRC 算法”,只是位序换了。
3.2 正确的 bit-wise 实现示例(标准 CRC-32)
def crc32_eth(data: bytes) -> int:
# 标准 CRC-32 (Ethernet/ZIP) 实现:RefIn=RefOut=True
poly = 0xEDB88320 # 0x04C11DB7 的反射形式
crc = 0xFFFFFFFF
for b in data:
crc ^= b
for _ in range(8):
if crc & 1:
crc = (crc >> 1) ^ poly
else:
crc >>= 1
return crc ^ 0xFFFFFFFF
该实现对 "123456789" 输出 0xCBF43926,可与标准验证。
4. 常用 CRC 多项式与应用
4.1 CRC-8(示例)
- Poly:0x07(x8+x2+x+1)(x^8 + x^2 + x + 1)(x8+x2+x+1)
- 常见参数:Init=0x00, RefIn=False, RefOut=False, XorOut=0x00
- 应用:简单总线、小数据包。
4.2 CRC-16 家族
1)CRC-16/IBM
- Poly:0x8005(反射形式实现常写 0xA001)
- 常见参数(CRC-16/IBM):Init=0x0000, RefIn=True, RefOut=True, XorOut=0x0000
- 早期协议使用,现代更常见的是基于它的变种。
2)CRC-16/Modbus
- 属于 CRC-16-IBM 家族的具体变种:
- Poly:0x8005(实现多用 0xA001)
- Init=0xFFFF, RefIn=True, RefOut=True, XorOut=0x0000
- 应用:Modbus RTU/ASCII
- Modbus 用的是带特定 Init/Ref 的变体。
3)CRC-16-CCITT 系列
基础多项式:
- Poly:0x1021 (x16+x12+x5+1)(x^{16}+x^{12}+x^5+1)(x16+x12+x5+1)
常见变体:
-
CRC-16/CCITT-FALSE:Init=0xFFFF, RefIn=False, RefOut=False, XorOut=0x0000
-
CRC-16/X25(HDLC、PPP):Init=0xFFFF, RefIn=True, RefOut=True, XorOut=0xFFFF
- 实现里常看到 Poly=0x8408,这其实是 0x1021 的比特反射形式,用于 LSB-first 右移实现。
HDLC/PPP 修正:
- 使用的是 CRC-16/IBM 家族?不是。是 CRC-16/CCITT 多项式 0x1021 的反射参数变种(X25)。
4.3 CRC-32 常见变种提示
- CRC-32/ADCCP(Ethernet/ZIP)——最常用。
- CRC-32/BZIP2 —— RefIn=RefOut=False。
- CRC-32C —— Poly=0x1EDC6F41(Castagnoli)。
工程上遇到“CRC32”,一定要看协议给的完整参数,不要想当然。
4.4 USB 专用 CRC
- USB Token:CRC5,多项式 x5+x2+1x^5 + x^2 + 1x5+x2+1(0x05)
- USB Data:CRC16,多项式 0x8005(与 CRC-16-IBM 家族相关,参数有协议规定)
5. 实现方式与优化
5.1 Bit-wise(按位算法)
核心思想:模拟多项式除法。
MSB-first 典型步骤(不反射):
-
crc初值设为 Init。 -
每个字节左对齐到 CRC 高位:
crc ^= byte << (width-8)。 -
重复 8 次:
- 若最高位为 1:
crc = (crc << 1) ^ poly - 否则:
crc <<= 1
- 若最高位为 1:
-
约束在指定位宽:
crc &= (1<<width)-1 -
最后做 RefOut(如有)、XorOut。
LSB-first 实现同理,只是用右移 + 反射多项式。
5.2 Table-driven(查表法)
将 256 个可能字节的影响预先算成表,加速到「每字节 O(1)」。
- 单表(Slice-by-1):经典 256 项表。
- Slice-by-4/8/16:对多字节并行查多个表,大幅提升吞吐量。
5.3 NEON/SIMD 与并行
- 对大块数据,可使用 SIMD 一次处理多字节,并配合 Slice-by-4/8。
- 对极高性能场景,可设计 GF(2) 矩阵乘法 + carry-less multiply(如 CRC-32C 在 x86 PCLMULQDQ 指令上的优化)。
6. 硬件实现要点(FPGA/ASIC)
CRC 本质是线性反馈移位寄存器(LFSR):
-
串行实现:每时钟 1 bit 输入,反馈按 Poly 连接。
-
并行实现:通过展开多项式,给出 “输入 k bit 后寄存器新值 = 线性组合” 的矩阵表达,组合逻辑一次算多比特。
-
常见配置:
- Init 值可配置;
- 支持 RefIn/RefOut:通过对输入/输出做比特反转;
- 支持 XorOut;
- 支持「继续累加」(用于流式数据)。
协议兼容要点:硬件 LFSR 结构 ≠ Poly 文本本身,务必根据协议给的参数推导 LFSR 拓扑。
7. 分块 / 并行 CRC 的正确组合方式
很多人会犯的错:把各块 CRC 简单 XOR —— 这是错误的。
正确原则(简述版):
- CRC 是线性的:
CRC(A∣B)\text{CRC}(A | B)CRC(A∣B) 可以由 CRC(A)\text{CRC}(A)CRC(A) 和 BBB 的数据推导出来。 - 若你先算出块 A 的 CRC,再算块 B 的 CRC,不可以直接 XOR。
- 合并时需要将 CRC(A)\text{CRC}(A)CRC(A) 视为对 A 的余数对应项,并对 B 的长度做 x8⋅len(B)x^{8\cdot len(B)}x8⋅len(B) 的多项式乘法,再与 B 的 CRC 线性组合。
工程实践:
- 通常用预先生成的“CRC combine”函数或矩阵(如 zlib 就有
crc32_combine)。 - 记住一句:“能并行算,但要用正确的数学合并,不是拍脑袋 XOR。”
8. 反向 CRC、补偿字节与攻击视角
因为 CRC 是线性的:
- 给定消息剩余部分和目标 CRC,能构造若干「补偿字节」使整体 CRC 等于期望值(fix-up byte)。
- 可以对单比特 / 多比特翻转设计,使 CRC 不变或变为预期值。
- 因此 CRC 适合作为错误检测,不适合作为安全校验/防篡改手段。
在协议/SW 里:
CRC != 安全哈希,不能用来防伪造。
9. 生成多项式选择与工程建议
选型时考虑:
-
码长 & 场景
- 小数据包 / 简单总线:CRC-8 / CRC-16 即可。
- 网络帧 / 文件 / 存储块:CRC-32 / CRC-32C。
-
标准优先
- 能用协议/行业标准就别造轮子。
- 同一类设备需兼容,就统一使用公开模型(参数六件套)。
-
错误检测能力
- 多项式决定检测单比特、双比特、奇偶数比特、突发错误的上界。
- 如 CRC-32C(Castagnoli)对短帧有更好性能与检测特性,在很多现代协议中被采用。
-
实现成本
- 软件中选便于表驱动/指令优化的;
- 硬件中选反馈 taps 少、逻辑扇出适中的。
10. 按字节处理的示例
这一节专门把「如何按字节处理」讲清,用标准 CRC-32(Ethernet/ZIP)参数做精确示范;
10.1 使用的 CRC 模型(固定死)
后文所有示例均指下述 CRC-32(IEEE 802.3):
- Width:32
- Poly:0x04C11DB7(实现用反射多项式 0xEDB88320)
- Init:0xFFFFFFFF
- RefIn:True
- RefOut:True
- XorOut:0xFFFFFFFF
采用LSB-first + 右移实现,对应伪代码:
crc = 0xFFFFFFFF
for each byte b in data:
crc ^= b
repeat 8 times:
if (crc & 1) == 1:
crc = (crc >> 1) ^ 0xEDB88320
else:
crc >>= 1
crc &= 0xFFFFFFFF
crc ^= 0xFFFFFFFF
注意:因为 RefIn=True,这里是直接 XOR b,而不是 b << 24;多出来的那种写法通常对应非反射变种。
10.2 单字节 0x31 的逐位推导(精细步骤)
数据字节:0x31(ASCII ‘1’,二进制 0011 0001)
- 初始值:
crc = 0xFFFFFFFF
- 与当前字节异或(RefIn=True,低位对齐):
crc = 0xFFFFFFFF ^ 0x31 = 0xFFFFFFCE
- 对该字节进行 8 次按位迭代(每次处理当前 crc 的最低位):
记多项式常量:P = 0xEDB88320。
下面每一步:
- 先看
crc & 1(最低位) - 若为 1:
crc = (crc >> 1) ^ P - 若为 0:
crc = crc >> 1 - 然后约束到 32 位。
| 步骤 | 说明 | 操作 | 新 crc(hex) | 当前最低位 |
|---|---|---|---|---|
| 初值 | 初始异或后 | - | 0xFFFFFFCE | 0 |
| 1 | bit0:最低位=0 → 右移 | crc >>= 1 | 0x7FFFFFE7 | 1 |
| 2 | bit1:最低位=1 → 右移 ^ P | (0x7FFFFFE7>>1)^P | 0xD2477CD3 | 1 |
| 3 | bit2:最低位=1 → 右移 ^ P | (0xD2477CD3>>1)^P | 0x849B3D49 | 1 |
| 4 | bit3:最低位=1 → 右移 ^ P | (0x849B3D49>>1)^P | 0xAFF51D84 | 0 |
| 5 | bit4:最低位=0 → 右移 | 0xAFF51D84>>1 | 0x57FA8EC2 | 0 |
| 6 | bit5:最低位=0 → 右移 | 0x57FA8EC2>>1 | 0x2BFD4761 | 1 |
| 7 | bit6:最低位=1 → 右移 ^ P | (0x2BFD4761>>1)^P | 0xF8462090 | 0 |
| 8 | bit7:最低位=0 → 右移 | 0xF8462090>>1 | 0x7C231048 | - |
因此:
处理完首字节
0x31后的中间 CRC 值为:0x7C231048(尚未做最终 XorOut,也未处理后续字节)。
这一步完整演示了:
- 为什么看最低位(RefIn=True → 右移);
- 为什么要按位判断是否异或多项式;
- 如何从 Init + 一个字节,得到正确中间状态。
10.3 多字节 "123456789" 的完整中间结果
同样用上述标准 CRC-32 算法,依次处理 9 个字节:
数据:b"123456789"
字节序列(十六进制):31 32 33 34 35 36 37 38 39
每处理完一个字节(包含 8 次 bit 循环)后的中间 CRC(未做最终 ^0xFFFFFFFF)如下:
| 字节序号 | 字符 | 十六进制 | 处理后中间 CRC(hex) |
|---|---|---|---|
| 1 | ‘1’ | 0x31 | 0x7C231048 |
| 2 | ‘2’ | 0x32 | 0xB0ACBB32 |
| 3 | ‘3’ | 0x33 | 0x77B79C2D |
| 4 | ‘4’ | 0x34 | 0x641C1F5C |
| 5 | ‘5’ | 0x35 | 0x340AC5E3 |
| 6 | ‘6’ | 0x36 | 0xF68D2C9E |
| 7 | ‘7’ | 0x37 | 0xAFFC9660 |
| 8 | ‘8’ | 0x38 | 0x651F2550 |
| 9 | ‘9’ | 0x39 | 0x340BC6D9 |
完成全部字节后,执行最终步骤:
Final CRC = 0x340BC6D9 ^ 0xFFFFFFFF = 0xCBF43926
这就是标准 CRC-32 对 "123456789" 的结果,用来验证你实现是否完全正确。
11. 并行优化的版本
完整可跑的 Python 脚本,里边包含:
- 标准 CRC-32(Ethernet/ZIP)bit-wise 实现
- 正确的 Slice-by-4 实现(对齐 zlib BYFOUR 版本)
- 正确的
crc32_combine(对齐 zlib 官方实现) - 用
"123456789"和分块"1234" + "56789"做对比验证
python3 crc32_verify.py
输出应该完全一致。
#!/usr/bin/env python3
import zlib
# 标准 CRC-32 (Ethernet/ZIP) 参数:
# Poly = 0x04C11DB7 (实现使用反射形式 0xEDB88320)
# Init = 0xFFFFFFFF, RefIn = True, RefOut = True, XorOut = 0xFFFFFFFF
POLY_REFLECTED = 0xEDB88320
INIT = 0xFFFFFFFF
XOROUT = 0xFFFFFFFF
GF2_DIM = 32 # CRC32 位宽
# ========== 1. 基准:bit-wise 实现 ==========
def crc32_bitwise(data: bytes) -> int:
crc = INIT
for b in data:
crc ^= b
for _ in range(8):
if crc & 1:
crc = (crc >> 1) ^ POLY_REFLECTED
else:
crc >>= 1
crc &= 0xFFFFFFFF
return crc ^ XOROUT
# ========== 2. 生成基础查表(Slice-by-1) ==========
def make_table(poly: int):
"""生成标准反射 CRC-32 的 256 项查表(和 zlib 第一张表一致)"""
tbl = []
for n in range(256):
c = n
for _ in range(8):
if c & 1:
c = (c >> 1) ^ poly
else:
c >>= 1
tbl.append(c & 0xFFFFFFFF)
return tbl
# ========== 3. 生成 Slice-by-4 的 4 张表 ==========
def make_slice4_tables(poly: int):
"""
按照 zlib BYFOUR 逻辑生成 4 张表:
T0: 单字节表
T1/T2/T3: 对应该字节位于 4 字节块中更高字节位置时的影响
"""
T0 = make_table(poly)
T1 = [0] * 256
T2 = [0] * 256
T3 = [0] * 256
for n in range(256):
c = T0[n]
# k=1
c = T0[c & 0xFF] ^ (c >> 8)
T1[n] = c
# k=2
c = T0[c & 0xFF] ^ (c >> 8)
T2[n] = c
# k=3
c = T0[c & 0xFF] ^ (c >> 8)
T3[n] = c
return T0, T1, T2, T3
T0, T1, T2, T3 = make_slice4_tables(POLY_REFLECTED)
# ========== 4. 正确的 Slice-by-4 实现 ==========
def crc32_slice4(data: bytes) -> int:
"""
Slice-by-4 优化版本,等价于标准 CRC-32。
对齐 zlib 中 crc32_little() 的 DOLIT4 宏逻辑:
c ^= *(uint32_t *)buf;
c = t3[c & 0xff] ^ t2[(c >> 8) & 0xff] ^
t1[(c >> 16) & 0xff] ^ t0[c >> 24];
"""
crc = INIT
n = len(data)
i = 0
# 处理 4 字节块
while i + 4 <= n:
d = (
data[i]
| (data[i + 1] << 8)
| (data[i + 2] << 16)
| (data[i + 3] << 24)
)
crc ^= d
crc = (
T3[(crc ) & 0xFF] ^
T2[(crc >> 8 ) & 0xFF] ^
T1[(crc >> 16) & 0xFF] ^
T0[(crc >> 24) & 0xFF]
)
i += 4
# 处理剩余字节(退回 Slice-by-1)
while i < n:
crc = T0[(crc ^ data[i]) & 0xFF] ^ (crc >> 8)
i += 1
return crc ^ XOROUT
# ========== 5. 多块 CRC 合并:crc32_combine ==========
def gf2_matrix_times(mat, vec):
"""GF(2) 矩阵 * 向量"""
sum_ = 0
idx = 0
while vec:
if vec & 1:
sum_ ^= mat[idx]
vec >>= 1
idx += 1
return sum_
def gf2_matrix_square(square, mat):
"""square = mat^2 (GF(2))"""
for n in range(GF2_DIM):
square[n] = gf2_matrix_times(mat, mat[n])
def crc32_combine(crc1, crc2, len2):
"""
等价于 zlib 的 crc32_combine():
已有:
crc1 = CRC32(M1)
crc2 = CRC32(M2)
返回:
CRC32(M1 || M2)
len2 是 M2 的字节长度。
注意:crc1/crc2 是“最终 CRC”(已做 XOROUT)的值。
"""
if len2 <= 0:
return crc1
# odd/even 矩阵用于构造 x^(len2*8) 作用在 CRC 上的线性变换
odd = [0] * GF2_DIM
even = [0] * GF2_DIM
# one zero bit operator
odd[0] = POLY_REFLECTED
row = 1
for n in range(1, GF2_DIM):
odd[n] = row
row <<= 1
# two zero bits
gf2_matrix_square(even, odd)
# four zero bits
gf2_matrix_square(odd, even)
# 对 crc1 应用 len2*8 个“0 bit”(即 len2 个零字节)的变换
while True:
gf2_matrix_square(even, odd)
if len2 & 1:
crc1 = gf2_matrix_times(even, crc1)
len2 >>= 1
if len2 == 0:
break
gf2_matrix_square(odd, even)
if len2 & 1:
crc1 = gf2_matrix_times(odd, crc1)
len2 >>= 1
if len2 == 0:
break
# 合并
crc1 ^= crc2
return crc1
# ========== 6. 验证部分 ==========
if __name__ == "__main__":
data = b"123456789"
part1 = b"1234"
part2 = b"56789"
# 1) 三种方式对整串 "123456789"
c_bit = crc32_bitwise(data)
c_slice4 = crc32_slice4(data)
c_zlib = zlib.crc32(data) & 0xFFFFFFFF
print(f"bitwise : 0x{c_bit:08X}")
print(f"slice-4 : 0x{c_slice4:08X}")
print(f"zlib : 0x{c_zlib:08X}")
# 2) 分块计算 + 合并
c1 = zlib.crc32(part1) & 0xFFFFFFFF
c2 = zlib.crc32(part2) & 0xFFFFFFFF
c_full = zlib.crc32(data) & 0xFFFFFFFF
c_comb = crc32_combine(c1, c2, len(part2))
print(f"CRC(part1) = 0x{c1:08X}")
print(f"CRC(part2) = 0x{c2:08X}")
print(f"CRC(full) = 0x{c_full:08X}")
print(f"combine(p1,p2) = 0x{c_comb:08X}")
你在终端应看到:
bitwise : 0xCBF43926
slice-4 : 0xCBF43926
zlib : 0xCBF43926
CRC(part1) = 0x9BE3E0A3
CRC(part2) = 0x131DA070
CRC(full) = 0xCBF43926
combine(p1,p2) = 0xCBF43926

1万+

被折叠的 条评论
为什么被折叠?



