首页 > 其他分享 >CRC 校验解析

CRC 校验解析

时间:2023-05-09 19:55:33浏览次数:38  
标签:字节 WIDTH 校验 long CRC 异或 解析 翻转

CRC 校验解析

一个 CRC 校验模型需要包含以下信息:

  • WIDTH,指 CRC 校验码的最终位数(二进制)
  • POLY,指用来做二进制除法的多项式。
  • INIT,指 CRC 的初始值。存在初始值是为了避免全 0 数据的校验码恒为 0。若初始值不为 0,则对于不同长度的全 0 数据,校验码一般也会不一样。
  • XOROUT,指最后对 CRC 校验码进行异或的数值。
  • REFIN,指是否对初始数据进行翻转。
  • REFOUT,指是否对输出数据进行翻转。

对于一些特定的、公开的模型,会有一个特定的名字。一般有模型的名字就可以知道其参数。比如 CRC4-ITU 代表的就是 WIDTH = 4, POLY = 0x03, INIT = 0x00, XOROUT = 0x00, REFIN = true, REFOUT = true 的一个 CRC 模型。

无论是有名字的模型还是没有名字的(自定义)模型,其框架都是一样的。所以我们可以先把代码框架搭建好。

class CRCCommon {
    public:
    	virtual const long long unsigned operator()(char(*nextData)(), size_t length) const=0; // 因为 CRC 对于数据是逐字节处理,所以每次获取的数据是 char 类型。使用函数指针,既可以兼容读取数组,也可以兼容直接从输入流获取数据。
    	long long unsigned file(FILE*) const; // 获取一个文件的校验码
};

template <unsigned WIDTH, long long unsigned POLY, long long unsigned INIT, long long unsigned XOROUT, bool REFIN, bool REFOUT>
class CRC : public CRCCommon {
    private:
    	CRC() {}
    public:
    	~CRC() {}
    	virtual const long long unsigned operator()(char(*nextData)(), size_t length); // 对于每个模型其计算方式不一样,所以每个子类需要用虚函数来实现该功能。
    	static CRC* getInstance() {
            // 每个实例会占用一个虚函数指针的空间。由于我们只需要使用 CRC 类的成员函数,不需要每次都定义一个实例。同时定义多个实例也会使得空间浪费。所以使用单例模式。
        	static CRC* crc = new CRC();
        	return crc;
    	}
};

为了更好理解 CRC 校验模型的原理并写出其代码,下面进行 CRC 手算。

比如说现在有这样一个模型:WIDTH = 5, POLY = 0xd, INIT = 0xf, XOROUT = 0x1, REFIN = false, REFOUT = true。而输入的数据是 0x1234

CRC 对于数据是逐字节处理的。输入数据共分为两个字节,分别是 0x120x34

定义 crc 表示校验码的值。初始时为 INIT = 0xf,然后异或第一字节,crc = 0xf ^ 0x12 = 0x1d = 0b00011101

这个数可以看做多项式 \(x^4+x^3+x^2+x^0\) 在 \(x=2\) 时的取值。上述多项式若看做 \(4\) 次多项式,则其系数按照降幂排列则为 1, 1, 1, 0, 1,恰好是 0b11101 的二进制表示。若看做更高次多项式,如 \(9\) 次多项式,则其系数按照降幂应为 0, 0, 0, 0, 0, 1, 1, 1, 0, 1,变成二进制数则是 \(10\) 位二进制数 0b0000011101

令二进制下 \(k\) 位数(包括前导 \(0\))对应一个 \(k-1\) 次多项式,则 0b11101 对应多项式 \(x^4+x^3+x^2+x^0\)。而我们给定的多项式 POLY = 0xd,其实应该是 WIDTH = 5 次多项式,任何次数高于 \(5\) 的项系数都是 \(0\),且 \(5\) 次项的系数必须是 \(1\)(这是人为规定的)。所以在表示 POLY 的时候,就偷懒不表示 \(5\) 次项系数了(根据 WIDTH = 5 即可补全 POLY)。所以 POLY 应该是 (1 << 5) | 0xd = 0x2d = 0b101101,它对应一个 \(5\) 次多项式。

CRC 校验的实质,就是求出 原数据左移 WIDTH 位之后,对 POLY 进行二进制取模的结果。

若对多个字节进行校验,则将上一字节校验的结果作为 INIT,异或该字节数据后,对 POLY 进行取模,得到的结果作为该字节的校验结果。也可以认为 INIT 是第 \(0\) 字节校验的结果,然后对于每一字节的校验就是当前字节的数据异或上一字节的结果,再进行取模。

所以,若对多个字节进行校验,上一字节校验的结果必定是二进制下 WIDTH 位数,一个字节固定是二进制下 \(8\) 位数。若 WIDTH > 8,则异或后的结果至多是 WIDTH 位数,对应一个 WIDTH-1 次多项式。但 POLYWIDTH 次多项式,取模的结果一定还是原数据。所以要先把原数据 左移 WIDTH 位再进行取模。

接下来说说“二进制取模”是如何运算的。

首先看看正整数的取模。有这样一种方法:比如十进制下 \(1234\) 对 \(9\) 取模,先在 \(9\) 后面添若干个 \(0\),使其变成不大于 \(1234\) 的最大的数,也就是 \(900\),然后原数 \(1234\) 一直减去 \(900\) 直到不能再减,得到 \(334\)。

下一步,去掉一个 \(0\),变成 \(90\)。然后 \(334\) 不断减去 \(90\),可以得到 \(64\)。

最后,不停减去 \(9\),得到 \(1\),所以 \(1234\) 对 \(9\) 取模的结果就是 \(1\)。

二进制取模原理相似,但是运算是在二进制下进行的,而且“减法”变成了“按位异或”。

例如,上面原数据,先异或 INIT,再左移 WIDTH 位后为 0b1110100000,而多项式是 0b101101。先在 POLY 后面添加若干个 \(0\),使得其首位和被除数首位对齐。虽然这个时候 POLY 的值已经大于原数据,但是我们采用的运算不是减法,而是按位异或,所以不会出现负数的情况。我们可以直接异或,因为首位(最高位)都是 \(1\),所以异或的结果必然会小于当前 POLY(最高位异或结果为 \(0\),异或的结果至少会比运算数少一位)。这样经过有限次运算之后,就可以得到一个小于 POLY 的数。这个数就是取模的结果。

比如说,用 x 代表原数据,p 表示多项式。则一开始

x = 0b1110100000
p = 0b101101 // 结尾的 0 省略不写
x = 0b 101110 // 异或运算的结果,和原始数据位对齐。整数除法的竖式也是位对齐的,不是吗?
p = 0b 101101
x = 0b     11000 // 已经小于原始多项式(即没有在后面补 0 的),所以运算结束

因此,xp 取模的结果就是 十进制下的 \(24\)。

然后再把 \(24\) 异或下一个字节的数据 0x34,继续对 POLY 取模可以得到 0x16

然后,由于 REFOUT = true,所以对 0x16 在 \(5\) 位二进制下进行位翻转(需要考虑前导 \(0\),比如 0b01111 翻转的结果是 0b11110 而非 0b1111。因为是在 \(5\) 位二进制下翻转而不是在有效位内翻转,所以补足 \(5\) 位用到的前导 \(0\) 也要翻转),得到的结果是 0x0d。最后异或 XOROUT = 0x1 就可以得到数据 0x1234 的自定义 \(5\) 位 CRC 模型校验码为 0x0c = 12

那么上述并没有提及到的 REFIN 是什么意思呢?当 REFIN = false 的时候,我们并不需要进行多余操作。而当 REFINtrue 的时候,我们需要把输入数据的 每一字节 按位翻转,也就是在 \(8\) 位二进制下翻转每一字节的数据。注意是 每一字节 翻转。REFIN 的翻转与 REFOUT 翻转的不同之处就在于,REFIN 是对输入数据的 每一字节 进行 \(8\) 位二进制翻转;而 REFOUT 是对输出的 校验码 进行 WIDTH 位二进制翻转(也应注意这个操作在异或 XOROUT 之前)。

例如,输入数据 0x1234 每字节翻转为 0x482c,因为 0x12 的翻转是 0x480x34 的翻转是 0x2c,所以每字节翻转的结果就是 0x482c,字节内的位翻转,字节间的顺序不变。

所以,0x1234REFIN = false 的情况下得出的 CRC 码,与 0x482cREFIN = true ,其他参数相同的情况下得出的 CRC 码是相同的。REFIN 的翻转应该是一拿到这一字节的数据就立刻进行翻转,翻转之后再与上一字节的结果异或,再进行二进制取模运算。


接下来考虑一下如何用代码实现 CRC 校验。

首先,不同的 CRC 模型的宽度 WIDTH 是不一样的,甚至可以自定义。但是一般来说,因为一个字节的宽度是 \(8\),目前最大的数据类型宽度是 \(64\),所以一般选取的 CRC 模型宽度下限是 \(8\),上限是 \(64\)。(但是也有一些宽度为 \(4,5,6,7\) 的公开模型)

回忆一下二进制取模的过程,要首先“使得其(除数多项式)首位和被除数首位对齐”。如何找到被除数的最高位?又如何使得除数与之对齐?需要考虑到的问题是,若 CRC 模型的宽度达到了最大值 \(64\) 位,那么在后面添 \(64\) 个 \(0\) 之后就变成了 \(128\) 位,已经超出了最大宽度范围;而且此时 POLY 的 \(64\) 次位(第 \(65\) 位)也要补一个 \(1\),也就是 POLY 实际上是 \(65\) 位。这样的话如果直接做那么无论是 被除数 还是 多项式除数 都已经超过了最大宽度。

为了解决这个问题,对于 WIDTHCRC 模型,我们强制所有操作只能在 WIDTH 位下进行,哪怕 WIDTH 可能只是 \(8\),使用 int 类型完全可以存储所有数据。但是考虑到方法的兼容性,我们必须想出一种只在 WIDTH 位下操作的方法。

“在原数据后面补 WIDTH 个 \(0\)”的操作可以分成 WIDTH 次做,每次只补一个 \(0\)。每次补一个 \(0\),数据就会左移一位,原数据的最高位就会被舍弃。

一开始,被除数是 WIDTH 位,所有高于 WIDTH 的位置都是 \(0\)。现在我们依次在被除数后面加 \(0\)。

若某次加一个 \(0\) 之前,第 WIDTH 位是 \(1\),那么加了 \(0\) 之后,第 WIDTH+1 位就会变成 \(1\)。因为 POLY 的第 WIDTH+1 位要补个 \(1\),所以这时 被除数和除数最高位对齐了!!!这时我们在补 \(0\) 之后就要进行异或了,异或之后第 WIDTH+1 位变成 \(0\),保持了“所有高于 WIDTH 的位置都是 \(0\)”这个性质。反之如果加 \(0\) 之前第 WIDTH 位是 \(0\),那么左移之后就不用进行异或。

然而实际操作时,因为 POLY 的第 WIDTH +1 位已经超出了宽度限制,所以我们不会把这位的 \(1\) 补上。同样,如果被除数的第 WIDTH 位是 \(1\),那么左移之后这一位就会被舍弃,我们并不能获取这一位的任何信息。所以我们要在左移之前就进行判断,如果第 WIDTH 位是 \(1\),那么左移之后再异或 POLY

举个例子,WIDTH = 7, POLY = 0x3d, INIT=XOROUT=0, REFIN=REFOUT=false 的一个模型,只针对一个字节的校验,代码实现是这样的:

int crc_7_3d(const char data) {
    // WIDTH = 7, POLY = 0x3d, INIT=XOROUT=0, REFIN=REFOUT=false
    // 注意该函数只针对一个字节进行校验。实际进行多字节校验还需要使用一个 while 循环。
    // 注:该代码正确性未经过检验,仅作为示例帮助理解。
    int WIDTH = 7;
    long long unsigned poly = 0b00111101; // 实际 poly 应该是 0b10111101,但是并不储存最高位的 1
    long long unsigned x = data;
    for(int i = 0; i < WIDTH; ++i) {
        if(x & 0b1000000) x = (x << 1) ^ poly; // 第 7 位左移后成为第 8 位,舍弃,所以 poly 的最高位没有储存并不是问题。
        else x <<= 1;
    }
    return x & 0b1111111; // 取最低 7 位返回
}

观察最后返回那一行,需要按位与 0b1111111。这是因为我们采取 long long unsigned 来存储数据,而左移的时候溢出位并没有真正“舍弃”,我们的操作只是“不予理睬”。实际上溢出的 \(1\) 是还在的,而 POLY 没有储存最高位的 \(1\),导致没有异或掉。所以溢出位的存在会干扰最后的结果,所以要按位与 0b1111111

以下介绍两种不同的实现方法,仅作了解即可:

有一种方法是把 WIDTH 个有效位全部送上储存类型的最高位进行操作。比如上述例子是在 long long unsigned 的第 1~7 位(称最低位为 第 \(1\) 位)进行操作,那么我们也可以把所有数据都 左移到 long long unsigned 的第 58~64 位操作,这样每次左移就会立即溢出。但是同样在返回之前,需要把第 58~64 位操作的结果 重新右移 到第 1~7 位再返回。

int crc_7_3d(const char data) {
    // WIDTH = 7, POLY = 0x3d, INIT=XOROUT=0, REFIN=REFOUT=false
    // 注意该函数只针对一个字节进行校验。实际进行多字节校验还需要使用一个 while 循环。
    // 注:该代码正确性未经过检验,仅作为示例帮助理解。
    int WIDTH = 7;
    long long unsigned poly = 0b00111101 << (64- WIDTH); // 实际 poly 应该是 0b10111101,但是并不储存最高位的 1
    long long unsigned x = data << (64- WIDTH);
    for(int i = 0; i < WIDTH; ++i) {
        if(x & 0x8000000000000000) x = (x << 1) ^ poly; // 判断原 WIDTH 位,今最高位,是否为 1。
        else x <<= 1;
    }
    return x >> (64 -WIDTH); // 右移复位
}

还有一种方法是在低位进行操作,但是把 被除数、除数 都进行 WIDTH 位二进制翻转,然后左移操作变成右移操作,最高位就变成最低位,判断时可以直接按位与 0b1 得到。而右移的时候也会直接舍弃。

这样的话我们首先需要一些进行二进制翻转的函数。

inline unsigned short reverseShort(unsigned short x) {
    // 执行 16 位二进制翻转
	x = ((x & 0x00ff) << 8) | ((x & 0xff00) >> 8); // 字节之间交换顺序,字节内顺序不变。
	x = ((x & 0x0f0f) << 4) | ((x & 0xf0f0) >> 4); // 半字节之间交换顺序,半字节内顺序不变。
	x = ((x & 0x3333) << 2) | ((x & 0xcccc) >> 2); // 每两位交换顺序,这两位的相对顺序不变。
	x = ((x & 0x5555) << 1) | ((x & 0xaaaa) >> 1); // 相邻两位交换顺序。
	return x; // 如果不能理解这一函数,可以手动模拟实现一下。建议把执行按位与的数写成二进制观察一下他们的规律。
}

inline unsigned reverseInt(unsigned x) {
    // 执行 32 位二进制翻转
	return (reverseShort(x & 0xffff) << 16u) | reverseShort(x >> 16);
}

inline long long unsigned reverseLong(long long unsigned x) {
    // 执行 64 位二进制翻转。
	return ((long long unsigned)reverseInt(x & 0xffffffff) << 32) | reverseInt(x >> 32);
}

实际情况下,为了程序的兼容性,进行 WIDTH 位翻转时,我们会使用 reverseLong(x) >> (64 - WIDTH) 就可以进行 WIDTH 位二进制翻转。

int crc_7_3d(const char data) {
    // WIDTH = 7, POLY = 0x3d, INIT=XOROUT=0, REFIN=REFOUT=false
    // 注意该函数只针对一个字节进行校验。实际进行多字节校验还需要使用一个 while 循环。
    // 注:该代码正确性未经过检验,仅作为示例帮助理解。
    int WIDTH = 7;
    long long unsigned poly = reverseLong(0b00111101) >> (64- WIDTH); // 实际 poly 应该是 0b10111101,但是并不储存最高位的 1
    long long unsigned x = reverseLong(data) >> (64- WIDTH);
    for(int i = 0; i < WIDTH; ++i) {
        if(x & 0b1) x = (x >> 1) ^ poly; // 判断原 WIDTH 位,今最高位,是否为 1。
        else x >>= 1;
    }
    return reverseLong(x) >> (64 -WIDTH); // 反转回来。
}

这个方法也有一个要注意的地方。就是实现多字节校验时,应该是该字节的数据异或到上一字节的结果的低 \(8\) 位。但是由于进行了翻转,所以应该是该字节的数据翻转后,异或到上一字节结果的高 \(8\) 位。所以此时就不是 reverseLong(data) >> (64 - 8),而应该也是 reverseLong(data) >> (64 - WIDTH)


上述提到了三种方法,不同的程序会采取不同的方法,因此列出以作了解。后两种方法理解起来比较复杂,所以我们采取第一种方法来构建我们的代码。

还记得前面我们搭建的代码框架吗?我们使用了继承的方法,每一个子类都继承自父类 CRCCommon,需要实现一个虚函数重载运算符。

在此之前回忆(总结)一下 CRC 校验的步骤:

  1. 获取该字节数据。若 REFIN = true 则进行翻转。
  2. 异或上一字节的结果。若上一字节为第 \(0\) 字节则异或 INIT
  3. 执行 WIDTH 次补 \(0\)、判断、异或 的 二进制取模操作。
  4. 若存在下一字节数据,则回到第 \(1\) 步。
  5. REFOUT = true 则将校验码进行 WIDTH 位二进制翻转。
  6. 将校验码异或 XOROUT 后返回,作为校验值。
inline unsigned short reverseByte(unsigned short x) {
    // 进行一字节的二进制翻转。
	x = ((x & 0x0f) << 4) | ((x & 0xf0) >> 4);
	x = ((x & 0x33) << 2) | ((x & 0xcc) >> 2);
	x = ((x & 0x55) << 1) | ((x & 0xaa) >> 1);
	return x;
}

template <unsigned WIDTH, long long unsigned POLY, long long unsigned INIT, long long unsigned XOROUT, bool REFIN, bool REFOUT>
virtual const long long unsigned CRC<WIDTH, POLY, INIT, XOROUT, REFIN, REFOUT>::operator()(char(*nextData)(), size_t length) const {
    // 注:该代码仅通过了一部分正确性验证。剩余部分验证将逐步完善。
    // 验证网站:http://www.ip33.com/crc.html
    long long unsigned x = INIT;
    long long unsigned poly = POLY;
    if(REFIN) {
        while(length--) {
            x ^= reverseByte(nextData());
            for(unsigned i = 0; i < WIDTH; ++i) {
                if(x & (1 << (WIDTH - 1))) x = (x << 1) ^ poly;
                else x <<= 1;
            }
        }
    } else {
        while(length--) {
            x ^= nextData();
            for(unsigned i = 0; i < WIDTH; ++i) {
                if(x & (1 << (WIDTH - 1))) x = (x << 1) ^ poly;
                else x <<= 1;
            }
        }
    }
    x &= (1ull << WIDTH) - 1; // 取低 WIDTH 位
    if(REFOUT) x = reverseLong(x) >> (64 - WIDTH); // reverseLong 的定义在上文代码处。
    return x ^ XOROUT;
}

标签:字节,WIDTH,校验,long,CRC,异或,解析,翻转
From: https://www.cnblogs.com/LASS/p/17386057.html

相关文章

  • 解析内存中的高性能图结构
    在进行各种图处理、图计算、图查询的时候,内存或是硬盘中如何存储图结构是一个影响性能的关键因素。本文主要分析了几种常见的内存图结构,及其时间、空间复杂度,希望对你有所启发。通常来说,对于图结构的几种常见的基础操作:插入一个点插入一个边删除一个边删除一个点的全部邻边......
  • 【configparser】Python解析配置文件的模块使用总结
    简介configparser是Pyhton标准库中用来解析配置文件的模块,并且内置方法和字典非常接近。Python2.x中名为ConfigParser,3.x已更名小写,并加入了一些新功能。调用importconfigparserconfig=configparser.ConfigParser()config.read("config.ini")常用方法#获取所用......
  • elementUI 多层结构动态生成el-form及校验
    如题,当整个el-form都是通过多层数据结构循环渲染出来的表单,那么每个el-form-item的prop和el-input、el-select等的v-model也是动态变量填充,怎么样才能内嵌rules校验呢?如下数据结构://form通过initData数据结构进行渲染constinitData=[{channel:'sms',title......
  • 逆向-第五次实验-PE文件解析
    #include<stdio.h>#include<string.h>#include<windows.h>charFileName[100]={0}; voidPrintNTHeaders();LPVOIDReadPEFile(); intmain(){ printf("Pleaseinput:(forexample:D:/user/Desktop/PE文件对齐、内存对齐/解析pe头文件/实验.exe)\n"......
  • 解决antd form表单校验错误时,设置scrollToFirstError 不能滚动到第一个校验错误位置
    使用antdform表单自带属性scrollToFirstError校验不通过时自动滚动到第一个校验错误位置,但是经常没有效果,手动添加一个滚动方法来处理//表单滚动到第一个报错处(antd)exportconstscrollToFirstError=()=>{document.querySelector('.ant-form-item-has-error')?.scro......
  • python Django校验表单登录案例
    定义一个视图函数,用于处理登录表单的提交动作。在该视图函数中,使用request.POST.get()方法获取POST请求中提交的用户名和密码数据,具体代码如下:fromdjango.shortcutsimportrender,redirectfromdjango.contrib.authimportauthenticate,logindeflogin_view(requ......
  • 螣龙安科实力再认证!受邀分享攻击面管理核心技术解析
    为靶向大型企业业务特点和实际需求,关注网络安全技术和科技发展,深入探讨业务与网络安全合规与新技术的结合实践,近日,由(ISC)²上海分会主办“金融行业一高端CSO研讨交流会”在上海交通大学长宁校区顺利举行。螣龙安科作为本次研讨会的支持单位协助活动圆满落幕。  螣龙安科CEO王......
  • vue+element输入框校验输入汉字再输入数字看似正常,实则有大问题,保存时数据不对
    在vue+elementUI项目中经常会使用到输入框限制为整数或者小数的需求,一般采用如下oninput="value=value.replace(/[^0-9.]/g,'')"解决,<el-input    :placeholder="请输入整数或者小数"    v-model="inputValue"   oninput="value=value.replace(/[^0-9......
  • Java反射--2021面试题系列教程(附答案解析)--大白话解读--JavaPub版本
    >Java反射--2021面试题系列教程(附答案解析)--大白话解读--JavaPub版本前言序言再高大上的框架,也需要扎实的基础才能玩转,高频面试问题更是基础中的高频实战要点。适合阅读人群Java学习者和爱好者,有一定工作经验的技术人,准面试官等。阅读建议本教程是系列教程,包含Java基础,JVM,容器,......
  • 运用nginx和阿里云解析配置二级域名
    进入阿里云管理控制台,在左侧菜单选择云解析,nginx配置文件的配置如下,配置完成后重启nginx即可公众号:chengziboke888......