从crc32到linux内核实现
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∑7ti∗2i,ti∈{0,1}P(x)=i=0∑7ti∗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∑7ti∗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内核实现相关推荐
- Linux内核移植之三:内核配置选项
内容来自 韦东山<嵌入式Linux应用开发完全手册> Linux内核配置选项多达上千个,一个个地进行选择既耗费时间,对开发人员的要求也比较高(需要了解每个配置选项的作用).一般的做法是在某 ...
- Linux内核移植之一:内核源码结构与Makefile分析
内容来自 韦东山<嵌入式Linux应用开发完全手册> 一.内核介绍 1.版本及其特点 Linux内核的版本号可以从源代码的顶层目录下的Makefile中看到,比如下面几行它们构成了Linu ...
- Linux 内核获取、初次编译、源码目录分析
目录 Linux 内核获取 Linux 内核初次编译 Linux 内核源码目录分析 1.arch 目录 2.block 目录 3.crypto 目录 4.Documentation 目录 5.driv ...
- linux内核源码目录结构(2.6.35.7版本)
以下内容源于朱有鹏嵌入式课程的学习,如有侵权,请告知删除. 1.单个文件 (1)Kbuild,Kbuild是kernel build的意思,就是内核编译的意思.这个文件就是linux内核特有的内核编译 ...
- 【华为云技术分享】Linux内核源码结构(1)
在上一期中,我们介绍了Linux内核发展的历史,也介绍了与其相关的UNIX和GNU的相关知识.从这一期开始,我们将介绍Linux内核的源码结构.我们将先根据Linux源码的目录结构进行分析,到本文章发 ...
- Linux内核学习之路_1_编译Linux内核
1.准备工作 1.1 学习环境 1.2 下载Linux内核源码 1.3 解压Linux内核 1.4 目录结构介绍 1.2.2 Linux内核配置 1.1 学习环境 本系列教程使用的环境如下: 操作系统 ...
- 【正点原子Linux连载】第三十五章 Linux内核顶层Makefile详解 -摘自【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.0
1)实验平台:正点原子阿尔法Linux开发板 2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434 2)全套实验源码+手册+视频下载地址: ...
- 如何编译Linux内核文件
如何编译Linux内核文件 参考:朱有鹏Uboot的全集 前言:我们的Linux内核文件动则数万个文件,很多个子文件夹,当然是使用Makefile管理了,但是是不是真的仅仅只是make一下就可以了呢? ...
- Linux内核源码组织结构
本文主要参考韦东山老师的<嵌入式Linux应用开发完全手册>,基于Linux-2.6.32.2源码. 概要:本文内容包含Linux源码树结构分析.Linux Makefile分析.Kcon ...
最新文章
- Internet History, Technology, and Security----第一周
- 计算机应用培训资料,计算机应用培训资料.doc
- C# redis 分布式session存储
- 实验三进程调度模拟程序
- ModuleNotFoundError: No module named 'win32api'
- 读书笔记 - 企业精简架构
- Python内置函数之随机函数
- 彩色图像分割MATLAB代码
- xml解析-jaxp添加结点
- 中兴myos和鸿蒙,继华为鸿蒙系统以后!中兴再次发布新系统MyOS:可媲美苹果
- PTA(BasicLevel)-1009 说反话
- pymysql executemany()函数
- ROP攻击:Challenge 0x14: Horcruxes
- 《数据库原理与应用》学习笔记(一):概论
- [資源]RAID是什么意思?RAID的应用
- 通过PS调出胶片色调的古风照片
- 数独问题每行每列每3X3
- 如何在Vue项目中应用TypeScript?
- 使用 Python 生成二维码
- 常见的DNSBL(邮件黑名单),及DNSBL(邮件黑名单)的选择