CRC算法详解

原文出处:http://blog.csdn.net/sparkliang/article/details/5671510

感谢作者的详解。转到此处以备学习。

作为blog再次发出来,详细描述一下CRC32算法的推导过程。

CRC算法的数学基础

CRC算法的数学基础就不再多啰嗦了,到处都是,简单提一下。它是以GF(2)多项式算术为数学基础的,GF(2)多项式中只有一个变量x,其系数也只有0和1,比如:

    1*x^6 + 0*x^5 + 1*x^4 + 0*x^3 + 0*x^2 +1*x^1 + 1*x^0

       =x^6 + x^4 + x + 1

加减运算不考虑进位和退位。说白了就是下面的运算规则:

    0+ 0 = 0    0 - 0 = 0

    0+ 1 = 1    0 - 1 = 1

    1+ 0 = 1    1 - 0 = 1

1 + 1 = 0    1 - 1 = 0
看看这个规则,其实就是一个异或运算。

每个生成多项式的系数只能是0或1,因此我们可以把它转化为二进制形式表示,比如g(x)=x^4 + x + 1,那么g(x)对应的二进制形式就是 10011,于是我们就把GF(2)多项式的除法转换成了二进制形式,和普通除法没有区别,只是加减运算没有进位和退位。

比如基于上述规则计算11010/1001,那么商是11,余数就是101,简单吧。

     

CRC校验的基本过程

采用CRC校验时,发送方和接收方用同一个生成多项式g(x) ,g(x)是一个GF(2)多项式,并且g(x)的首位和最后一位的系数必须为1。

CRC的处理方法是:发送方用发送数据的二进制多项式t(x)除以g(x),得到余数y(x)作为CRC校验码。校验时,以计算的校正结果是否为0为据,判断数据帧是否出错。设生成多项式是r阶的(最高位是x^r)具体步骤如下面的描述。

发送方:

1)在发送的m位数据的二进制多项式t(x)后添加r个0,扩张到m+ r位,以容纳r位的校验码,追加0后的二进制多项式为 T(x);

2)用T(x)除以生成多项式g(x),得到r位的余数y(x),它就是CRC校验码;

3)把y(x)追加到t(x)后面,此时的数据s(x)就是包含了CRC校验码的待发送字符串;由于s(x) = t(x) y(x),因此s(x)肯定能被g(x)除尽。

接收方:

1)接收数据n(x),这个n(x)就是包含了CRC校验码的m+r位数据;

2)计算n(x)除以g(x),如果余数为0则表示传输过程没有错误,否则表示有错误。从n(x)去掉尾部的r位数据,得到的就是原始数据。

生成多项式可不是随意选择的,数学上的东西就免了,以下是一些标准的CRC算法的生成多项式:

标准

生成多项式

16进制表示

CRC12

x^12 + x^11 + x^3 + x^2 + x + 1

0x80F

CRC16

x^16 + x^15 + x^2 + 1

0x8005

CRC16-CCITT

x^16 + x^12 + x^5 + 1

0x1021

CRC32

x^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 + 1

0x04C11DB7

原始的CRC校验算法

根据多项式除法,我们就可以得到原始的CRC校验算法。假设生成多项式g(x)是r阶的,原始数据存放在data中,长度为len个bit,reg是r+1位的变量。以CRC-4为例,生成多项式g(x)=x^4 + x + 1,对应了一个5bits的二进制数字10011,那么reg就是5 bits。

reg[1]表明reg的最低位,reg[r+1]是reg的最高位。

通过反复的移位和进行除法,那么最终该寄存器中的值去掉最高一位就是我们所要求的余数。所以可以将上述步骤用下面的流程描述:

  1. reg = 0;  
  2. data = data追加r个;  
  3. pos = 1;  
  4. while(pos <= len)  
  5. {  
  6.     if(reg[r+1] == 1) // 表明reg可以除以g(x)  
  7.     {  
  8.         // 只关心余数,根据上面的算法规则可知就是XOR运算  
  9.         reg = reg XOR g(x);  
  10.     }  
  11.     // 移出最高位,移入新数据  
  12.     reg = (reg<<1) | (data[pos]);  
  13.     pos++;  
  14. }  
  15. return reg; // reg中的后r位存储的就是余数  

改进一小步——从r+1到r

由于最后只需要r位的余数,所以我们可以尝试构造一个r位的reg,初值为0,数据data依次移入reg[1],同时把reg[r]移出 reg。

根据上面的算法可以知道,只有当移出的数据为1时,reg才和g(x)进行XOR运算;于是可以使用下面的算法:

 

  1. reg = 0;  
  2. data = data追加r个;  
  3. pos = 1;  
  4. while(pos < len)  
  5. {  
  6.     hi-bit = reg[r];  
  7.     // 移出最高位,移入新数据  
  8.     reg = (reg<<1) | (data[pos]);  
  9.     if(hi-bit == 1) // 表明reg可以除以g(x)  
  10.     {  
  11.         reg = reg XOR g(x);  
  12.     }  
  13.     pos++;  
  14. }  
  15. return reg; // reg中存储的就是余数  

这种算法简单,容易实现,对任意长度生成多项式的G(x)都适用,对应的CRC-32的实现就是:

  1. // 以4 byte数据为例  
  2. #define POLY 0x04C11DB7L // CRC32生成多项式  
  3. unsigned int CRC32_1(unsigned int data)  
  4. {  
  5.     unsigned char p[8];  
  6.     memset(p, 0, sizeof(p));  
  7.     memcpy(p, &data, 4);  
  8.     unsigned int reg = 0, idx = 0;  
  9.     for(int i = 0; i < 64; i++)  
  10.     {  
  11.         idx = i/8;  
  12.         int hi = (reg>>31)&0x01; // 取得reg的最高位  
  13.         // 把reg左移1bit,并移入新数据到reg0  
  14.         reg = (reg<<1)| (p[idx]>>7);  
  15.         if(hi) reg = reg^POLY; // hi=1就用reg除以g(x)  
  16.         p[idx]<<=1;  
  17.     }  
  18.     return reg;  
  19. }  

从bit扩张到byte的桥梁

但是如果发送的数据块很长的话,这种方法就不太适合了。它一次只能处理一个bit的数据,效率太低。考虑能不能每次处理一个byte的数据呢?事实上这也是当前的CRC-32实现采用的方法。

这一步骤是通往基于校验表方法的桥梁,让我们一步一步来分析上面逐bit的运算方式,我们把reg和g(x)都采用bit的方式表示如下:   


考虑把上面逐bit的算法执行8次,如果某次移出的不是1,那么reg不会和g(x)执行XOR运算,事实上这相当于将reg和0执行了XOR运算。执行过程如下所示,根据hi-bit的值,这里的G可能是g(x)也可能是0。


从上面的执行过程清楚的看到,执行8次后,old-reg的高8bit被完全移出,new-reg就是old-reg的低24bit和数据data新移入的8bit和G一次次执行XOR运算所得到的。

       XOR运算满足结合律,那就是:A XOR B XOR C = A XOR (BXOR C),于是我们可以考虑把上面的运算分成两步进行:

1)先执行R高8bit与G之间的XOR运算,将计算结果存入X中,如下面的过程所示。


2)将R左移8bit,并移入8bit的数据,得到的值就是,然后再与X做XOR运算。

根据XOR运算的结合率,最后的结果就等于上面逐bit的算法执行8次后的结果,根据这个分解,我们可以修改逐bit的方式,写出下面的算法。

 

  1. // 以4 byte数据为例  
  2. #define POLY 0x04C11DB7L // CRC32生成多项式  
  3. unsigned int CRC32_2(unsigned int data)  
  4. {  
  5.     unsigned char p[8];  
  6.     memset(p, 0, sizeof(p));  
  7.     memcpy(p, &data, 4);  
  8.     unsigned int reg = 0, sum_poly = 0;  
  9.     for(int i = 0; i < 8; i++)  
  10.     {  
  11.         // 计算步骤1  
  12.         sum_poly = reg&0xFF000000;  
  13.         for(int j = 0; j < 8; j++)  
  14.         {  
  15.             int hi = sum_poly&0x80000000; // 测试reg最高位  
  16.             sum_poly <<= 1;  
  17.             if(hi) sum_poly = sum_poly^POLY;  
  18.         }  
  19.         // 计算步骤2  
  20.         reg = (reg<<8)|p[i];  
  21.         reg = reg ^ sum_poly;  
  22.     }  
  23.     return reg;  
  24. }  

 

初见Table-Driven

 

变换到上面的方法后,我们离table-driven的方法只有一步之遥了,我们知道一个字节能表示的正整数范围是0~255,步骤1中的计算就是针对reg的高Byte位进行的,于是可以被提取出来,预先计算并存储到一个有256项的表中,于是下面的算法就出炉了,这个和上面的算法本质上并没有什么区别。

  1. #define POLY 0x04C11DB7L // CRC32生成多项式  
  2. static unsigned int crc_table[256];  
  3. unsigned int get_sum_poly(unsigned char data)  
  4. {  
  5.     unsigned int sum_poly = data;  
  6.     sum_poly <<= 24;  
  7.     for(int j = 0; j < 8; j++)  
  8.     {  
  9.         int hi = sum_poly&0x80000000; // 取得reg的最高位  
  10.         sum_poly <<= 1;  
  11.         if(hi) sum_poly = sum_poly^POLY;  
  12.     }  
  13.     return sum_poly;  
  14. }  
  15. void create_crc_table()  
  16. {  
  17.     for(int i = 0; i < 256; i++)  
  18.     {  
  19.         crc_table[i] = get_sum_poly(i&0xFF);  
  20.     }  
  21. }  
  22. // 以byte数据为例  
  23. unsigned int CRC32_3(unsigned int data)  
  24. {  
  25.     unsigned char p[8];  
  26.     memset(p, 0, sizeof(p));  
  27.     memcpy(p, &data, 4);  
  28.     unsigned int reg = 0, sum_poly = 0;  
  29.     for(int i = 0; i < 8; i++)  
  30.     {  
  31.         // 计算步骤1  
  32.         sum_poly = crc_table[(reg>>24)&0xFF];  
  33.         // 计算步骤2  
  34.         reg = (reg<<8)|p[i];  
  35.         reg = reg ^ sum_poly;  
  36.     }  
  37.     return reg;  
  38. }  

更进一步

 

上面的这个算法已经是一个Table-Driven的CRC-32算法了,但是实际上我们看到的CRC校验代码都是如下的形式:


 
 
  1. r=0;  
  2. while(len--)  
  3.      r = (r<<8) ^ t[(r >> 24) ^ *p++];  

下面我们将看看是做了什么转化而做到这一点的。

 

 

首先上述CRC算法中,我们需要为原始数据追加r/8Byte个0, CRC-32就是4Byte。或者我们可以再计算原始数据之后,把0放在后面单独计算,像这样:

  1. // 先计算原始数据  
  2. for(int i = 0; i < len; i++)  
  3. {  
  4.         sum_poly = crc_table[(reg>>24)&0xFF];  
  5.         reg = (reg<<8)|p[i];  
  6.         reg = reg ^ sum_poly;  
  7. }  
  8. // 再计算追加的4Byte 0  
  9. for(int i = 0; i < 4; i++)  
  10. {  
  11.         reg = (reg<<8) ^ crc_table[(reg>>24)&0xFF];  
  12. }  

这看起来已经足够好了,而事实上我们可以继续向下进行以免去为了附加的0而进行计算。在上面算法中,最后的4次循环是为了将输入数据的最后r/8位都移出reg,因为0对reg的值并没有丝毫影响。

对于CRC-32,对于任何输入数据Dn...D8…D5D4…D1,第一个for循环将Dn…D8…D5都依次移入,执行XOR运算再移出reg;并将D4…D1都移入了reg,但是并未移出;因此最后的4次循环是为了将D4…D1都移出reg。

Di与Ri执行XOR运算后值将会更新,设更新后的值表示为Di’,不论执行了多少次XOR运算。

如果reg初始值是0,那么第一个for循环中开始的4次循环干的事情就是,把Dn…Dn-3移入到reg中(与0做XOR结果不变),执行4次后reg的值就是Dn.Dn-1.Dn-2.Dn-3;

第5次循环的结果就是:reg =crc_table[Dn] ^ Dn-1.Dn-2.Dn-3.Dn-4;

第6次循环的结果就是:reg =crc_table[Dn-1’] ^ Dn-2’.Dn-3’.Dn-4;Dn移出reg。

因此上面的计算可以分为3个阶段:

1)前4次循环,将Dn.Dn-1.Dn-2.Dn-3装入reg;

2)中间的n-4次循环,依次将Di移入reg,在随后的4次循环中,依次计算Di+4,Di+3,Di+2和Di+1对Di的影响;最后移出reg; 

3)最后的4次循环,实际上是为了计算D4,D3,D2和D1都能执行第2步的过程;

具体考察Di:

1)Di移入到reg中,R1=Di,接着与crc_table[R4]执行XOR运算;

2)循环4次后,Di成为reg的最高位R4,并且因为受到了Dn…Di+1的影响而更新为Di’;

上面的运算步骤如下面所示,其中F是对应得crc_table[R]的值。

可以清晰的看到,最后reg的高Byte是Di和F之间一次次XOR运算的结果。依然根据XOR运算的结合律,我们可以分两步走:

1) 先执行F之间的XOR运算,设结果为FF,它就是reg的首字节;

2) 然后再直接将Di和FF进行XOR运算,并根据结果查CRC表;

3) 计算出XOR运算后,Di…Di-3已经移入reg;因此再将查表结果和(reg<<8)执行XOR运算即可;

这就是方法2,于是我们的table-driven的CRC-32校验算法就可以写成如下的方式了:

  1. reg = 0;  
  2. for(int i = 0; i < len; i++)  
  3. {  
  4.     reg = (reg<<8) ^ crc_table[(reg>>24)&0xFF ^ p[i]];  

郁闷的位逆转

看起来我们已经得到CRC-32算法的最终形式了,可是、可是在实际的应用中,数据传输时是低位先行的;对于一个字节Byte来讲,传输将是按照b1,b2,...,b8的顺序。而我们上面的算法是按照高位在前的约定,不管是reg还是G(x),g32,g31,...,g1;b8,b7,...,b1; r32,r31,...,r1。

先来看看前面从bit转换到Byte一节中for循环的逻辑:

  1.               sum_poly = reg&0xFF000000;  
  2. for(int j = 0; j < 8; j++)  
  3. {  
  4.     int hi = sum_poly&0x80000000; // 测试reg最高位  
  5.     sum_poly <<= 1;  
  6.     if(hi) sum_poly = sum_poly^POLY;  
  7. }  
  8. // 计算步骤2  
  9. reg = (reg<<8)|p[i];  
  10. reg = reg ^ sum_poly;  

在这里的计算中,p[i]是按照p8,p7,...,p1的顺序;如果p[i]在这里变成了p1,p2,...,p8的顺序;那么reg也应该是r1,r2,...,r32的顺序,同样G(x)和sum_poly也要逆转顺序。转换后的G(x)= POLY = 0xEDB88320。

于是取reg的最高位的sum_poly的初值就从sum_poly= reg & 0xFF000000变成了sum_poly= reg & 0xFF,测试reg的最高位就从sum_poly & 0x80000000变成了sum_poly&0x01;

移出最高位也就从sum_poly<<=1变成了sum_poly>>=1;于是上面的代码就变成了如下的形式:

  1.               sum_poly = reg&0xFF;  
  2. for(int j = 0; j < 8; j++)  
  3. {  
  4.     int hi = sum_poly&0x01; // 测试reg最高位  
  5.     sum_poly >>= 1;  
  6.     if(hi) sum_poly = sum_poly^POLY;  
  7. }  
  8. // 计算步骤2  
  9. reg = (reg<<8)|p[i];  
  10. reg = reg ^ sum_poly;  

为了清晰起见,给出完整的代码:

  1. // 以4 byte数据为例  
  2. #define POLY 0xEDB88320L // CRC32生成多项式  
  3. unsigned int CRC32_2(unsigned int data)  
  4. {  
  5.     unsigned char p[8];  
  6.     memset(p, 0, sizeof(p));  
  7.     memcpy(p, &data, 4);  
  8.     unsigned int reg = 0, sum_poly = 0;  
  9.     for(int i = 0; i < 8; i++)  
  10.     {  
  11.         // 计算步骤1  
  12.         sum_poly = reg&0xFF;  
  13.         for(int j = 0; j < 8; j++)  
  14.         {  
  15.             int hi = sum_poly&0x01; // 测试reg最高位  
  16.             sum_poly >>= 1;  
  17.             if(hi) sum_poly = sum_poly^POLY;  
  18.         }  
  19.         // 计算步骤2  
  20.         reg = (reg<<8)|p[i];  
  21.         reg = reg ^ sum_poly;  
  22.     }  
  23.     return reg;  
  24. }  

依旧像上面的思路,把计算sum_poly的代码段提取出来,生成256个元素的CRC校验表,再修改追加0的逻辑,最终的代码版本就完成了,为了对比;后面给出了字节序逆转前的完整代码段。

  1. // 字节逆转后的CRC32算法,字节序为b1,b2,…,b8  
  2. #define POLY 0xEDB88320L // CRC32生成多项式  
  3. static unsigned int crc_table[256];  
  4. unsigned int get_sum_poly(unsigned char data)  
  5. {  
  6.     unsigned int sum_poly = data;  
  7.     for(int j = 0; j < 8; j++)  
  8.     {  
  9.         int hi = sum_poly&0x01; // 取得reg的最高位  
  10.         sum_poly >>= 1;  
  11.         if(hi) sum_poly = sum_poly^POLY;  
  12.     }  
  13.     return sum_poly;  
  14. }  
  15. void create_crc_table()  
  16. {  
  17.     for(int i = 0; i < 256; i++)  
  18.     {  
  19.         crc_table[i] = get_sum_poly(i&0xFF);  
  20.     }  
  21. }   
  22. unsigned int CRC32_4(unsigned char* data, int len)  
  23. {  
  24.     unsigned int reg = 0; // 0xFFFFFFFF,见后面解释  
  25.     for(int i = 0; i < len; i++)  
  26.     {  
  27.         reg = (reg<<8) ^ crc_table[(reg&0xFF) ^ data[i]];  
  28.         return reg;  
  29.     }  
  30. }  
  31. // 最终生成的校验表将是:  
  32. // {0x00000000,  0x77073096,  0xEE0E612C,  0x990951BA,  
  33. //  0x076DC419,  0x706AF48F,  0xE963A535,  0x9E6495A3,  
  34. // … …}   
  35. // 字节逆转前的CRC32算法,字节序为b8,b7,…,b1  
  36. #define POLY 0x04C11DB7L // CRC32生成多项式  
  37. static unsigned int crc_table[256];  
  38. unsigned int get_sum_poly(unsigned char data)  
  39. {  
  40.     unsigned int sum_poly = data;  
  41.     sum_poly <<= 24;  
  42.     for(int j = 0; j < 8; j++)  
  43.     {  
  44.         int hi = sum_poly&0x80000000; // 取得reg的最高位  
  45.         sum_poly <<= 1;  
  46.         if(hi) sum_poly = sum_poly^POLY;  
  47.     }  
  48.     return sum_poly;  
  49. }  
  50. void create_crc_table()  
  51. {  
  52.     for(int i = 0; i < 256; i++)  
  53.     {  
  54.         crc_table[i] = get_sum_poly(i&0xFF);  
  55.     }  
  56. }   
  57. unsigned int CRC32_4(unsigned char* data, int len)  
  58. {  
  59.     unsigned int reg = 0;// 0xFFFFFFFF,见后面解释  
  60.     for(int i = 0; i < len; i++)  
  61.     {  
  62.         reg = (reg<<8) ^ crc_table[(reg>>24)&0xFF ^ data[i]];  
  63.         return reg;  
  64.     }  
  65. }  

长征结束了

到这里长征终于结束了,事实上,还有最后的一小步,那就是reg初始值的问题,上面的算法中reg初始值为0。在一些传输协议中,发送端并不指出消息长度,而是采用结束标志,考虑下面的这几种可能的差错:

1)在消息之前,增加1个或多个0字节;

2) 在消息(包括校验码)之后,增加1个或多个0字节;

显然,这几种差错都检测不出来,其原因就是如果reg=0,处理0消息字节(或位),reg的值保持不变。解决这种问题也很简单,只要使reg的初始值非0即可,一般取0Xffffffff,就像你在很多CRC32实现中发现的那样。

到这里终于可以松一口气了,CRC32并不是像想象的那样容易的算法啊!事实上还真不容易!这就叫做“简单的前面是优雅,背后是复杂”!


  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值