CRC檢驗算法的原理及實現
循環冗餘校驗(英語:Cyclic redundancy check,通稱“CRC”)是一種根據網絡數據包或計算機文件等數據產生簡短固定位數校驗碼的一種散列函數,主要用來檢測或校驗數據傳輸或者保存後可能出現的錯誤。生成的數字在傳輸或者存儲之前計算出來並且附加到數據後面,然後接收方進行檢驗確定數據是否發生變化。一般來說,循環冗餘校驗的值都是32位的整數。由於本函數易於用二進制的計算機硬件使用、容易進行數學分析並且尤其善於檢測傳輸通道干擾引起的錯誤,因此獲得廣泛應用。此方法是由W. Wesley Peterson於1961年發表[1]。
簡介[編輯]
CRC是基於有限域**GF(2)**的校驗和算法。它的機制是 將原始信息數據流被解釋爲一個長二進制流,這個二進制流被另一個固定的預定義(短)的二進制數所除,這個除法的餘數部分就是檢驗和。
這裏的二進制數(除數和被除數)並不是正常的整數值,而是將原始數據的二進制的多個位作爲數。
比如,原始數據 0x25 = 0010 0101 可以解釋成生成多項式 0x^7 +0x6+1*5+0*4+0*x3+1x2+0*x1+1x^0
有限域GF(2),2會變成0,因爲對係數的加法運算都會再取2的模數 2 Mod 2=0 (商=1)
例如:
乘法也是類似的:
我們同樣可以對生成多項式作除法並且得到商和餘數。例如,如果我們用_x_ + x + x_除以_x + 1。我們會得到:
也就是說:
等價於:
這裏除法得到了商_x_ + 1和餘數-1,因爲是奇數所以最後一位是1。
字符串中的每一位其實就對應了這樣類型的生成多項式的係數。爲了得到CRC,我們首先將其乘以,這裏 n 是一個固定生成多項式的**階數,然後再將其除以這個固定的生成多項式,餘數的係數就是CRC。
在上面的等式中,x^2 +x+1表示了本來的信息位是111
, x+1是所謂的鑰匙**,而餘數1(也就是x^0就是CRC. key的最高次爲1,所以我們將原來的信息乘上x^1來得到 x3+x2+x,也可視爲原來的信息位補1個零成爲1110
。
一般來說,其形式爲:
這裏M(x)是原始的信息生成多項式。K(x)是 n 階的“鑰匙”生成多項式。 表示了將原始信息後面加上n個0。R(x)是餘數,即時CRC“檢驗和”。
在通信中,發送者在原始的信息數據M後附加上n位的R(替換本來附加的0)再發送。接受者受到M和R後,檢查是否能被K(x)整除。如果是,那麼接受者認爲該心事正確的。
值得注意的是就是發送者所想要發送的數據。這個串又叫做 codeword.
CRCs經常被叫做“校驗和”,但是這樣的說法嚴格來說並不是準確的,因爲技術上來說,校驗“和”是通過加法來計算的,而不是CRC這裏的除法。
“錯誤糾正編碼”(Error–Correcting Codes,簡稱ECC)常常和CRCs緊密相關,其語序糾正在傳輸過程中所產生的錯誤。這些編碼方式常常和數學原理緊密相關。例如常見於通信或信息傳遞上BCH碼、前向錯誤更正、Error detection and correction等。
二進制的除法不同於整數除法。CRC計算的底層是基於XOR(異或)操作的,因爲在二進制中,XOR操作等於不借位的減法。
- 初始時輸入的原始數據(以二進制劉標識)
- 被除數是是由CRC算法定義預先定義的固定生成多項式. 比如 CRC-n 使用一個固定的(n+1)位二進制數
- CRC檢驗和的值 = 除數%被除數的餘數
在做除法之前,需要要在信息數據之後先加上n個0.
Example:
源數據是 0xC2 =b11000010.
CRC-8生成多項式(被除數)假設爲 b100011101
被除數大小爲9bits(因此這是一個 CTC-8檢驗函數),因此在源數據後添加8位0 bits數據
我們將被除數和源數據對齊,手工進行除法(二進制異或)運算
H
1100001000000000
100011101010011001
100011101
-----------
000101111
100011101 (*)
----------
001100101
100011101
----------
010001001
100011101
----------
000001111 =0x0F求得 CRC檢驗值爲 0x0F
示例要點:
- 在手動進行除法的每一步中,除數的前導“1”總是被除數的第一個“1”對齊。這意味着每步計算可能並不只是向右移動1位,有時也會移動多位(如 line (*))
- 計算過程中如果除數將實際輸入數據的每個位歸零(不包含填充字節,即最後一位爲H所示位置),則算法將停。在最後一步中,列H和所有先前的列都包含0,因此算法停止
CRC 驗證
在數據傳輸過程中,CRC檢驗值隨源數據一起發送給接受端,數據接收端接收到數據後可以使用相同的方式對源數據計算得到CRC 並驗證計算得到的CRC值和發送過來的CRC值是否一致。另一種更常見的做法是將CRC值附加到實際的數據上,接受端接受到整個數據後再次計算CRC值,如果值爲0則說明數據傳輸過程中應該沒有發生錯誤。
Example verfication
源數據加上CRC值爲 b1100001000001111 ,我們使用的是 8bit的CRC 因此CRC值同樣爲8bit (00001111)。檢驗和過程使用的生成多項式值(被除數)是預先定義的,所以在接收端可以直接獲取到。1100001000001111
1000111010100110010
100011101
----------
000101111001
100011101
----------
00110010011
100011101
-----------
010001110
10001110
---------
00000000 -> 餘數爲0,驗證數據應該是完整的
CRC移位計算原理
我們已經手動驗證了CRC算法,但是具體如何變成實現呢? 計算的源數據可能很長(超過1個字節),因此不能直接通過"Inpudate data % generator polynomial"就得到結果。計算過程必須一步步來(移位計算),這裏使用了移位寄存器的概念
設定移位寄存器有固定的寬度,每次將寄存器移動一個位時,及刪除最右邊或者最左邊的位,在空出的位置上移入一個新的位數據(來自源數據)。CRC使用左移寄存器:當移位時,最高有效位彈出,MSB-1的位向左移動一個位置到MSB,位置MSB-2的位移動到MSB-1的位置,以此類推。輸入流的下個位插入移動到因爲左移位而空出來的位置(最右的位)
CRC 使用左移位的具體就算過程如下
- 初始化“寄存器”爲0
- 原始數據流依次移動到“寄存器”中,每次移動1 bit,移動後如果最高位(MSB)爲1,則執行執行一次 XOR
- 如果源數據的所有位都處理了,則“寄存器”中的值就是 CRC value
示例
源數據 = 0xC2=b11000010( 添加8bit的0 後: b1100001000000000),生成多項式= b100011101
- CRC-8 寄存器初始化
— --- — --- — --- — ---
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | <-- b1100001000000000
— --- — --- — --- — ---
- 寄存器左移一位 ,MSB爲0 ,因此不做任何操作,數據源最高位移動到空出的LSB位
— --- — --- — --- — ---
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | <-- b1000010000000000
— --- — --- — --- — ---
- 重複步驟,直到MSB爲1,此時數據如下
— --- — --- — --- — ---
| 1| 1 | 0 | 0 | 0 | 0 | 1 | 0 | <-- b00000000
— --- — --- — --- — ---
- 再次左移寄存器時, MSB 1將被移除,因此需要執行XOR運算
— --- — --- — --- — ---
1 <-| 1| 0 | 0 | 0 | 0 | 1 | 0 | 0 | <-- b0000000
— --- — --- — --- — ---
執行 b110000100^b100011101=b010011001 最高位被丟棄,因此新的CRC寄存器的值爲 b010011001
— --- — --- — --- — ---
| 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | <-- b0000000
— --- — --- — --- — ---
- 左移寄存器, MSB 1被移除,執行:b100110010 ^ b 100011101 = b000101111
— --- — --- — --- — ---
| 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | <-- b000000
— --- — --- — --- — ---
- 重複以上步驟直到 源數據全部被移入寄存器
— --- — --- — --- — ---
| 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | <--
— --- — --- — --- — ---
此時寄存器中的值即爲計算得出的CRC檢驗值 0x0F.
代碼實現示例
以下是一個Java實現的CRC-8檢驗和,CRC-8檢驗和應該使用一個9位的生成項,但是在算法實現中跟蹤計算這樣一個未對齊(8位)的數據比較麻煩。幸運的是,由於多項式的最高位總是1,而且在運算過程中總是將除數的第一個1和被除數的第一個1對齊,並且進行異或後它的結果總是0,因此在算法時間中我們可以丟棄最高位的1。因此在算法中我們可以直接使用 00011101 =0x1D作爲生成項
public class CRC_8_DEMO {
public static byte Compute_CRC8_Simple_OneByte_ShiftReg(byte byteVal) {
byte generator = 0x1D;
byte crc = 0; /* 初始化CRC寄存器爲0 */
/*添加8位0即0x00在數據的末尾 */
byte[] inputstream = new byte[]{byteVal, 0x00};
/* 循環字節輸入流,每次處理一個字節,每個字節的處理 從最高位每次向左移動1個位 */
for (byte b : inputstream) {
for (int i = 7; i >= 0; i--) {
/* 檢查 crc寄存器中最高位是否爲1 */
if ((crc & 0x80) != 0) { /* 如果最高位爲1,則左移一位*/
crc = (byte) (crc << 1);
/* shift in next bit of input stream:
* If it's 1, set LSB of crc to 1.
* If it's 0, set LSB of crc to 0. */
/* 從 b 獲取下一位(最高位),如果爲1,則填充crc的最低位爲 1
* 如果最高位爲1,則填充crc的最低位爲 1
* 如果最高位爲0,則填充crc的最低位爲 0 */
crc = ((byte) (b & (1 << i)) != 0) ? (byte) (crc | 0x01) : (byte) (crc & 0xFE);
/* Perform the 'division' by XORing the crc register with the generator polynomial */
/* 通過^操作 執行二進制除法 */
crc = (byte) (crc ^ generator);
} else { /* MSB不爲1,則crc寄存器 左移一位,並且字節b left shift移除最高位填充crc的 LSB */
crc = (byte) (crc << 1);
crc = ((byte) (b & (1 << i)) != 0) ? (byte) (crc | 0x01) : (byte) (crc & 0xFE);
}
}
}
return crc;
}
}
輸入流只有一個字節時 優化CRC-8 實現
從目前的CRC-8的實現上來看 還有一些複雜,那麼能否進一步簡化呢?
- 最開始的8次 左移是無用的,因此 CRC 寄存器被初始化爲0 因此,至少需要向左移動8次 ,CRC的首位纔可能爲1;因此我們可以直接使用 input stream 填充crc
- 此時inputstream中只剩下8位0, 因爲 在crc執行左移操作時 ,默認會在最低字節填充0,因此我們不必執行 shift in操作 (即把inpustream中的位移入crc)
優化後的單字節 CRC算法實現如下
public class CRC_8_DEMO {
public static byte Compute_CRC8_Simple_OneByte(byte byteVal) {
byte generator = 0x1D;
byte crc = byteVal; /*直接使用輸入字節替代, 省略掉了不必要的8次從輸入字節移位到crc寄存器的操作*/
for (int i = 0; i < 8; i++) {
if ((crc & 0x80) != 0) {
/* 最高有效位爲1, 左移crc寄存器 並執行 XOR操作*/
crc = (byte) ((crc << 1) ^ generator);
} else { /* 最高有效位不爲1,則只執行移位 */
crc <<= 1;
}
}
return crc;
}
}
通用的 CRC8實現
上面 我們討論並實現了只有一個字節時的CRC8的實現,那麼如果輸入是一個字節數組呢。回到最初的CRC8處理字節數組的函數 _Compute_CRC8_Simple_OneByte_ShiftReg() ,該函數可以很容易適配只有一個字節的情況,只要我們把一個字節固定爲 0x00。 那麼對於Compute_CRC8_Simple_OneByte()函數呢?
兩者的邊界在於:如果一個字節已經被計算(處理)過了,那麼另一個字節的處理如何整合到現有的計算過程呢?讓我們看一個簡單的例子(手動計算實例)
假設輸入流字節爲{0x01,0x02}, 多項式值爲 0x1D
000000010000001000000000
100011101
-----------
0000111110000
100011101
-----------
0111011010
100011101
-----------
0110001110
100011101
-----------
0100100110
100011101
----------
0001110110 =0x76
該算法一次處理一個字節碼並且在當前字節完全處理完之前不會考慮下一個字節
回到 函數 Compute_CRC8_Simple_OneByte, crc的值設置爲 0x01 ,輸入流爲 0000000100000000…我們看下只處理首個字節時的處理過程
0000000100000000
100011101
----------
000011101
比對下 包含第二個字節 0x02時的處理過程
000000010000001000000000
100011101
-----------
000011111
我們可以發現第一個示例中只處理了首個字節,第二個示例同時處理了2個字節,如果將第一個示例中的CRC值(處理結果) 000011101 當做下一個字節,然後重新使用異或計算得出 000011101 ^ 00000010 = 000011111 。
因此,我們可以很容易拓展目前的算法以處理容易長度的輸入字節數組
public class CRC_8_DEMO {
public static byte Compute_CRC8_Simple(byte[] bytes) {
byte generator = 0x1D;
byte crc = 0; /* start with 0 so first byte can be 'xored' in */
for (byte currByte : bytes) {
crc ^= currByte; /* XOR-in the next input byte */
for (int i = 0; i < 8; i++) {
if ((crc & 0x80) != 0) {
crc = (byte) ((crc << 1) ^ generator);
} else {
crc <<= 1;
}
}
}
return crc;
}
}
CRC-8查表法實現
到目前爲止,該算法的效率很低,因爲它是逐位工作的。對於較大的輸入數據,這可能非常慢。但是我們的CRC-8算法如何才能被加速呢?
我們知道除數總是當前的crc字節值——一個字節只能取256個不同的值。並且生成多項式(=除數)是固定的。爲什麼不通過固定的多項式預先計算每個可能的字節的除法並將這些結果存儲在一個查找表中呢?顯而易見因爲對於相同的除數和被除數,餘數總是相同的!然後輸入流可以按字節處理,而不是按位處理。
讓我們用我們常用的例子來手動演示這個過程:
假設輸入流字節爲{0x01,0x02}, 多項式值爲 0x1D
1.初始化 crc = 0x00
2.異或下一個輸入字節 0x00^0x01 =0x01
3.使用CRC-8計算 數據0x01的結果,從查找表中 查找異或0x01對應的結果:table[0x01]=crc= 0x1D
4.異或處理下一個輸入字節byte 0x02:0x1D^0x02 = 0x1F
5.使用CRC-8計算 數據0x01F的結果: table[0x1F] = crc= 0x76
至於表的數據,我們可以根據定義的 多項式值提前生成,比如我們生成一個CRC-32的查找表,其中生成多項式爲0xedb88320
CRC 算法規範
下列標準參數用於定義CRC算法實例:
- Name: 一個CRC算法實例必須以某種方式標識,因此每個公開定義的CRC參數集都有一個名稱,例如CRC-16/CCITT。
- Width: 定義計算出的CRC值的字節數(n bites);同時也定義了使用的多項式生成器的字節數(n=1 bits)。最常用的寬度是8、16和32位。但從理論上講,從1開始的所有寬度都是可能的。在實踐中,甚至使用非常大的(80位)或不均勻的(5位或31位)寬度。
- Polynomia: 使用的多項式生成器的值。存在不同的形式,包括正常、反轉等
- Initial Value: CRC寄存器的初始值,在上面的例子中都是0,但是理論上任何值都是可以的,但是不同的值檢驗的精度可能有差異
- **Input reflected: **如果這個值是true,則輸入字節在使用前應該是反轉的,即以相反的殊勳使用輸入字節的位。舉個例子 byte 0x82 = b10000010 ,Reflected(0x82)=Reflected(b10000010)=b01000001=0x41
- Result reflected: 如果這個值是true,則最終的計算的CRC結果在返回前被反轉。
- Final XOR value: 對最終的CRC結果值進行異或操作,這個操作在 "Result reflected"之後。
- Check value [可選的]:這個值不是必須的,該值的作用是 用於幫助驗證實現的CRC檢驗算法是否正確。這個值通常是字符"123456789"或字節數組:[0x31,0x32,0x33,0x34,x035,036,x037,0x38,0x39] 進行CRC檢驗運算後的結果
補充說明(值得注意的地方)
CRC的基本數學原理
首先讓我們回顧以下CRC定義中的一些基礎數學知識
CRC-n 使用了一個生成多項式 G(x),這個多項式有n階以及n+1個項式
n+1項表示多項式的長度爲n+1(使用了普通的多項式表示法,即最高項在左邊)
比如:一個 CRC-8的生成多項式值爲0x07 = 100000111 = 1x8+0*x7+0x6+0*x5+0x4+0*x3+1x2+1*x1+1*x^0
CRC的計算依賴於多項式的除法,可以表述爲 M(x)*x^n =G(x)*Q()+R(X) ,其中
CRC-1 等同於 奇偶檢驗位
奇偶檢驗位表示一個二進制數中位值爲1的數量是奇數還是偶數。
比如:
0x34 = 0011 0100 有3個1的位,因爲3爲奇數,因此奇偶檢驗結果爲1;
CRC-1 階級爲1,並且有2個多項式:a*1+b*x0. 因爲CRC檢驗的最高位總是1,因此a=1; 如果b=0則該多項式沒有任何意義,因爲 與多項式0x10最終計算的結果總是爲0(讀者可以試驗下)。因此CRC-1使用的生成多項式爲0x11(二進制),而因爲在程序的實際運算過程中我們可以省略第一位(上文有介紹過)因此在程序中表達爲 0x01。
輸入數據爲0x34的運算過程
001101000000
11
---------
0001000
11
-------
0100
11
—
010
11
–
01 = CRC = 1
爲什麼在CRC 算法中,加法等於減法
CRC 計算使用的是多項式算法。CRC的多項式算法是基於有限域上兩個元素(0和1)的除法
要定義加法運算,只有四種不同的情況:
0 + 0 = 0
0 + 1 = 1
1 + 0 = 1
1 + 1 = 0 -注意這裏沒有進位,因爲有限域爲(0,1)
定義減法如下:
0 - 0 = 0
0 - 1 = 1
1 - 0 = 1
1 - 1 = 0
我們看到加法和減法是一樣的:基本上這就是我們在上面已經使用過幾次的XOR操作。( 因爲 1+1=0,1-1=0,1^1=0)
這就是爲什麼我們說M(x)*^n - R(x)和M(x) *x^n+ R(x)在CRC算法中是相同的。
爲什麼乘以x^n等於添加n個0
這是因爲每在末尾加一個0,等於多項式乘以一個n,比如我們有一個多項式 xn+xn-1+…x1+x0.乘以x後等於
x*(xn+xn-1+…x1+x0) =x(n+1)+xn+…x^1 ,通過與x相乘,多項式的層級增加了1,這就等於在右邊加上一個0。舉個例子可能會更清楚
假設有多項式 01010010 = x^6 + x^4 + x ,添加一個0後:
010100100 = x^7 + x^5 + x^2 = x * (x^6 + x^4 + x). 因此多項式每乘於x 等於添加一個0
"移位寄存器"的初始值是否可以不爲0
在目前所有的實現示例中,crc寄存器中的值總是爲0的,那麼如果crc寄存器的值不爲0這個結果是否依舊是正確的?或者說寄存器爲0和爲非0兩者結果是否相同?
讓我們先假設寄存器初始值爲0 和爲非0的結果是相同的,那麼看下面這樣一個例子
case A: crc初始值 = 0x00, 多項式 = 0x9B,輸入數據爲 [0xFF,0x01]
case B: crc初始值爲 = 0FF, 多項式 = 0x9B,輸入數據爲 [0x01]
分析:因爲case A的初始值爲0x00,因此在計算過程中,首先移動8個位,因此等價於 crc初始值爲 0xFF 輸入數據爲[0x01],此時 case A = case B,所以兩個的計算結果應該是一樣的,對吧.
但是實際上兩種情況計算出(使用程序運算)的CRC結果是不一樣的,分別是 0x2A和0xE0。
這表明了,先移位改變crc寄存器的值,再使用crc程序運行的結果是不一樣的。事實上,在CRC32的實現中,寄存器的初始值通常爲0xFFFFFFFF。
讓我們分析下一個的 CRC-8 實現,並從中尋找原因
public class CRC_8_DEMO {
public static byte Compute_CRC8_Simple(byte[] bytes) {
byte generator = 0x1D;
byte crc = 0; /* start with 0 so first byte can be 'xored' in */
for (byte currByte : bytes) {
//注意,這裏對輸入的字節先執行了 ^=currByte的操作
crc ^= currByte; /* XOR-in the next input byte */
for (int i = 0; i < 8; i++) {
if ((crc & 0x80) != 0) {
crc = (byte) ((crc << 1) ^ generator);
} else {
crc <<= 1;
}
}
}
return crc;
}
}
注意第10行的代碼,在進行 crc寄存器和generator的異或操作前,先對crc和currByte執行了異或操作,因此如果 crc初始值不爲0,當第一次執行 crc的初始值是0時,執行異或操作後的結果是crc = currByte即首個字節,但當crc初始值不爲0時,第一次循環內 crc^=currByte的結果則不是currByte。
該實驗並不是說 crc的初始值不能爲非0,只是爲了說明初始值不同,計算的結果也不同。
Normal、Reversed的生成項形式
我們可能在一些有關CRC中的文章中知道 crc 生成項有 正常、反轉、鏡像等形式,其實原理是一樣的,我們上文介紹的計算方式都是 normal的,即處理數據是從 從左到右、從高位到低位,但是在計算的現實情況中,數據可能是從低位到高位(比如在很多底層硬件的數據通信中),因此有了reversed的形式,數據的處理是從右向左移位的。
CRC與數據完整性[編輯]
儘管CRC在數據錯誤檢測中非常有用,但CRC並不能可靠地校驗數據完整性(即數據沒有發生任何變化),這是因爲CRC多項式是線性結構,可以非常容易地_故意_改變量據而維持CRC不變,參見CRC and how to Reverse it中的證明。我們可以用Message authentication code校驗數據完整性。
CRC發生碰撞的情況[編輯]
與所有其它的散列函數一樣,在一定次數的碰撞測試之後CRC也會接近100%出現碰撞。CRC中每增加一個數據位,碰撞機率就會減少接近50%,如CRC-20與CRC-21相比。
- 理論上來講,CRC64的碰撞概率大約是每18×10個CRC碼出現一次。
- 由於CRC的不分解多項式特性,所以經過合理設計的較少位數的CRC可能會與使用較多數據位但是設計很差的CRC的效率相媲美。在這種情況下CRC-32幾乎同CRC-40一樣優秀。
設計CRC多項式[編輯]
生成多項式的選擇是CRC算法實現中最重要的部分,所選擇的多項式必須有最大的錯誤檢測能力,同時保證總體的碰撞概率最小。多項式最重要的屬性是它的長度,也就是最高非零係數的數值,因爲它直接影響着計算的校驗和的長度。
最常用的多項式長度有
- 9位(CRC-8)
- 17位(CRC-16)
- 33位(CRC-32)
- 65位(CRC-64)
在構建一個新的CRC多項式或者改進現有的CRC時,一個通用的數學原則是使用滿足所有模運算不可分解多項式約束條件的多項式。
- 這種情況下的不可分解是指多項式除了1與它自身之外不能被任何其它的多項式整除。
生成多項式的特性可以從算法的定義中推導出來:
- 如果CRC有多於一個的非零係數,那麼CRC能夠檢查出輸入消息中的所有單數據位錯誤。
- CRC可以用於檢測短於2k的輸入消息中的所有雙位錯誤,其中k是多項式的最長的不可分解部分的長度。
- 如果多項式可以被x+1整除,那麼不存在可以被它整除的有奇數個非零係數的多項式。因此,它可以用來檢測輸入消息中的奇數個錯誤,就像奇偶校驗函數那樣。
參考
https://www.cnblogs.com/masonzhang/p/10261855.html
http://www.sunshine2k.de/articles/coding/crc/understanding_crc.html#ch1