81、实战:CRC校验与哈希(循环冗余校验+MD5模拟)
数据完整性校验,说白了就是确保你收到的数据跟发送出去的一模一样。我在做嵌入式通信项目时,经常遇到数据被干扰的情况——比如传感器传回来的温度值突然变成-273度,那肯定是有比特位翻转了。这时候CRC和哈希就派上用场了。
这一讲,我们手写一个CRC校验,再模拟一个简化版的MD5。嗯,别被“MD5模拟”吓到,我们只取其核心思想,不搞完整实现。
一、CRC校验:循环冗余校验
CRC的原理其实不复杂。你把数据看作一个二进制多项式,然后跟一个约定的“生成多项式”做模2除法,余数就是CRC校验码。接收方用同样的多项式再除一次,如果余数为0,数据大概率没问题。
我个人习惯用查表法实现CRC,因为效率高。直接计算虽然直观,但每个字节都要移位判断,在高速通信中扛不住。
1.1 CRC-8 查表法实现
先看一个简单的CRC-8。生成多项式我用0x07(对应x^8 + x^2 + x + 1)。
#include <stdio.h>
#include <stdint.h>
// 生成CRC-8查表
void crc8_init_table(uint8_t table[256], uint8_t poly) {
for (int i = 0; i < 256; i++) {
uint8_t crc = i;
for (int j = 0; j < 8; j++) {
if (crc & 0x80)
crc = (crc << 1) ^ poly;
else
crc <<= 1;
}
table[i] = crc;
}
}
// 计算CRC-8
uint8_t crc8_calc(const uint8_t *data, size_t len, const uint8_t table[256]) {
uint8_t crc = 0;
for (size_t i = 0; i < len; i++) {
crc = table[crc ^ data[i]];
}
return crc;
}
int main() {
uint8_t table[256];
crc8_init_table(table, 0x07);
uint8_t test[] = "Hello, CRC!";
uint8_t crc_val = crc8_calc(test, sizeof(test) - 1, table);
printf("CRC-8: 0x%02X\n", crc_val);
return 0;
}
1.2 CRC-16 与 CRC-32
CRC-8只能检测8位以内的错误,实际项目中更常用CRC-16或CRC-32。比如Modbus协议用CRC-16,以太网用CRC-32。原理一样,只是多项式位数更多,查表更大(CRC-16需要65536个表项,但通常用256字节的查表法,分高低字节处理)。
我曾经在一个工业控制项目中,因为用了CRC-8,结果两个不同错误的数据帧居然通过了校验。后来换成CRC-16,再没出过问题。所以选型时别省那点计算量,安全第一。
二、MD5模拟:理解哈希的核心思想
MD5是哈希函数,把任意长度的数据映射成128位的摘要。真正的MD5有64轮复杂运算,我们这里模拟一个简化版——保留“分组处理”和“非线性函数”这两个核心思想。
2.1 简化版MD5设计
我们的模拟版只做4轮运算,每轮用不同的非线性函数。输入数据先填充到512位的整数倍,然后每512位(64字节)一组处理。
#include <stdio.h>
#include <stdint.h>
#include <string.h>
// 左旋函数
#define LEFT_ROTATE(x, n) (((x) << (n)) | ((x) >> (32 - (n))))
// 模拟MD5的四个非线性函数
#define F(x, y, z) (((x) & (y)) | ((~x) & (z)))
#define G(x, y, z) (((x) & (z)) | ((y) & (~z)))
#define H(x, y, z) ((x) ^ (y) ^ (z))
#define I(x, y, z) ((y) ^ ((x) | (~z)))
// 模拟一轮处理
void simd5_round(uint32_t *a, uint32_t b, uint32_t c, uint32_t d,
uint32_t k, uint32_t s, uint32_t i, uint32_t func_sel) {
uint32_t f;
switch (func_sel) {
case 0: f = F(b, c, d); break;
case 1: f = G(b, c, d); break;
case 2: f = H(b, c, d); break;
case 3: f = I(b, c, d); break;
default: f = 0;
}
*a = b + LEFT_ROTATE(*a + f + k + i, s);
}
// 模拟MD5主循环(简化版,只做4轮,每轮4步)
void simd5_hash(const uint8_t *input, size_t len, uint32_t output[4]) {
// 初始向量(跟标准MD5一样)
uint32_t h0 = 0x67452301;
uint32_t h1 = 0xEFCDAB89;
uint32_t h2 = 0x98BADCFE;
uint32_t h3 = 0x10325476;
// 这里简化处理:假设输入正好64字节,不处理填充
// 实际MD5需要填充到512位倍数
uint32_t *words = (uint32_t *)input;
uint32_t a = h0, b = h1, c = h2, d = h3;
// 第1轮(4步)
simd5_round(&a, b, c, d, words[0], 7, 0xD76AA478, 0);
simd5_round(&d, a, b, c, words[1], 12, 0xE8C7B756, 0);
simd5_round(&c, d, a, b, words[2], 17, 0x242070DB, 0);
simd5_round(&b, c, d, a, words[3], 22, 0xC1BDCEEE, 0);
// 第2轮(4步)
simd5_round(&a, b, c, d, words[1], 5, 0xF61E2562, 1);
simd5_round(&d, a, b, c, words[2], 9, 0xC040B340, 1);
simd5_round(&c, d, a, b, words[3], 14, 0x265E5A51, 1);
simd5_round(&b, c, d, a, words[0], 20, 0xE9B6C7AA, 1);
// 第3轮(4步)
simd5_round(&a, b, c, d, words[2], 4, 0xD62F105D, 2);
simd5_round(&d, a, b, c, words[3], 11, 0x02441453, 2);
simd5_round(&c, d, a, b, words[0], 16, 0xD8A1E681, 2);
simd5_round(&b, c, d, a, words[1], 23, 0xE7D3FBC8, 2);
// 第4轮(4步)
simd5_round(&a, b, c, d, words[3], 6, 0x289B7EC6, 3);
simd5_round(&d, a, b, c, words[0], 10, 0xEAA127FA, 3);
simd5_round(&c, d, a, b, words[1], 15, 0xD4EF3085, 3);
simd5_round(&b, c, d, a, words[2], 21, 0x04881D05, 3);
// 累加结果
output[0] = h0 + a;
output[1] = h1 + b;
output[2] = h2 + c;
output[3] = h3 + d;
}
int main() {
uint8_t test[64] = {0};
memcpy(test, "Hello, SimMD5!", 15);
uint32_t digest[4];
simd5_hash(test, 64, digest);
printf("SimMD5: ");
for (int i = 0; i < 4; i++)
printf("%08x", digest[i]);
printf("\n");
return 0;
}
三、CRC与哈希的对比
你可能会问:CRC和哈希到底选哪个?我根据经验总结了一张表:
| 特性 | CRC | 哈希(如MD5) |
|---|---|---|
| 主要用途 | 检测随机错误(传输/存储) | 数据完整性验证、数字签名 |
| 抗碰撞性 | 弱(故意构造碰撞很容易) | 强(MD5已不推荐,但比CRC强得多) |
| 计算速度 | 极快(硬件支持) | 较快(软件实现) |
| 输出长度 | 8/16/32/64位 | 128/160/256/512位 |
| 典型场景 | 通信协议、存储校验 | 文件校验、密码存储 |
说白了,CRC是“防意外”的,哈希是“防人为”的。你在串口通信里用CRC就够了,但下载文件时最好用SHA256验证完整性。
四、核心逻辑流程图
下面这张图展示了CRC和哈希在数据校验中的整体流程。我特意把两个流程并排放,方便对比。
五、避坑指南与实战建议
我在项目中踩过不少CRC和哈希的坑,这里分享几个最痛的:
- 多项式选错:我曾经用了一个非标准的多项式,结果跟硬件CRC模块对不上。后来老老实实查标准——CRC-16/CCITT、CRC-32/以太网,这些都有国际标准,别自己发明。
- 初始值问题:CRC的初始值可以是0x0000或0xFFFF,不同协议不一样。我见过两个模块因为初始值不同,互相不认对方的数据。
- 哈希不要用于密码存储:MD5和SHA1已经被破解,存密码要用bcrypt或Argon2。我有个朋友把用户密码用MD5存了,结果数据库泄露后,所有密码被彩虹表秒破。
- 性能考量:如果你在单片机(比如STM32)上做CRC,很多芯片有硬件CRC模块,直接调用寄存器就行,比软件实现快几十倍。别傻乎乎自己写查表法。
嗯,这一讲就到这里。代码可以直接拿去用,但记得根据你的项目调整多项式和初始值。下次遇到数据校验的问题,你应该知道怎么选了。