对M4芯片的CRC模块改造来计算标准CRC32
【摘要】
最近使用的M4系列芯片中,有用于计算CRC的硬件CRC模块,这个模块计算出的校验和与我们平时使用的CRC32并不相同。下文用CRC’32指代M4的硬件CRC模块计算出的校验和,以与标准CRC32区分。虽然我们可以使用纯软件的方法来计算CRC32,不过,既然有CRC硬件,我们不妨试试对其进行加工,软件硬件配合来进行计算。一、 案例概述
这个硬件CRC模块有两个问题:其一,是其计算的CRC’32,计算过程与标准的CRC32算法并不一致;其二,是传给CRC模块的数据,必须以32位进行分组。若分组后剩余1~3字节,不足32位的情况,是无法计算的,必须补齐32位才行。但补齐之后,CRC值自然也就改变了。二、 问题原因分析
标准CRC32的模型如下:POLY = 0x04C11DB7, INIT_REM = 0xFFFFFFFF, FIN_XOR = 0xFFFFFFFF, REF_IN = TRUE, REF_REM = TRUE。
那么算法不一致,就一定是上述几项有不同。根据Data Sheet的描述:
Uses CRC-32 (Ethernet) polynomial: 0x4C11DB7.
The CRC calculator can be reset to 0xFFFF FFFF with the RESET control bit in the CRC_CR register.
从这两点可以确定的是POLY和INIT_REM的值,剩下的三个虽然不确定,但是取值有限,可以轻易穷举得出。
FIN_XOR的值虽然是32位的,有着丰富的可能性,但是常见取值只有两个,那就是0x00000000和0xFFFFFFFF。REF_IN和REF_REM都是布尔值,取值也只有TRUE和FALSE。这样组合之后,只有8种可能。依次测试对照,便可以确定,其CRC’32模型如下:
POLY = 0x04C11DB7, INIT_REM = 0xFFFFFFFF, FIN_XOR = 0x00000000, REF_IN = FALSE, REF_REM = FALSE。
是最糟糕的情况,也就是模型的后三个参数全部不同。
三、 解决方案
对于CRC算法不一致的问题,既然已经找到了模型之间的差异,自然就可以进行改造,也就是,在输入时逆序,在输出时逆序,输出后与0xFFFFFFFF异或(也就是全取反)。对于剩余不足32位的情况,这是硬件设计上的问题,索性CRC算法是一个纯粹的线性算法,而且是可逆的(这里提到的可逆,不是指用校验和推算原文),因此想要破解是很容易的。破解的方式无外乎两种:其一,是在数组前补若干字节,以凑齐32位;其二,是在数组后补若干字节,以凑齐32位。
以5字节举例说明,数组是D0 D1 D2 D3 D4,补齐的方式有如下几种:
前补三字节:?? ?? ?? D0, D1 D2 D3 D4
前补七字节:?? ?? ?? ??, ?? ?? ?? D0, D1 D2 D3 D4
前补11字节或者更多:暂不考虑
后补三字节:D0 D1 D2 D3, D4 ?? ?? ??
后补七字节:D0 D1 D2 D3, D4 ?? ?? ??, ?? ?? ?? ??
后补11字节或者更多:暂不考虑
对于前补字节的方法,我们只要保证这几个字节对计算CRC没有影响便可。换句话说,就是要这几个字节的CRC’32结果是0xFFFFFFFF,这样一来,寄存器的初值是0xFFFFFFFF,处理这几个字节之后的值也是0xFFFFFFFF,可以达到伪造校验和的目的。
对于后补字节的方法,我们只要简单的补零,并且在补零之后得到临时的CRC’32结果,然后利用CRC算法的可逆性,使用软件计算来逆向消除补零产生的影响,即可得到真正的CRC’32结果。
对比前补字节和后补字节的方法,前者需要事先计算出要补充的字节,然后交给硬件计算就行了;后者则统一补充零字节,但需要额外编写CRC的逆运算函数。二者相比,我们选择前者,接下来计算那些“魔术字节”。
四、 实践情况
根据CRC’32模型,编写M4特有的硬件CRC计算函数软件版如下:
<span style="font-size:14px;">unsigned long crc32_m4( const unsigned char *buf, unsigned long len )
{
unsigned long rem = 0xFFFFFFFF;
unsigned long byte_p = 0;
int bit_p = 0;
if (buf != 0)
{
for (byte_p = 0; byte_p < len; byte_p++)
{
for (bit_p = 7; bit_p >= 0; bit_p--)
{
if (((rem >> 31U) ^ (buf[byte_p] >> bit_p)) & 1U)
{
rem = (rem << 1U) ^ 0x04C11DB7U;
}
else
{
rem = rem << 1U;
}
}
}
}
return rem;
}</span>
CRC算法有一个很奇妙的性质,以32位的CRC来讲,那就是,当数据的位数小于等于32的时候,不同的数据得到的校验和一定不重复。1~4字节的数据,长度相同且内容不同的数据,得到的CRC32一定不同,当然CRC’32也一定不同。进一步说,对于32位的数据,其等效的32位整型值与32位CRC校验和是一一对应的,稍微花点时间就可以穷举出我们需要的“魔术字节”了。这样看来,前补三字节的方法很有可能不存在。实际上,对24位数据进行穷举,计算CRC’32的值,确实不存在校验和是0xFFFFFFFF的情况。
接下来讨论前补七字节的方法。前四字节的32位进行穷举,后三字节固定为零,即要寻找
CRC’32(?? ?? ?? ?? 00 00 00) == 0xFFFFFFFF 的情况。
<span style="font-size:14px;">#include <stdio.h>
#define LENGTH 7
int main()
{
unsigned long i[2] = { 0, 0 };
while (1)
{
if (crc32_m4 ((unsigned char *)i, LENGTH) == 0xFFFFFFFF)
{
printf ("ans: %08X
", i[0]);
break;
}
i[0]++;
}
return 0;
}</span>
算出这七个字节是:6A A5 9E 9D 00 00 00。将#define LENGTH 分别改成6和5,可以算出:
前补六个字节为:97 46 CD 0A 00 00
前补五个字节为:CC 60 21 D0 00
<span style="font-size:14px;">#include <stdint.h>
#include "stm32f4xx_crc.h"
#include "stm32f4xx_rcc.h"
uint32_t crc32_std( const uint8_t *buf, uint32_t len )
{
uint32_t ans = 0;
if (buf > 0 && buf + (len - 1) > buf)
{
uint32_t i = 0, v = 0;
RCC_AHB1PeriphClockCmd (RCC_AHB1Periph_CRC, ENABLE);
CRC_ResetDR ();
switch (len % 4)
{
while (1)
{
v = ((v >> 0x01) & 0x55555555) | ((v & 0x55555555) << 0x01);
v = ((v >> 0x02) & 0x33333333) | ((v & 0x33333333) << 0x02);
v = ((v >> 0x04) & 0x0F0F0F0F) | ((v & 0x0F0F0F0F) << 0x04);
CRC->DR = v; // <-- CRC_CalcCRC v;
if (i >= len)
{
break;
}
case 0: // 所有的字节可以按照4字节一组构成32位的组, 故不必前补额外的字节
v = buf[i] << 24 | buf[i + 1] << 16 | buf[i + 2] << 8 | buf[i + 3];
i += 4;
continue;
case 1: // 因多出1个字节无法构成32位, 故前补7字节以将之补齐
CRC->DR = 0x6AA59E9D; // <-- CRC_CalcCRC 0x6AA59E9D;
v = buf[i];
i += 1;
continue;
case 2: // 因多出2个字节无法构成32位, 故前补6字节以将之补齐
CRC->DR = 0x9746CD0A; // <-- CRC_CalcCRC 0x9746CD0A;
v = buf[i] << 8 | buf[i + 1];
i += 2;
continue;
case 3: // 因多出3个字节无法构成32位, 故前补5字节以将之补齐
CRC->DR = 0xCC6021D0; // <-- CRC_CalcCRC 0xCC6021D0;
v = buf[i] << 16 | buf[i + 1] << 8 | buf[i + 2];
i += 3;
continue;
}
default:
break;
}
v = CRC->DR; // <-- CRC_GetCRC;
v = ((v >> 0x01) & 0x55555555) | ((v & 0x55555555) << 0x01);
v = ((v >> 0x02) & 0x33333333) | ((v & 0x33333333) << 0x02);
v = ((v >> 0x04) & 0x0F0F0F0F) | ((v & 0x0F0F0F0F) << 0x04);
v = ((v >> 0x08) & 0x00FF00FF) | ((v & 0x00FF00FF) << 0x08);
v = ((v >> 0x10) & 0x0000FFFF) | ((v & 0x0000FFFF) << 0x10);
ans = ~v;
RCC_AHB1PeriphClockCmd (RCC_AHB1Periph_CRC, DISABLE);
}
return ans;
}</span>
代码中的CRC->DR是硬件CRC的寄存器,向其写值便是输入数据,从之读数便是获取校验和。代码中对变量v进行的大量位运算,是为了进行高低位的逆序。虽然可以使用查找表法来进行逆序,但是,要使用查找表的话,倒不如直接用查找表来软件计算CRC了。五、效果评价
在STM32F429上,使用该方法计算CRC32,与使用zlib库中的CRC32函数相比,在小数据量时,该方法比不过zlib库;在大数据量时,该方法快过zlib库一点,不是快很多。该方法的性能问题主要集中在,逆序时使用的大量位交换操作,这一点比M4的CRYP和HASH差的地方,就在于后两个模块可以定义输入数据的数据类型,由硬件进行位的逆序。