CRC(Cyclic Redundancy Check)中文名是循环冗余校验,由于计算简单等,被广泛用于数据校验,具有很强的检错能力。最常见的有网络传输中信息的校验,但同样的道理,我们也可以将其应用到软件中,如:winrar对文件的校验。另外,我们还可以将它应用到软件上面来保护软件被恶意修改,还可以用在内存动态校验上面。用途多多,变通方式也不一,但基本原理都是这个。
CRC算法的是以GF(2)(2元素伽罗瓦域)多项式算术为数学基础的,嗯,听着是有点恐怖,但别被吓着了,其实说白了也很简单。虽然我们大可跳过数学部分用计算机的术语来描述,但我依旧不想跳过这部分,以防后来者忽略数学在计算机中的重要性,能看到数学在计算机的应用也能鼓舞起他们学习数学的信心。无可置疑,很多算法都源于数学,都以数学为支撑,数学功底在一定程度上决定你能走到的高度。
首先,我们来介绍一下2元素伽罗瓦域及其基本运算。
GF(2)多项式中只有一个变量x,其系数也只有0和1,如:
1*x^6 + 1*x^5 + 0*x^4 + 0*x^3 + 1*x^2 +1*x^1 + 1*x^0
即:
x^6 + x^5 + x^2 + x + 1
(x^n表示x的n次幂)
GF(2)的多项式加减运算其实都一样:模2运算,运算时不考虑借位和进位。咋一听好像很高级似的,其实说白了就是异或运算。
0 + 0 = 0 0 - 0 = 0
0 + 1 = 1 0 - 1 = 1
1 + 0 = 1 1 - 0 = 1
1 + 1 = 0 1 - 1 = 0
比如:P1 = x^6 +x^5 + x^2+ x + 1,P2= x^3+ x^2 + 1。P1 + P2 = x^6+ x^5 + x^3+ x。即对应项想加减。
GF(2)的多项式乘除运算也与一般的多项式大体一样,只是加减时进行的是模2运算。不再详述,待会看下去就行。
CRC的数学基础就说到这里吧。再说下去只会让大家都懵了,我们还是换一些直白点的语言来描述下CRC原理。
在小学我们就知道,乘法可以通过一直加某个数来得到结果,同样除法也可以通过一直减某个数来得到结果。看下下面两个式子:
9/3=3 (余数是0),即9 – 3 – 3 –3 = 0
9/6 = 1 (余数是3),即9 – 6 = 3
用二进制来表示就是:
1001 -->9
0011 - -->3
------------------------------------------------
0110 -->6
0011 - -->3
------------------------------------------------
0011 -->3
0011 - -->3
------------------------------------------------
0000 -->余数
减了三次,显然商是3,而最后刚好减完,余数为0。
再来看下另外一个:
1001 -->9
0110 - -->6
------------------------------------------------
0011 -->3 , 余数
这次商是1,余数是3。
很容易吧。但上面的是有借位的,并不是我们之前说的2元素伽罗瓦域的减法。我们再来看看2元素伽罗瓦域的除法,也即CRC的运算。
如果忽略借位,那8/3 =?……?
1000 -->8
11 - -->3
------------------------------------------------
100 -->4
11 - -->3
------------------------------------------------
10 -->2
11 - -->3
------------------------------------------------
1 -->1 , 余数 ……The CRC
好奇怪,是吧,为什么从高位开始减呢?我们还原成除法就不奇怪了。
111 -->商
----------
11)1000
11
-------------------
100
11
--------------------
10
11
--------------------
1 -->余数,The CRC
普通的除法可以还原为熟悉的减法,但2元素伽罗瓦域的除法记得注意点了。从高位开始减。另外,余数得比除数的字长少,以二进制来准。不懂?11的字长是2,1的字长是1,这样懂了吧。而余数就是我们所关心的CRC。
进行一个CRC运算我们需要选择一个除数,这个除数我们叫它为“poly”(生成项),宽度W就是最高位的位置,所以我刚才举的例子中的除数3,这个poly 11的W是1,而不是2。注意最高位总是1,就像普通的数字小数点前面的0可以忽略一样。这里还要注意宽度与字长的区别,字长是从1开始计算的,而宽度是从0开始计算的。
现在我们再来看看改进后的CRC除法。
Poly = 1001,宽度W = 3
位串Bitstring = 11110
Bitstring + W zeroes = 11110 + 000 = 11110000
11110000
1001|||| -
-------------
1100|||
1001||| -
------------
1010||
1001|| -
-----------
0110|
0000| -
----------
1100
1001 -
---------
101 --> 5,余数 --> The CRC!
最后,还有两点要注意一下。咋这么多注意啊,别急,我们慢慢来。
1、只有当Bitstring的最高位为1,我们才将它与Poly进行XOR运算,否则我们只是将Bitstring左移一位。其实这很好理解,想下除法运算就明白了,这里只是用简单的减法来代替而已。
2、XOR运算的结果就是被操作位串Bitstring与Poly的低W位进行XOR运算,因为最高位总为0。
好了,以上就是CRC的原理了,晕了?那就再看一遍吧。其实也很好理解,也就一个XOR亦或运算。
明白了原理,我们还得讲它转化为程序的思想啊,我们用计算机的思维再来理下头绪看看。
我们假设待测数据是1101 0110 11,生成项Poly是10011,假设有一个4 bits的寄存器,通过反复的移位和进行CRC的除法,最终该寄存器中的值就是我们所要求的余数。
3 2 1 0 Bits
+---+---+---+---+
Pop<-- | | | | | <----- Augmented message(已加0扩张的原始数据)
+---+---+---+---+
1 0 0 1 1 = The Poly 生成项
依据这个模型,我们得到了一个最简单的算法:
1、把register中的值置0.
2、把原始的数据后添加w个0.
While (还有剩余没有处理的数据)
Begin
把register中的值左移一位,读入一个新的数据并置于register最低位的位置。
If (如果上一步的左移操作中的移出的一位是1)
register = register XOR Poly.
End
这里的Poly是10011,字长是5,位宽w是4,则生成最后的CRC的位宽也是4,所以,我们用一个4位的寄存器就可以了。另外,目标位串最高位如果是0的话,则是简单的移出去。真正参与运算的是1,而1 xor 1 = 0。所以我们可以将Poly最前面的1忽略,以及将Register移出的最高位忽略,因为这里的运算总是0。
唔~~,好像都解释一遍了吧,自己在脑海里面模拟下这个过程。还不懂的就回头再仔细看看,一些寄存器之类术语不懂的,我也不想解释了,百度一下吧。
下面看下完整的程序:
#include "StdAfx.h"
#include "stdio.h"
int main()
{
char POLY=0x13; //生成项Poly,0x13=10011b,这样CRC是4bit
unsigned short data = 0x035B; //待测数据是0x35B = 1101011011b,12bit,注意,数据不是16bit
unsigned short regi = 0x0000; //用0来填充寄存器的值,防止原先的垃圾值影响结果
//按CRC计算的定义,待测数据后加入4位 0,以容纳4bit的CRC;
//这样共有16bit待测数据,从第5bit开始做除法,就要做16-5+1=12次XOR
data <<= 4;
// 按二进制一位一位地开始计算
for ( int cur_bit = 15; cur_bit >= 0; -- cur_bit ) //处理16次,前4次实际上只是加载数据
{
//判断最高位是否为1
if ( ( ( regi >> 4 ) & 0x0001 ) == 0x1 ) regi = regi ^ POLY;
regi <<= 1; // 寄存器左移一位
// 读取目标位串下一位
unsigned short tmp = ( data >> cur_bit ) & 0x0001; //加载待测数据1bit到tmp中,tmp只有1bit
regi |= tmp; //这1bit加载到寄存器中
}
if ( ( ( regi >> 4 ) & 0x0001 ) == 0x1 ) regi = regi ^ POLY; //做最后一次XOR
//这时, regi中的值就是CRC
printf("0x%08X\n",regi);
return 0;
}
运行后打印出结果0x0000000E,也就是二进制的1110b,结果正确。
唔~~,原理说了,算法描述了,程序也给出来了。好像都完成了。其实不然,上面这个算法效率比较低,每次只能处理1bit,这速度得计算到什么时候啊,虽然计算机速度快,但也经不起耗啊,浪费资源,可耻。
那如何改进呢?如果我们能一次性运算一字节是不是就会快点了呢?显然的,但1 xor 1 =0,这很显然的,一字节怎么xor呢,一字节进行异或可以有256种结果,该怎么做好呢?我们可以先建立一张表,把这些结果都计算出来,然后放到这张表里面,为了方便索引,我们用寄存器移出的那一字节位串来进行索引。聪明的你会说,不对啊,结果还跟Poly生成项有关啊,毕竟xor是个二元运算。呵呵,Poly是一定的,我们只要根据我们程序使用的Poly来生成一个表就行了,或者事先在程序保存下来。在速度改进的同时,空间占用会大一点,不过这点空间相对来说还是可以忽略的。
显然,不同的生成项检错能力也不同,找到一个好的生成项需要具备比较好的数学基础。但不用担心,很多先驱已经为我们做好这件事。
以下是一些标准的CRC算法的生成多项式:
标准 | 生成多项式 | 16进制表示 |
CRC12 | x^12 + x^11 + x^3 + x^2 + x + 1 | 80F |
CRC16 | x^16 + x^15 + x^2 + 1 | 8005 |
CRC16-CCITT | x^16 + x^12 + x^5 + 1 | 1021 |
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 | 04C11DB7 |
其实,这个索引表,网上很多的地方都不一样,很杂乱,很多人都是根据自己的理解来生成一个CRC表,这样也可以满足基本的校验需要。其实这个表有好几种,这里只介绍一种,正规查询表,也就是被正式引用的一种。Winrar对生成的压缩包里面的每个文件都有一个CRC32校验值,那就是用正规查询表来生成的。但还不止这样,还有各种颠倒(reflect)啊之类的,其实正规查询表本身就是经过反射(reflect)的,有兴趣的再下载附件里面的资料来看看。这里只给出实现。
先看看CRC32的正规查询表吧
unsigned LONG CRC32Table[256] = {
0x00000000,0x77073096,0xEE0E612C,0x990951BA,0x076DC419,0x706AF48F,0xE963A535,0x9E6495A3,
0x0EDB8832,0x79DCB8A4,0xE0D5E91E,0x97D2D988,0x09B64C2B,0x7EB17CBD,0xE7B82D07,0x90BF1D91,
0x1DB71064,0x6AB020F2,0xF3B97148,0x84BE41DE,0x1ADAD47D,0x6DDDE4EB,0xF4D4B551,0x83D385C7,
0x136C9856,0x646BA8C0,0xFD62F97A,0x8A65C9EC,0x14015C4F,0x63066CD9,0xFA0F3D63,0x8D080DF5,
0x3B6E20C8,0x4C69105E,0xD56041E4,0xA2677172,0x3C03E4D1,0x4B04D447,0xD20D85FD,0xA50AB56B,
0x35B5A8FA,0x42B2986C,0xDBBBC9D6,0xACBCF940,0x32D86CE3,0x45DF5C75,0xDCD60DCF,0xABD13D59,
0x26D930AC,0x51DE003A,0xC8D75180,0xBFD06116,0x21B4F4B5,0x56B3C423,0xCFBA9599,0xB8BDA50F,
0x2802B89E,0x5F058808,0xC60CD9B2,0xB10BE924,0x2F6F7C87,0x58684C11,0xC1611DAB,0xB6662D3D,
0x76DC4190,0x01DB7106,0x98D220BC,0xEFD5102A,0x71B18589,0x06B6B51F,0x9FBFE4A5,0xE8B8D433,
0x7807C9A2,0x0F00F934,0x9609A88E,0xE10E9818,0x7F6A0DBB,0x086D3D2D,0x91646C97,0xE6635C01,
0x6B6B51F4,0x1C6C6162,0x856530D8,0xF262004E,0x6C0695ED,0x1B01A57B,0x8208F4C1,0xF50FC457,
0x65B0D9C6,0x12B7E950,0x8BBEB8EA,0xFCB9887C,0x62DD1DDF,0x15DA2D49,0x8CD37CF3,0xFBD44C65,
0x4DB26158,0x3AB551CE,0xA3BC0074,0xD4BB30E2,0x4ADFA541,0x3DD895D7,0xA4D1C46D,0xD3D6F4FB,
0x4369E96A,0x346ED9FC,0xAD678846,0xDA60B8D0,0x44042D73,0x33031DE5,0xAA0A4C5F,0xDD0D7CC9,
0x5005713C,0x270241AA,0xBE0B1010,0xC90C2086,0x5768B525,0x206F85B3,0xB966D409,0xCE61E49F,
0x5EDEF90E,0x29D9C998,0xB0D09822,0xC7D7A8B4,0x59B33D17,0x2EB40D81,0xB7BD5C3B,0xC0BA6CAD,
0xEDB88320,0x9ABFB3B6,0x03B6E20C,0x74B1D29A,0xEAD54739,0x9DD277AF,0x04DB2615,0x73DC1683,
0xE3630B12,0x94643B84,0x0D6D6A3E,0x7A6A5AA8,0xE40ECF0B,0x9309FF9D,0x0A00AE27,0x7D079EB1,
0xF00F9344,0x8708A3D2,0x1E01F268,0x6906C2FE,0xF762575D,0x806567CB,0x196C3671,0x6E6B06E7,
0xFED41B76,0x89D32BE0,0x10DA7A5A,0x67DD4ACC,0xF9B9DF6F,0x8EBEEFF9,0x17B7BE43,0x60B08ED5,
0xD6D6A3E8,0xA1D1937E,0x38D8C2C4,0x4FDFF252,0xD1BB67F1,0xA6BC5767,0x3FB506DD,0x48B2364B,
0xD80D2BDA,0xAF0A1B4C,0x36034AF6,0x41047A60,0xDF60EFC3,0xA867DF55,0x316E8EEF,0x4669BE79,
0xCB61B38C,0xBC66831A,0x256FD2A0,0x5268E236,0xCC0C7795,0xBB0B4703,0x220216B9,0x5505262F,
0xC5BA3BBE,0xB2BD0B28,0x2BB45A92,0x5CB36A04,0xC2D7FFA7,0xB5D0CF31,0x2CD99E8B,0x5BDEAE1D,
0x9B64C2B0,0xEC63F226,0x756AA39C,0x026D930A,0x9C0906A9,0xEB0E363F,0x72076785,0x05005713,
0x95BF4A82,0xE2B87A14,0x7BB12BAE,0x0CB61B38,0x92D28E9B,0xE5D5BE0D,0x7CDCEFB7,0x0BDBDF21,
0x86D3D2D4,0xF1D4E242,0x68DDB3F8,0x1FDA836E,0x81BE16CD,0xF6B9265B,0x6FB077E1,0x18B74777,
0x88085AE6,0xFF0F6A70,0x66063BCA,0x11010B5C,0x8F659EFF,0xF862AE69,0x616BFFD3,0x166CCF45,
0xA00AE278,0xD70DD2EE,0x4E048354,0x3903B3C2,0xA7672661,0xD06016F7,0x4969474D,0x3E6E77DB,
0xAED16A4A,0xD9D65ADC,0x40DF0B66,0x37D83BF0,0xA9BCAE53,0xDEBB9EC5,0x47B2CF7F,0x30B5FFE9,
0xBDBDF21C,0xCABAC28A,0x53B39330,0x24B4A3A6,0xBAD03605,0xCDD70693,0x54DE5729,0x23D967BF,
0xB3667A2E,0xC4614AB8,0x5D681B02,0x2A6F2B94,0xB40BBE37,0xC30C8EA1,0x5A05DF1B,0x2D02EF8D
};
这个表其实可以在程序里生成,下面给出的完整代码就是在程序里面生成的。
#include "StdAfx.h"
#include "stdio.h"
//颠倒字串的顺序
unsigned long int Reflect(unsigned long int ulReflect, int nBitCount)
{
unsigned long int value=0;
// 交换bit0和bit7,bit1和bit6,类推
for(int i = 1; i < (nBitCount + 1); i++)
{
if(ulReflect & 1)
value |= 1 << (nBitCount - i);
ulReflect >>= 1;
}
return value;
}
unsigned long CRC32(void * DataBuf,unsigned long ulLen)
{
unsigned long int crc,temp;
unsigned long int ulPolynomial = 0x04c11db7; //标准的CRC32生成项
static unsigned long int crc32_table[256] = {0};
static int IsDoTable = 0;
//检查是否已生成正规查询表
if (0 == IsDoTable)
{
for(int i = 0; i <= 0xFF; i++) // 生成CRC32“正规查询表”
{
temp=Reflect(i, 8);
crc32_table[i]= temp<< 24;
for (int j = 0; j < 8; j++)
{
unsigned long int t1,t2;
unsigned long int flag=crc32_table[i]&0x80000000;
t1=(crc32_table[i] << 1);
if(flag==0)
t2=0;
else
t2=ulPolynomial;
crc32_table[i] =t1^t2 ;
}
crc=crc32_table[i];
crc32_table[i] = Reflect(crc32_table[i], 32);
}
IsDoTable = 1;
}
//计算CRC32值
unsigned long lRes;
unsigned long ii;
unsigned long m_CRC = 0xFFFFFFFF; //寄存器中预置初始值
char *p = (char *)DataBuf;
for(ii=0; ii <ulLen; ii++)
{
m_CRC = crc32_table[( m_CRC^(*(p+ii)) ) & 0xff] ^ (m_CRC >> 8); //计算
}
lRes= ~m_CRC; //取反
return lRes;
}
int main()
{
char DataBuf[512]={0x31,0x32,0x33,0x34}; //待测数据,为Ascii字符串"1234"
printf("0x%08X\n",CRC32(DataBuf,4));
return 0;
}
下面是结果:
与WINRAR的完全一样。
学了两天CRC了,这只是一些总结。包括基本的原理及最终优化实现,但更深一层的优化算法没有细说,如果有读者想要继续了解更深一层的优化,可以下载附件来看看,在此再次感谢附件的作者。
忘了CSDN的附件是单独管理的,这里给出个资源链接吧:http://download.csdn.net/detail/epluguo/5980501
关于CRC的更多资料,而已参阅wiki百科:http://zh.wikipedia.org/zh-cn/%E5%BE%AA%E7%8E%AF%E5%86%97%E4%BD%99%E6%A0%A1%E9%AA%8C