轻松无痛苦学习CRC指南

原文地址:http://www.repairfaq.org/filipg/LINK/F_crc_v31.html

1. 前言

1.1 关于版权和作者

“Everything you wanted to know about CRC algorithms, but were afraid to ask for fear that errors in your understanding might be detected.”
  你想要知道所有关于CRC算法的知识,但又害怕去问一些你不理解的东西,因为它可能是错误的。

  • 作者:Ross N. Williams
  • 电子邮箱:[email protected]
  • 日期:19 August 1993
  • 版本:3.00
  • FTP地址:ftp.adelaide.edu.au/pub/rocksoft/crc_v3.txt
  • 网址:http://www.on.net/clients/rocksoft/rocksoft/
  • 公司:Rocksoft(tm) Pty Ltd
  • 地址:16 Lerwick Avenue, Hazelwood Park 5066, Australia
  • 传真:+61 8 373-4911 (c/- Internode Systems Pty Ltd)
  • 电话:+61 8 379-9217 (10am to 10pm Adelaide Australia time)
  • 备注:”Rocksoft” is a trademark of Rocksoft Pty Ltd, Australia
  • 状态:Copyright (C) Ross Williams, 1993,1994,1995,1996. However, permission is granted to make and distribute verbatim copies of this document provided that this information block and copyright notice is included. Also, the C code modules included in this document are fully PUBLIC DOMAIN (PD).
  • 致谢:Thanks to Jean-loup Gailly ([email protected]) and Mark Adler ([email protected]) who both proof read this document and picked out lots of nits as well as some big fat bugs.

本文档中用到的 C 源代码:

  • crcmodel.h (9KB)
  • crcmodel.c (6KB)
  • crctable.c (8KB)

1.2 摘要

  这个文档描述了 CRCs(Cyclic Redundancy Codes,循环冗余码)以及详细说明了基于 table-driven 的实现方法。实际上有很多关于 CRCs 的文献,尤其是基于 table-driven 来实现的,但是实在让人晦涩难懂(至少对于我来说是)。所以本文档试图简单明了(不代表不严谨)地解释 CRCs,并且会把实现方法的每个细节都记录下来。除此之外,本文档给出一个参数化模型的CRC算法—— Rocksoft^tm Model CRC Algorithm。这个模型算法通过参数化的方法能够表示大部分的 CRC,因此,如果想了解特定的算法,不妨先参考本文档中的方法。上面的 C 源代码中提供了一个低速的 CRC 算法模型的实现,最后一节给出了实现高速 table-driven 的2种形式,并提供了一个生成 CRC 查找表程序。


2. 介绍:错误检测

  错误检测技术的目标是为了能够让接收者知道接收到的信息是否被破坏了,这是因为传输通道存在噪声干扰,可能会对所传输的信息引入错误。一般的做法是,发送者调用一个函数对即将传输的信息生成一个被称为“校验和(checksum)”的值,并把这个值附加到该信息上。而接收者在接收到信息时,会调用相同的函数来计算该信息的校验和,并与该信息附带的校验和进行比较,以此来确认该信息是否正确。例如,我们简单地对传输信息中各字节求和,接着用该和 mod 256(即以256为模),它们可能会像下面这样:(所有数字都是十进制的)

  Message                    :  6 23  4
  Message with checksum      :  6 23  4 33
  Message after transmission :  6 27  4 33

  如上,信息的第2个字节在传输过程中受到了破坏(由23变成27)。不过,接收者能够对计算所得的校验和(37)与信息附带的校验和(33)进行比较。但是如果校验和本身在传输过程中受到破坏,那么正确的信息就可能会被判断为错误的。确实,这是一个安全方面的缺陷。当传输信息的 and/or 校验和发生了破坏,而所传输的信息是一致的,此时就会导致这样一个危险的错误。很不幸,产生这种错误的可能性是完全不可避免的,降低这种可能性最好的办法就是增加校验信息量(例如:将校验和由1个字节拓宽到2个字节)。

  其他错误检测技术,涉及对信息进行复杂的转换,并将冗余信息注入。然而,本文档只对 CRC 算法进行讲解,通过错误检测处理后,传输的信息由原始的信息数据以及附加在其后的校验和组成。如:

  <original intact message> <checksum>
即:<       原 始 信 息      > < 校 验 和 >

3. 复杂性的需要

  在上一节的校验和例子中,我们通过一种简单地将各字节直接相加,然后对256取模的校验和算法来检测传输信息是否有被破坏。

  Message                    :  6 23  4
  Message with checksum      :  6 23  4 33
  Message after transmission :  6 27  4 33

  这个算法的问题在于——实在太简单了。如果同时有很多数据被随机破坏,但这个错误的信息却有 1/256 的机率会被认为是正确的。如:

  Message                    :  6 23  4
  Message with checksum      :  6 23  4 33
  Message after transmission :  8 20  5 33

  为了加强校验效果,我们尝试由8位寄存器改为16位寄存器(也就是模数用65536代替原来的256),显然此时判断失误的概率由 1/256 降低到 1/65536。这基本上是一个好主意,但现在问题在于所用的公式不够“随机”—— 这个简单的求和公式,无论求和寄存器有多宽,每一个移入的字节都只能会影响到它的一个字节。例如,上面的第二个例子中,即使求和寄存器拥有兆字节级别的宽度,这个错误仍然不被检测到。解决这个问题的办法是,用更复杂的公式来代替简单求和公式,而这个复杂的公式应该能够让每一个移入的字节对整个求和寄存器造成影响。

  因此,我们看到,一个健壮的校验和算法至少对以下两方面有要求:

宽度
  一个寄存器的宽度能够把这种判断出错概率降到足够低的一个概率(假如是32位的寄存器,则概率为 1/2^32)。
  
复杂性
  需要一个公式,让每一个移入的字节有能力去改变任意位数的寄存器,从而产生足够的混乱性、随机性。

注意:“校验和”这个术语据推测是用于描述早期的求和公式的,但它现在已经有更广泛的含义,包括像 CRC 这样复杂的算法所产生的值。CRC 算法能够很好地满足复杂性的要求,并且能够适应不同的校验和宽度。


4. CRC算法背后的基本思想

  我们在哪里可以找到比求和公式更复杂、更合适的公式呢?自然会想到各种各样的设计,我们可以使用的圆周率或者哈希,让每个移入的字节与寄存器中的所有字节之间构成数字表。甚至可以在网上保存一个大的电话簿,并让每一个移入的字节通过索引一个新的电话号码来作为下一个寄存器的值。因此,处理的方法是无限的。
  
  然而,我们并不需要走这么远,接下来介绍的算法足够满足要求。加法显然不足以形成一个有效的校验,而除法却可以(只要除数的宽度和校验和寄存器宽度一致)。
  
  CRC算法的基本思想是:把信息简单地当作一个巨大的二进制数,然后用另一个固定的二进制数字去除,并从中得到余数。当接收到信息时,接收者用相同的除数去执行除法运算,然后用得到的余数与“校验和”进行比较(此时,这里的“校验和”实际上是余数)。
  
  例子:假设消息由2个字节组成(6,23),跟上面的例子一样。用十六进制数表示为 0x0617,二进制表示为 0000-0110-0001-0111。假设校验和寄存器的宽度是1个字节,恒定因子 1001 作为除数,那么,校验和就等于 0000-0110-0001-0111 除以 1001 后所得的余数。这样的话,此方法显然也适用于32位的校验和寄存器,并且所得的校验和是混乱的。所以,我们将用到历史悠久、非常好用的长除法(你在学校学过的,记得吗?),只不过这一次,它用在二进制:

          ...0000010101101 = 00AD =  173 = QUOTIENT
         ____-___-___-___-
9= 1001 ) 0000011000010111 = 0617 = 1559 = DIVIDEND
DIVISOR   0000.,,....,.,,,
          ----.,,....,.,,,
           0000,,....,.,,,
           0000,,....,.,,,
           ----,,....,.,,,
            0001,....,.,,,
            0000,....,.,,,
            ----,....,.,,,
             0011....,.,,,
             0000....,.,,,
             ----....,.,,,
              0110...,.,,,
              0000...,.,,,
              ----...,.,,,
               1100..,.,,,
               1001..,.,,,
               ====..,.,,,
                0110.,.,,,
                0000.,.,,,
                ----.,.,,,
                 1100,.,,,
                 1001,.,,,
                 ====,.,,,
                  0111.,,,
                  0000.,,,
                  ----.,,,
                   1110,,,
                   1001,,,
                   ====,,,
                    1011,,
                    1001,,
                    ====,,
                     0101,
                     0000,
                     ----
                      1011
                      1001
                      ====
                      0010 = 02 = 2 = REMAINDER

  十进制中,1559 除以 9 得到商为 173,余数为 2。
  
  虽然并不是输入信息的每一位都对商的造成影响,但是4位的余数在计算过程中受到相当多的影响,如果输入信息(被除数)有更多的字节,那么它的值所受到的影响会更多。这就是为什么我们使用除法而不是加法来计算校验和的原因。
  
  如你所想,使用4位校验和,传输信息会变成 0x06172(0617是原始信息,2是校验和)。然后接收者将用 0x0617 除以 0x9,判断所得余数是否为 0x2。


5. 多项式算法

  上一节介绍的长除法校验方法与被称为 CRC 检验的方法非常类似,实际上,CRC 校验方法显得有些古怪,我们需要深入研究一些奇怪的数字系统来了解它们。
  
  在处理 CRC 算法时,你会听到一个词——多项式。一个给定的 CRC 算法将被称为使用一个特定的多项式,而 CRC 算法通常使用多项式运算来操作。这是什么意思呢?
  
  不像上一节一样,把除数、被除数(信息)、商、余数被视为正整数,而是将它们视为多项式的二进制系数。把每个数展开为一个位串,其位是一个多项式的系数。例如,普通数字23(十进制)的十六进制为 0x17,二进制为 0b10111,因此它对应的多项式为:

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

或者,更简单地表示为:

   x^4 + x^2 + x^1 + x^0

  使用这样的技巧,被除数(信息)和除数就可以用多项式来表示了,除了现在多了一些 x 外,我们完全可以像以前一样,使用所有的算法。例如,假设我们用 0b1101 乘以 0b1011,利用多项式我们可以这样做:

(x^3 + x^2 + x^0)(x^3 + x^1 + x^0)
= (x^6 + x^4 + x^3
 + x^5 + x^3 + x^2
 + x^3 + x^1 + x^0) = x^6 + x^5 + x^4 + 3*x^3 + x^2 + x^1 + x^0

  此时,为了得到正确的答案,我们需要做一些处理,因为二进制逢二进一,所以 3*x^3 项需要进位,得到:

   x^7 + x^3 + x^2 + x^1 + x^0

  是的,多项式运算就像普通的算术运算,只不过是有点抽象,并把所有的项都明确纳入计算中。那么,现在问题出在哪呢?

  问题是,假如我们不知道 x 是多少,我们就无法进行下去。我们不知道 3*x^3 等于 x^4 + x^3,因为我们不知道 x 是2。在多项式运算中,所有系数之间的关系是未知的,因此,每个 x 次幂的系数实际上都是强类型,也就是说 x^2 的系数和 x^3 的系数是两种完全不同的类型。
  由于每个 x 次幂的系数的相互隔离,数学家们想出了各种各样的多项式算法,简单地改变多项式系数的运算规则。由此设计出一种被称为“多项式模2运算”的运算规则,要求多项式所有的系数必须是0或1,并且多位模2除法采用模2减法(不带借位的二进制减法)。回到前面的例子:

(x^3 + x^2 + x^0)(x^3 + x^1 + x^0)
= (x^6 + x^4 + x^3
 + x^5 + x^3 + x^2
 + x^3 + x^1 + x^0)
= x^6 + x^5 + x^4 + 3*x^3 + x^2 + x^1 + x^0

  我们尝试用另外一种运算规则,按之前的运算规则 3*x^3 这个项会产生进位(因为我们知道 x=2)。现在我们用模2运算法则,假设我们不知道 x 是多少,并且不带进位,对所有多项式系数进行模2运算,那么,会得到如下结果:

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

正如 Knuth [Knuth81] 所说 (p.400):
  “读者应该注意多项式算法和多精度算术(第4.3.1)之间的相似性,其中基数 b 取代了 x。它们的主要区别是,多项式 x^k 的系数 u_k 与相邻的系数(x^{k-1} 和 x^{k+1})没有关系,所以这里不存在进位或借位的概念。事实上,模数为 b 的多项式运算与基数为 b 的多精度算术运算本质上是一样的,只是后者存在进位或借位操作。”
  
  所以,多项式模2运算就是不带进位或借位的、以2为模数的二进制算术运算。在研究和分析 CRC 或其他错误纠正算法时,多项式是非常有用的数学工具。为了阐述它们,我们没有过多的扩展和啰嗦,也没有对余数进行过多的讲解,更多是通过例子直接运用相关算术。那么,请记住,多项式模2运算法则的特点 —— 不带进位或借位的二进制运算。

你可能感兴趣的:(C语言)