什么是CRC???
你說小學3年級的小明同學不知好歹喜歡村長女兒王大麻子,於是羞澀的他想到寫一封情書已表心意。正所謂閨女似情人,愛女心切的村長凡是信件統統要過他之手。如果這份情書被她爸稍加“幾筆”豈不悲劇了?
奇偶驗證
如何驗證情書是否被動過手腳(驗證數據是否損壞),介於王大麻子數學不行,數數還行。作為數學課代表的小明同學立刻想到一個好主意:將所有的文字轉化二進制,只要數一數1的數量是奇數還是偶數不就可以了嗎!
比如我要傳遞M這個字符那么他的ASCII的值為0100 1101(M),就是數據末尾加上一個1或0,使其整個數據的1的數量可以湊齊奇數或偶數。
如果規定信件所有1的個數為奇數的話,那么0100 1101(M)才4個1,我們就數據的末尾加上一個1湊齊奇數(5個1):0100 1101 1;如果數據中的1剛好奇數,我們就在末尾加上一個0就可(這個方法叫做奇校驗),當然相反如果規定的信件所有1的個數為偶數(偶校驗),剛好處理的方法也是相反。
雖然這個方法簡單到連他沒有上學的弟弟都做得起(很容易通過硬件方式實現驗證),但是這個的出錯率是五五開啊(剛好少/多偶數個1),且永遠檢查不出0的錯誤(就算多100個零也看不出來)。
累加和校驗
你說奇偶不行,哪我相加總該行吧?於是乎小明同學又推出第二號方案:將每一個文字(字節)的值相加來驗證。
比如傳遞LOVE,我們就可以得到四個數字76(L) 79(O) 86(V) 69(E),他們相加可得310,如果這個數字嫌他太大了不好寫,我們可以限制他在0~255(一個字節)這個范圍之內,那么我們就得到一個驗證數字55 =310 – 255(最簡單的辦法就是減去255的倍數)。然后將數字附加數據后面,對方接受到數據執行同樣的操作對比驗證數字是否一致。
雖說這個方法要比上面的方法要可靠一些,但凡事總有列外:
一、比如數據錯誤剛好等於驗證數字
我們要傳輸的數據:76 79 86 69 310(驗證數字)
發生錯誤的數據: 76 78(-1) 87(+1) 69 310(驗證數字還是一樣的)
二、單個字符的出錯率為1/256
CRC驗證原理
無數日夜小明同學冥思苦想,最終還剩最后三根頭發之際他學會除法,頭頂一涼想到一個絕佳主意:如果我將信件上的所有文字組合成一個長數字,然后用一個我和王大麻子都知道的數字(約定的多項式)去除以,將余數作為一個驗證數字的話……
假設我要傳輸一個字符87(W),和王大麻子約定除數為6,那么結果很明顯就是87 ÷ 6 = 14……3,那么我們可以將3作為驗證數字附加原始數據的末尾發送。
但明顯我們常規的借位除法大大超出了王大麻子的數學水平,於是小明同學決定不做人了發明一個二進制除法,並取名為“模二除法”。
所謂模二除法實際形式上和我們的平常使用的除法一樣的(用於二進制運算),但是唯一不一同的是關於計算當中一切的加減法統統換成“異或”(XOR),一句話概括異或運算就是:異性相吸(返回真1),同性相斥(返回假0):1 Xor 0 = 1,0 Xor 1 = 1, 1 Xor 1 = 0, 0 Xor 0 = 0。
那么我們用上面列子來試一下模二除法(模二除法的結果不等於普通除法)
王大麻子表示:我去!算完之后我還要核對余數?!不能再簡單點嗎?
於是乎小明同學又開始沒日沒夜苦想,最終當最后的狼牙山“三壯士”也離他而去時,他頭頂一涼想到一個絕佳主意:在信件數據的末尾(即數據低位LSB)補上我和王大麻子都知道的數字的長度-1的0(生成項長度-1)然后在被同知數字相除,得到的余數再附加原始數據的末尾上。(這里說的原始數據是指沒有補零的,且余數長度一定與補零的長度一致)
口說無憑,我們來重新用這個升級計算方法試一試。
我們將余數10補在原始數據1010111的末尾得到了一個新數:101011110,然后我們再用110去除,神奇的事情發生了:沒有余數了。(接受端只要將已修改的數據與生成項模二相除沒有余數即代表數據無誤)
這便是CRC(循環冗余校驗,Cyclic Redundancy Check)是一種為了檢測數據是否損壞處理辦法。而當中的模二除法是無借位(不向高位借位的)的性質十分重要:意味我們計算CRC只需要簡單的數據位移和Xor即可(到后面你就知道了)。
當然理論上講CRC還是出錯的可能(不過已經比程序猿能找到女朋友的幾率要低了),所以選擇一個好的除數就是一個非常重要的事情,關於CRC的除數有個專業的名稱:生成多項式,簡稱生成項。如何選擇生成項這是需要一定的代數編碼知識(已經不是我這種咸魚能搞懂的問題了)。好在我們可以直接用大佬計算好的生成項,不同的生成項有不同的CRC,如:CRC8、CRC16、CRC-CCITT、CRC32……。
從數學的角度來理解CRC(如果您只是了解如何計算CRC的話可以直接跳過本節)
我們不妨嘗試將二進制轉化一種多項式:數字中多少位1代表的是x的多少次方,0乘以任何數都是0所以不用管。
比如101010(42),可以轉化為:x^5+x^3+x。(注意最低位0次方,任何數零次方都等於1)
假設用x^3+x^2+x除去x+1,可以得到如下式子:
很好我們在兩邊再乘一個x+1:
這里不難看出:得到一個商×除數+余數的式子,其中的(x^2+1)就是商,-1就是余數(我也不沒懂為啥余數是負數)。我們還可以對左邊式子再提取一次x,可得:
我們可以理解這里提取的x是對於x^2+x+1(1011)進行補一次零變成了10110,實際上這個補零還有個說法叫做左移(要注意數據的方向高位在左邊)。
如何理解補零的性質這個很簡單,我們類比十進制的補零如:1要補兩次零變成100,很明顯補零就是乘與10的幾次方。回到二進制中就是2的幾次方,而多項式中的x就可以代表2。
通過以上的式子的整理可以得出通用公式即是:
就代表的是原始數據, 代表的是數據末補n個0, 代表的是Key也就是生成項, 代表的余數也是上一節提到FCS。接收者接受到 看看是能被 整除不,可以即為正確數據。
要注意的一點 的長度(補零長度)受限於 (目的是可以直接追加在末尾,而不影響原始數據):他們長度一致,且一定比 的長度少1。
關於 為什么一定比 的長度少1?我個人愚見是特殊的模二除法: 的最高位一定1(不然沒有意義啊),而數據處理過程中需要計算數據的最高位也是1(可以對照着除法的當中與除數對應的那個被除數的那部分數據),他們進行Xor就變成0,實際計算往往是剩下的部分( 長度-1)(在程序設計中反正都會變成0干脆都不計算首位,這就是為啥網上的CRC生成多項式的簡記碼都是默認舍棄首位1的原因)。
CRC的原理實際上遠比這些要復雜的多,涉及到群論這類我們這些吃瓜群眾可望不可即的數理知識。以上也只是我對Wiki的搬運和理解瞎猜,希望大家能通過這個簡單列子大概了解CRC的性質,如有不對之處有望大佬不惜賜教!
直接計算法
雖說上一節我們已經知道CRC是怎么計算的,但明顯電腦可不會寫除法公式(程序很難直接用這種方法)。且不說要對齊一個超長二進制數據進行逐位計算的困難不說,單單是像vbscript這種既沒有幾個位操作符又用起來蛋疼的語言基本上是很難實現(大佬:喵喵喵?)。不過可以轉化一個思路:比如每次只取一部分數據來計算如何?
仔細觀察計算的過程,可以發現其實每一次Xor都是固定不動的生成項與其對應的數據首位“消1”。那我們就可以假想出一個與生成項長度一致的“盒子”,取出一部分的數據出來若首位是1時就進行一次Xor,遇到0則左移到1為止,左移造成的右端的空缺用0補充。而這里0希望理解為一種“存儲”,它“存儲” 生成項中未和數據進行計算的那一部分,按順序先后附加被計算數據的后面,當先一部分的數據全部計算之后,實際上“盒子”中剩下都是未和數據計算的部分的“和”11011 xor 10110 = 11011 xor ( 10000 xor 00100 xor 00010)(這里實際上就是Xor的交換律到后面就會體會到他的強大)
驅動表法
實際上CRC就像開心消消樂一樣,就是不斷消除首位數據。這時你想:要是能一口氣消除一個字節(8bit)以上的數據那該多好!
一般來講文藝青年會這么做(CRC的正常計算):
但是2B青年會這么想:可不可以將生成項先Xor了?
我們可以先提前計算出與數據前幾位相符的生成項之和再Xor從而到達一口氣消掉了多位數據的目的,Xor的乘法交換律允許我們提前計算出需要的數據A Xor B Xor C = A Xor ( B Xor C )
既然如此干脆直接將前八位0000 0000 ~ 1111 1111(0 ~ 255,一個字節)這個范圍所有的生產項的和全部計算存儲成表格,等計算的時候直接取出數據的首字節出來作為索引找到對應表格的中生存項的和與去掉首位字節的數據進行Xor不就可以了嗎。
表的由來
雖說想法很美好,但是如何實現前8位從0~255所有的生成項之和了?我們來思考一下CRC計算的本質是什么?復讀沒錯是不斷地消除首位數據,那么在Xor運算下消除方法就是:數據一樣!
那么我們將0~255這256個數字進行CRC逐位計算后剩下不就是已經消除掉前8位數據的生成項之和嗎!(因為前8位數據一樣所以被消除了)
用通俗點語言就是:我們提前將一個字節的CRC驗證碼計算出來。
以下就是關於CRC16的正序表格計算代碼:
1 Const PLAY = &H8005& '這里生成項(CRC16),注意這里默認首位1是去掉的 2 ReDim Table(255) '這里存儲表格 3 4 '// 計算表格部分 5 Dim I,J,Temp 6 '對於0~255這256個數字進行CRC計算,並將計算好的CRC驗證碼按順序存儲在表格(數組)中 7 For I = 0 To 255 8 Temp = (I * &H100&) And &HFFFF& 9 For J = 0 To 7 10 If (Temp And &H8000) Then 11 Temp = (Temp * 2) And &HFFFF& 12 Temp = Temp Xor PLAY 13 Else 14 Temp = (Temp * 2) And &HFFFF& 15 End If 16 Next 17 Table(I) = Temp And &HFFFF& 18 Next 19 20 '// 輸出CRC16表格代碼(使用VbsEdit的調試) 21 Dim y,x,u 22 Debug.WriteLine "Dim CRC16_Table:CRC16_Table = Array( _" 23 For y = 1 To 64 24 For x = 1 To 4 25 Debug.Write "&H",String(4 - Len(Hex(Table(u))),"0"),Hex(Table(u)),"&" 26 If u <> 255 Then Debug.Write ", " Else Debug.Write " " 27 u = u + 1 28 Next 29 Debug.WriteLine "_" 30 Next 31 Debug.WriteLine ")"
代碼無誤的話,應該會在調試框中得到如下代碼:
1 Dim CRC16_Table:CRC16_Table = Array( _ 2 &H0000&, &H8005&, &H800F&, &H000A&, _ 3 &H801B&, &H001E&, &H0014&, &H8011&, _ 4 &H8033&, &H0036&, &H003C&, &H8039&, _ 5 &H0028&, &H802D&, &H8027&, &H0022&, _ 6 &H8063&, &H0066&, &H006C&, &H8069&, _ 7 &H0078&, &H807D&, &H8077&, &H0072&, _ 8 &H0050&, &H8055&, &H805F&, &H005A&, _ 9 &H804B&, &H004E&, &H0044&, &H8041&, _ 10 &H80C3&, &H00C6&, &H00CC&, &H80C9&, _ 11 &H00D8&, &H80DD&, &H80D7&, &H00D2&, _ 12 &H00F0&, &H80F5&, &H80FF&, &H00FA&, _ 13 &H80EB&, &H00EE&, &H00E4&, &H80E1&, _ 14 &H00A0&, &H80A5&, &H80AF&, &H00AA&, _ 15 &H80BB&, &H00BE&, &H00B4&, &H80B1&, _ 16 &H8093&, &H0096&, &H009C&, &H8099&, _ 17 &H0088&, &H808D&, &H8087&, &H0082&, _ 18 &H8183&, &H0186&, &H018C&, &H8189&, _ 19 &H0198&, &H819D&, &H8197&, &H0192&, _ 20 &H01B0&, &H81B5&, &H81BF&, &H01BA&, _ 21 &H81AB&, &H01AE&, &H01A4&, &H81A1&, _ 22 &H01E0&, &H81E5&, &H81EF&, &H01EA&, _ 23 &H81FB&, &H01FE&, &H01F4&, &H81F1&, _ 24 &H81D3&, &H01D6&, &H01DC&, &H81D9&, _ 25 &H01C8&, &H81CD&, &H81C7&, &H01C2&, _ 26 &H0140&, &H8145&, &H814F&, &H014A&, _ 27 &H815B&, &H015E&, &H0154&, &H8151&, _ 28 &H8173&, &H0176&, &H017C&, &H8179&, _ 29 &H0168&, &H816D&, &H8167&, &H0162&, _ 30 &H8123&, &H0126&, &H012C&, &H8129&, _ 31 &H0138&, &H813D&, &H8137&, &H0132&, _ 32 &H0110&, &H8115&, &H811F&, &H011A&, _ 33 &H810B&, &H010E&, &H0104&, &H8101&, _ 34 &H8303&, &H0306&, &H030C&, &H8309&, _ 35 &H0318&, &H831D&, &H8317&, &H0312&, _ 36 &H0330&, &H8335&, &H833F&, &H033A&, _ 37 &H832B&, &H032E&, &H0324&, &H8321&, _ 38 &H0360&, &H8365&, &H836F&, &H036A&, _ 39 &H837B&, &H037E&, &H0374&, &H8371&, _ 40 &H8353&, &H0356&, &H035C&, &H8359&, _ 41 &H0348&, &H834D&, &H8347&, &H0342&, _ 42 &H03C0&, &H83C5&, &H83CF&, &H03CA&, _ 43 &H83DB&, &H03DE&, &H03D4&, &H83D1&, _ 44 &H83F3&, &H03F6&, &H03FC&, &H83F9&, _ 45 &H03E8&, &H83ED&, &H83E7&, &H03E2&, _ 46 &H83A3&, &H03A6&, &H03AC&, &H83A9&, _ 47 &H03B8&, &H83BD&, &H83B7&, &H03B2&, _ 48 &H0390&, &H8395&, &H839F&, &H039A&, _ 49 &H838B&, &H038E&, &H0384&, &H8381&, _ 50 &H0280&, &H8285&, &H828F&, &H028A&, _ 51 &H829B&, &H029E&, &H0294&, &H8291&, _ 52 &H82B3&, &H02B6&, &H02BC&, &H82B9&, _ 53 &H02A8&, &H82AD&, &H82A7&, &H02A2&, _ 54 &H82E3&, &H02E6&, &H02EC&, &H82E9&, _ 55 &H02F8&, &H82FD&, &H82F7&, &H02F2&, _ 56 &H02D0&, &H82D5&, &H82DF&, &H02DA&, _ 57 &H82CB&, &H02CE&, &H02C4&, &H82C1&, _ 58 &H8243&, &H0246&, &H024C&, &H8249&, _ 59 &H0258&, &H825D&, &H8257&, &H0252&, _ 60 &H0270&, &H8275&, &H827F&, &H027A&, _ 61 &H826B&, &H026E&, &H0264&, &H8261&, _ 62 &H0220&, &H8225&, &H822F&, &H022A&, _ 63 &H823B&, &H023E&, &H0234&, &H8231&, _ 64 &H8213&, &H0216&, &H021C&, &H8219&, _ 65 &H0208&, &H820D&, &H8207&, &H0202& _ 66 )
我們可以以空間換取時間,直接將CRC表格計算好作為一個常數數組使用。
得到表格之后回到驅動表法的計算:取出CRC寄存器中的首位字節,然后將CRC左移去掉首字節然后取出一字節新數據裝入CRC低端空出字節中,根據取出首字節找到對應表格中的生成項之和與CRC寄存器進行Xor,然后重復這個步驟直到數據全部取完計算完。要注意的是:因為驅動表法是一個一個字節計算,所以他必須計算之前在原始數據上補零(不能像直接計算法那樣通過逐位左移方式自己完成補零)。