0. 序

本文主要讨论软件方法计算crc32的算法以及linux内核中的实现。

1. 基本概念

crc32主要用于通信中的数据完整性校验。基于网上已经有一大堆相关的阐述,这里就不再赘述。不过还是回顾一下基本的计算方法。

通信双方需要约定一个基本的作为除数的多项式,且在mod2的意义下考虑多项式的系数(因此系数只能是0或1)。将待传输的数据视为多项式的系数(每个bit对应一个系数),使其作为被除数。这里需要严格定义一下如何把一组数据映射到多项式的系数上:一组数据可以用一个指针地址加一个byte长度表示。地址越低,对应的多项式系数越高(主要方便接收方校验)。而对于一个byte内部,如果约定是most significant bit对应多项式的高次系数,这称为正向校验,如果是least significant bit作为多项式的高次系数,这称为反向校验。

举一个两字节数据的例子

低地址 -> 高地址
{0x12, 0x34 }
// 二进制:00010010b, 00110100b

如果是正向校验,这组数据映射到的多项式为x12+x9+x5+x4+x2x^{12}+x^9+x^5+x^4+x^2x12+x9+x5+x4+x2
而如果是反向校验,则映射为x14+x11+x5+x3+x2x^{14}+x^{11}+x^5+x^3+x^2x14+x11+x5+x3+x2

这样的话,由于crc校验位都是最后发送的(即位于高地址处),若设事先约定的除数多项式为R(x)R(x)R(x),数据对应的多项式为P(x)P(x)P(x),而crc校验位对应的多项式为Q(x)Q(x)Q(x),且Q(x)Q(x)Q(x)的次数为α−1\alpha-1α−1,那么在接收方看到的多项式则为P(x)xα+Q(x)P(x)x^\alpha+Q(x)P(x)xα+Q(x),我们希望选取适当的Q(x)Q(x)Q(x),使得R(x)∣P(x)xα+Q(x)(mod2)R(x)\quad |\quad P(x)x^\alpha+Q(x)(\bmod 2)R(x)∣P(x)xα+Q(x)(mod2)
由多项式带余除法的知识可以知道,若R(x)R(x)R(x)的次数为α\alphaα,则我们可以唯一地选取一个次数不超过α−1\alpha-1α−1的多项式Q(x)Q(x)Q(x),使得R(x)∣P(x)xα−Q(x)≡P(x)xα+Q(x)(mod2)R(x)\quad |\quad P(x)x^\alpha-Q(x)\equiv P(x)x^\alpha+Q(x) (\bmod 2)R(x)∣P(x)xα−Q(x)≡P(x)xα+Q(x)(mod2)

现在对于crc32来说,R(x)R(x)R(x)是一个如下的次数为32的多项式(一般地,crcN的除数多项式次数为N)x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x1+1x^{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+1x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x1+1因此我们需要在数据末尾额外添加32bit的crc校验位,代表一个次数不超过31的多项式Q(x)Q(x)Q(x),且为得到Q(x)Q(x)Q(x)只需要做一个带余除法就好Q(x)≡P(x)x32(mod2)Q(x) \equiv P(x)x^{32} (\bmod 2)Q(x)≡P(x)x32(mod2)

2. linux内核对1bit算法的实现

为求一组数据的crc,最简单的算法就是模拟多项式带余除法的过程,每次循环移位1bit(好图出处)。

运算过程中每次看目前的余数多项式的最高位,如果是1,则减去一个除数多项式,然后除数右移,如果是0,单纯除数右移。

Linux内核实现位于lib/crc32.c文件中。正向校验的实现如下

static inline u32 __pure crc32_be_generic(u32 crc, unsigned char const *p,size_t len, const u32 (*tab)[256],u32 polynomial)
{#if CRC_BE_BITS == 1       // 表示采用 1bit算法int i;while (len--) {crc ^= *p++ << 24;  // 余数修正for (i = 0; i < 8; i++)crc = (crc << 1) ^ ((crc & 0x80000000) ? polynomial :0);// 多项式带余除法过程中的mod2运算等价于计算机中的异或运算}return crc;
}

简单起见,我们先认为传入的初始crc参数值为0。要理解上面的代码,关键在于想清楚返回的crc的各位bit如何对应到余数多项式系数上。对于正向校验,一个字节的MSB对应到多项式的高位,而一个u32的4个字节谁对应到多项式的高位则依赖于算法的具体实现了(只要在最后把结果copy到数据末尾时把顺序搞对就好)。但为了方便移位计算,这里的crc对应到最终的不超过31次多项式的方法是这样的:第k位bit对应多项式xkx^kxk的系数。这样对应的好处是,模拟多项式带余除法时除数逐渐右移的过程在代码中的实现就是crc逐渐左移。

这里的一个trick是,由于crc32的除数多项式的x32x^{32}x32项的系数必然为1,因此省略掉了,使用polynomial参数来代表剩下的x31…x0x^{31} \dots x^{0}x31…x0项的系数,正好可以用一个u32来表示。另外,polynomial参数的多项式系数和自己的bit对应关系应与crc参数相同,根据上面已经给出的crc32的除数多项式,不难得到这里的polynomial值应为0x04C11DB7

所以算法中循环内干的事情就是,查看目前的余数多项式最高位是否为1,如果是1的话,先左移再减去除数多项式(因为除数多项式最高位被省略了,如果我们有一个u33来完整表示除数多项式的话,就应该先减去除数多项式再左移了)。

如果把正向校验的代码搞明白了,反向校验的代码也很好理解了。

static inline u32 __pure crc32_le_generic(u32 crc, unsigned char const *p,size_t len, const u32 (*tab)[256],u32 polynomial)
{#if CRC_LE_BITS == 1   // 表示采用 1bit算法int i;while (len--) {crc ^= *p++;  // 余数修正for (i = 0; i < 8; i++)crc = (crc >> 1) ^ ((crc & 1) ? polynomial : 0);}return crc;
}

反向校验时一个byte的LSB对应多项式的高次系数,为了方便移位,此时crc的第k位bit对应多项式的x31−kx^{31-k}x31−k项的系数,根据这个对应关系,不难知道传入的polynomial参数的值应为0xEDB88320

上面的正向校验或者反向校验的代码并不依赖于大小端(因为数据是逐byte读入的)。但从linux的函数命名中可以看出前者更与大端绑定,后者更与小端绑定。能想到的一种猜测是,对于正向校验而言,如果是小端机器,crc的most significant byte是在高地址,而这个byte对应的是多项式的高次。因此在把crc值copy到数据末尾时,需要提前先把crc转为大端,使得多项式的高次在低地址处。而大端机器就省略了这一步。反向校验也是同样的分析。

在之前为了便于理解,我们假定了传入的crc初始参数为0,不过在真实情况下一般初始值为0xffffffff。在内核文档中有如下解释

Two more details about CRC implementation in the real world:

Normally, appending zero bits to a message which is already a multiple
of a polynomial produces a larger multiple of that polynomial. Thus,
a basic CRC will not detect appended zero bits (or bytes). To enable
a CRC to detect this condition, it’s common to invert the CRC before
appending it. This makes the remainder of the message+crc come out not
as zero, but some fixed non-zero value. (The CRC of the inversion
pattern, 0xffffffff.)

The same problem applies to zero bits prepended to the message, and a
similar solution is used. Instead of starting the CRC computation with
a remainder of 0, an initial remainder of all ones is used. As long as
you start the same way on decoding, it doesn’t make a difference.

也就是说,传入初始crc非0是为了检测传输过程中多余的前导0。另外,为了检测传输过程中末尾的多余0,crc值通常会bit反转。最终的data + crc对应的多项式就不再是除数多项式的倍数了,这样接收方校验出的crc不再是0,而是0xffffffff的crc值。

3 Sarwate algorithm

1988年,Dilip V. Sarwate 在论文Computation of Cyclic Redundancy Checks via Table Look-Up中提出了使用lookup table来加速crc计算的方法。这样每次循环内部可以处理一个byte的数据。原论文对于lookup table的构造写得复杂了,其实lookup table要存的就是提前算好的多项式余数嘛。

对于计算crc32, 一般地,我们假设希望一次处理N bit的数据,那么需要准备一个u32 table[1 << N],通常情况下N=8(下面的论述都假设N=8,且为正向校验),就是256个表项的table。具体这个table存的什么呢?设III为table的第III个表项,
I=∑i=07ti∗2i,ti∈{0,1}P(x)=∑i=07ti∗xiQ(x)≡P(x)x32(modR(x))I = \sum_{i=0}^7t_i*2^i, t_i \in \{0,1\} \\ P(x) = \sum_{i=0}^7t_i*x^i \\ Q(x) \equiv P(x)x^{32} \quad(\bmod R(x)) I=i=0∑7​ti​∗2i,ti​∈{0,1}P(x)=i=0∑7​ti​∗xiQ(x)≡P(x)x32(modR(x))
其中R(x)R(x)R(x)是约定的除数多项式。注意如果是反向校验,P(x)应写为(因此正向校验和反向校验的table是不一样的)P(x)=∑i=07ti∗x7−iP(x) = \sum_{i=0}^7t_i*x^{7-i}P(x)=i=0∑7​ti​∗x7−i
而table[I]table[I]table[I]中存的就是Q(x)Q(x)Q(x)系数的bit表示(因为Q(x)次数不超过31,可以用一个u32来表示,正向校验中,第k位bit对应xkx^kxk的系数,反向校验中则对应x31−kx^{31-k}x31−k的系数)。直观来讲,table[I]table[I]table[I]存的就是III对应的crc余数。

接下来看内核代码的实现,反向校验版本(正向校验版本也是类似的,不再列出)

static inline u32 __pure crc32_le_generic(u32 crc, unsigned char const *p,size_t len, const u32 (*tab)[256],u32 polynomial)
{# elif CRC_LE_BITS == 8   // 表示使用 Sarwate algorithm算法/* aka Sarwate algorithm */while (len--) {crc ^= *p++;  // 余数修正crc = (crc >> 8) ^ tab[0][crc & 255];}// tab[0]对应的是上面提到的256个表项的tablereturn crc;
}/* 一个更加原始的版本,请忽略可能的大小端问题以及数组越界的问题while(len--){u32 tmp = tab[0][*p++];*(u32*)p ^= tmp;}return *p;
*/

额外解释一下算法是怎么进行的:Sarwate在论文中提到,如果按照原本的查表法,我们不得不修改原本的字符序列(参考上面的更加原始的版本),这里用到的trick是,我们把tab表中对接下来的byte的余数影响存到了crc这个临时变量里(或者理解为我们假设接下来的byte都是0),到实际需要访问表项时再读入接下来的byte,通过摸2加法求出真实的余数,再用这个结果去访问表项(所谓的余数修正,类似的trick在1bit算法中也用到了)。

另外值得注意的是,该算法的实现也不依赖于端序

4. slice-by-N algorithm(端序)

读者很容易把上面描述的Sarwate algorithm推广到一次处理16bit甚至更多,但此时table表项指数级增加是不能够接受的。在05年时,Intel提出了slice-by-N algorithm,号称能够一次处理任意多的bit,而且table表项的大小是线性增加的(但实际并没有这么神,所以我用了号称这个词)。

具体想法是这样,以一次处理32bit为例(论文中称为slice-by-4),我们需要一个u32 table[4][256]table[0]就对应Sarwate algorithm中的256表项,即table[0][I]table[0][I]table[0][I]为III的crc余数,一般地,table[k][I]table[k][I]table[k][I]存放的是I∗28kI*2^{8k}I∗28k的crc余数。对应一个32bit数M,M的crc余数可以由如下式子给出(只是直观的结果,忽略了正反校验以及端序的影响)

 table[0][M & 255] ^ table[1][(M >> 8) & 255] ^ table[2][(M >> 16) & 255] ^ table[3][(M >> 24) & 255]

可以看到,虽然说一次处理32bit,但其实查表次数和原本的Sarwate algorithm是一样的,不过从原论文中可以看出,算术运算次数确实减少了,以及访问待计算crc的byte序列的次数确实减少了。另外,把算法应用到一次处理64bit,可以发现一个额外的bonus:读取的64bit中,有32bit不需要进行余数修正(因为每次算出的余数只有32bit)。

现在我们来看看内核的实现,值得注意的是,因为一次处理不止一个byte,我们需要注意端序给代码带来的影响。选取的代码是正向校验,且实现slice-by-8。

static inline u32 __pure crc32_be_generic(u32 crc, unsigned char const *p,size_t len, const u32 (*tab)[256],u32 polynomial){crc = (__force u32) __cpu_to_be32(crc);  // 把crc转为大端表示crc = crc32_body(crc, p, len, tab);crc = __be32_to_cpu((__force __be32)crc); // 将结果crc从大端转回cpu端序return crc;
}

我们看到,在调用实际计算crc32的代码前,先进行了一个端序转换,再一次提醒读者,在正向校验时,tab中存的32位bit依次对应余数多项式的32个系数(第31位bit对应多项式的第31次方系数,以此类推)。而linux传入的tab实际上也进行了一个端序转换,在crc32table.h文件中,正向校验的table表声明如下

static const u32 ____cacheline_aligned crc32table_be[8][256] = {{tobe(0x00000000L), tobe(0x04c11db7L), tobe(0x09823b6eL), tobe(0x0d4326d9L),
tobe(0x130476dcL), tobe(0x17c56b6bL), tobe(0x1a864db2L), tobe(0x1e475005L),
tobe(0x2608edb8L), tobe(0x22c9f00fL), tobe(0x2f8ad6d6L), tobe(0x2b4bcb61L),
tobe(0x350c9b64L), tobe(0x31cd86d3L), tobe(0x3c8ea00aL), tobe(0x384fbdbdL),
......
}
}

tobe宏的声明来自crc32.c

#if CRC_BE_BITS > 8   // 表示使用slice-by-N算法
# define tobe(x) ((__force u32) cpu_to_be32(x))
#else    // 表示使用Sarwate algorithm或者1bit算法
# define tobe(x) (x)
#endif

前面提到过,1bit算法以及Sarwate algorithm并不依赖于端序,因此tobe是一个空操作,而在slice-by-N算法中,把余数表转为了大端序,我们在接下来分析crc32_body函数时将会看到这样做的原因。

下面的代码剔除了一些无关的预编译判断。为了便于分析,不妨假设是一台小端机器。

static inline u32 __pure
crc32_body(u32 crc, unsigned char const *buf, size_t len, const u32 (*tab)[256])
{# ifdef __LITTLE_ENDIAN
#  define DO_CRC(x) crc = t0[(crc ^ (x)) & 255] ^ (crc >> 8)
#  define DO_CRC4 (t3[(q) & 255] ^ t2[(q >> 8) & 255] ^ \t1[(q >> 16) & 255] ^ t0[(q >> 24) & 255])
#  define DO_CRC8 (t7[(q) & 255] ^ t6[(q >> 8) & 255] ^ \t5[(q >> 16) & 255] ^ t4[(q >> 24) & 255])
# else
#  define DO_CRC(x) crc = t0[((crc >> 24) ^ (x)) & 255] ^ (crc << 8)
#  define DO_CRC4 (t0[(q) & 255] ^ t1[(q >> 8) & 255] ^ \t2[(q >> 16) & 255] ^ t3[(q >> 24) & 255])
#  define DO_CRC8 (t4[(q) & 255] ^ t5[(q >> 8) & 255] ^ \t6[(q >> 16) & 255] ^ t7[(q >> 24) & 255])
# endifconst u32 *b;size_t    rem_len;const u32 *t0=tab[0], *t1=tab[1], *t2=tab[2], *t3=tab[3];const u32 *t4 = tab[4], *t5 = tab[5], *t6 = tab[6], *t7 = tab[7];u32 q;/* Align it */if (unlikely((long)buf & 3 && len)) {do {DO_CRC(*buf++);} while ((--len) && ((long)buf)&3);}rem_len = len & 7;len = len >> 3;b = (const u32 *)buf;for (--b; len; --len) {q = crc ^ *++b; /* use pre increment for speed */crc = DO_CRC8;q = *++b;   // bonus:这里不需要进行余数修正,少一次异或运算crc ^= DO_CRC4;}len = rem_len;/* And the last few bytes */if (len) {u8 *p = (u8 *)(b + 1) - 1;do {DO_CRC(*++p); /* use pre increment for speed */} while (--len);}return crc;
#undef DO_CRC
#undef DO_CRC4
#undef DO_CRC8
}

函数的第一步是对buf进行四字节的对齐,如果没有对齐,就反复调用Sarwate algorithm,直到buf对齐为止。如果原本机器是小端,由于这里crc和tab都是大端序,因此余数多项式的高次项系数被反转到了低位byte上,所以DO_CRC中拿到差别系数的方法是crc ^ (x)) & 255,即拿出余数的最低位。而如果原本机器是大端,因此crc和tab都没有被反转,余数多项式的高次项系数仍然在高位byte,所以DO_CRC中是用余数的最高位byte来查表(可以看到,这与原本的算法正好反过来了)。

对齐buf之后就是进行slice-by-8的算法了。重点是要想明白从byte序列中读一个u32后,该u32的bit如何对应到多项式的系数上。假设是小端机器,读一个u32后,原本byte序列的低地址对应余数多项式的高次系数,同时低地址又对应到u32的低位。因此这个u32的高位byte对应余数多项式的低位系数,但原本计算出的正向校验的tab是高位byte对应余数多项式的高位系数,因此我们把tab和原始的crc都转为大端序,这样便和读入的u32对齐了(对于大端机器分析,可以发现读入的u32是自然和原本的tab对齐的)。这便是转为大端序的本质原因

考虑到最后如果buf的长度不是8字节对齐的,该函数最后又调用Sarwate algorithm把非对齐的byte解决掉,然后返回最后的结果。

另外,上面转为大端序是基于计算正向校验的前提,根据对称性可以猜测,如果是计算反向校验,那么应该转为小端序(事实的确如此)。

5. 结

本文主要参考了Computation of Cyclic Redundancy Checks via Table Look-Up以及slice-by-N algorithm这两篇论文。算法本身并不难,不过看真实算法的代码时,考虑到各种细节还是挺绕的(尤其是端序的问题)。

另外,在思考有关端序的问题上,LSB, MSB这样的概念是有效的抽象(可以隔离掉端序的干扰)。

从crc32到linux内核实现相关推荐

  1. Linux内核移植之三:内核配置选项

    内容来自 韦东山<嵌入式Linux应用开发完全手册> Linux内核配置选项多达上千个,一个个地进行选择既耗费时间,对开发人员的要求也比较高(需要了解每个配置选项的作用).一般的做法是在某 ...

  2. Linux内核移植之一:内核源码结构与Makefile分析

    内容来自 韦东山<嵌入式Linux应用开发完全手册> 一.内核介绍 1.版本及其特点 Linux内核的版本号可以从源代码的顶层目录下的Makefile中看到,比如下面几行它们构成了Linu ...

  3. Linux 内核获取、初次编译、源码目录分析

    目录 Linux 内核获取 Linux 内核初次编译 Linux 内核源码目录分析 1.arch 目录 2.block 目录 3.crypto 目录 4.Documentation 目录 5.driv ...

  4. linux内核源码目录结构(2.6.35.7版本)

    以下内容源于朱有鹏嵌入式课程的学习,如有侵权,请告知删除. 1.单个文件 (1)Kbuild,Kbuild是kernel build的意思,就是内核编译的意思.这个文件就是linux内核特有的内核编译 ...

  5. 【华为云技术分享】Linux内核源码结构(1)

    在上一期中,我们介绍了Linux内核发展的历史,也介绍了与其相关的UNIX和GNU的相关知识.从这一期开始,我们将介绍Linux内核的源码结构.我们将先根据Linux源码的目录结构进行分析,到本文章发 ...

  6. Linux内核学习之路_1_编译Linux内核

    1.准备工作 1.1 学习环境 1.2 下载Linux内核源码 1.3 解压Linux内核 1.4 目录结构介绍 1.2.2 Linux内核配置 1.1 学习环境 本系列教程使用的环境如下: 操作系统 ...

  7. 【正点原子Linux连载】第三十五章 Linux内核顶层Makefile详解 -摘自【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.0

    1)实验平台:正点原子阿尔法Linux开发板 2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434 2)全套实验源码+手册+视频下载地址: ...

  8. 如何编译Linux内核文件

    如何编译Linux内核文件 参考:朱有鹏Uboot的全集 前言:我们的Linux内核文件动则数万个文件,很多个子文件夹,当然是使用Makefile管理了,但是是不是真的仅仅只是make一下就可以了呢? ...

  9. Linux内核源码组织结构

    本文主要参考韦东山老师的<嵌入式Linux应用开发完全手册>,基于Linux-2.6.32.2源码. 概要:本文内容包含Linux源码树结构分析.Linux Makefile分析.Kcon ...

最新文章

  1. Internet History, Technology, and Security----第一周
  2. 计算机应用培训资料,计算机应用培训资料.doc
  3. C# redis 分布式session存储
  4. 实验三进程调度模拟程序
  5. ModuleNotFoundError: No module named 'win32api'
  6. 读书笔记 - 企业精简架构
  7. Python内置函数之随机函数
  8. 彩色图像分割MATLAB代码
  9. xml解析-jaxp添加结点
  10. 中兴myos和鸿蒙,继华为鸿蒙系统以后!中兴再次发布新系统MyOS:可媲美苹果
  11. PTA(BasicLevel)-1009 说反话
  12. pymysql executemany()函数
  13. ROP攻击:Challenge 0x14: Horcruxes
  14. 《数据库原理与应用》学习笔记(一):概论
  15. [資源]RAID是什么意思?RAID的应用
  16. 通过PS调出胶片色调的古风照片
  17. 数独问题每行每列每3X3
  18. 如何在Vue项目中应用TypeScript?
  19. 使用 Python 生成二维码
  20. 常见的DNSBL(邮件黑名单),及DNSBL(邮件黑名单)的选择

热门文章

  1. ISO 26262 Functional Safety Requirement Types】
  2. quartz记录job状态
  3. 滚动条插件vue-scroll
  4. 工赋开发者社区 | MES/MOM系统的几种主流系统集成方式
  5. RTL8211F-CG硬件调试日志
  6. QToolButton双击事件
  7. 火眼云宣布完成5000万元A轮融资
  8. 一篇文章教你使用运放实现三角波、方波(详细电路分析)+multisim仿真
  9. PointCut切点表达式
  10. 理解3ds max 中的衰减贴图