您的位置:首页 > 编程语言 > C语言/C++

使用乘法表计算GF(2^8)中的乘法

2013-10-12 15:02 225 查看
作业要求:
构造256字节的表格X2,使得X2[i] ={02}*i,i是GF(2^8)中的元素。将此表运用于GF(2^8)的乘法,与之前作业的乘法对比,看谁的速度快?

直奔主题吧。
先看看主函数:

int main()
{
//  -- 构造乘法表X2 --
createX2();

//  -- 进入乘法运算程序 --
int abin[8], bbin[8], c[8];
LARGE_INTEGER start_t, stop_t, freq; // start_t表示计时开始时间,stop_t表示计时结束时间,freq为计时器的时钟频率
double exe_time;
QueryPerformanceFrequency(&freq);
// fprintf(stdout, "The frequency of your pc is %d.\n", freq.QuadPart);

char choose='y';
while(choose=='y'||choose=='Y')
{
//  -- 清空之前的计算结果 --
for(int i=0; i<8; i++)
{
c[i]=0; // 清空结果
}
state=0; // 清空当前查表次数

//  -- 输入乘数a和b --
if(input(abin, 'a')==0) // 如果输入a没有出错
{
if(input(bbin, 'b')==0) // 如果输入b没有出错
{
//  -- 程序开始执行,开始计时 --
QueryPerformanceCounter(&start_t);

// -- 将输入的二进制数组转换为十进制数组并保存到temp中 --
int temp[2];
temp[0]=binToDec(abin, 0);
temp[1]=binToDec(abin, 1);

//  -- 执行乘法运算 --
for(i=7; i>=0; i--)
{
if(bbin[i]==1)
{
xor(c, leftshift(abin, 7-i, temp)); // 求和
}
}

//  -- 输出运算结果 --
cout<<"运算结果为:";
for(i=0; i<8; i++)
{
cout<<c[i];
}

//  -- 程序结束执行,结束计时 --
QueryPerformanceCounter(&stop_t);
exe_time = 1e3*(stop_t.QuadPart-start_t.QuadPart)/freq.QuadPart;
cout<<endl;
fprintf(stdout, "计算用时:%fms.\n", exe_time);
}
}

//  -- 是否继续执行程序 --
cout<<endl<<"是否继续?y/n:";
choose=cin.get();
if(cin.get()==10)
{
// 跳过输入y/n后的回车键,防止影响下面的输入
}
}

return 0;
}


程序流程:构造乘法表X2 —— 初始化工作 ——
输入数a和数b —— 用查表法计算乘法结果 —— 输出运算结果和运算所用时间 —— 是否循环执行程序。

下面分段来看。
1.构造乘法表X2
为了便于使用该表,将其声明为全局静态变量:

static int *xtable[16][16]; // 乘法表X2

构造表的函数:

/* 建造乘法表X2 */
void createX2()
{
int temp=0, arr[2], bin[8];
int j, k;

// 初始化xtable前半部分:00-7F
for(j=0; j<8; j++)
{
for(k=0; k<16; k++)
{
intToArr(temp*2, arr);
temp++;
xtable[j][k]=new int[2];
xtable[j][k][0]=arr[0];
xtable[j][k][1]=arr[1];
}
}
// 初始化xtable后半部分:80-FF
for(j=8; j<16; j++)
{
for(k=0; k<16; k++)
{
intToArr(temp, arr);
decToBin(arr, bin);
leftshift1(bin);
arr[0]=binToDec(bin, 0);
arr[1]=binToDec(bin, 1);
xtable[j][k]=new int[2];
xtable[j][k][0]=arr[0];
xtable[j][k][1]=arr[1];
temp++;
}
}
}

由于00-7F的运算结果为00-FE,没有溢出,所以可以作为表的前半部分初始化。80-FF全部溢出,需要先左移1位再异或1BH,所以作为表的后半部分初始化。
注意由于xtable二维数组中的元素均为指针类型,如果对于每一次赋值都使用xtable[j][k]=arr;那么xtable中所有指针都指向同一块内存块arr,由于arr[2]最后的结果为14 5,所以所有xtable中的元素都指向{14, 5}数组,因此必须首先new一个xtable[j][k]指针,其长度为2,然后分别对其中的两个元素赋值。

在后半部分初始化中,例如255,先要用intToArr函数将其转化为两位的16进制数组(每一位都是十进制数),即{15,15}:

/* 将整数转换为数组,例如78=4 14
* 参数temp为要转换的整数
* 参数a[]为转换结果
*/
void intToArr(int temp, int a[])
{
a[1]=temp%16;
a[0]=(temp-a[1])/16;
}

然后用decToBin函数将两位16进制数组转换为二进制数组,即11111111:

/* 将2位十进制数组转换为8位二进制数组
* 参数d[]为要转换的十进制数组
* 参数bin[]为转换后的二进制数组
*/
void decToBin(int d[], int bin[])
{
int h0=d[0];
int h1=d[1];

int i=3;
while(i>=0)
{
bin[i--]=h0%2;
h0/=2;
}

i=7;
while(i>=4)
{
bin[i--]=h1%2;
h1/=2;
}
}

先将转换后的数组bin左移1位,再作溢出处理,即使用leftshift1函数:

/* 乘2,左移1位,如果溢出则异或1BH */
void leftshift1(int a[])
{
int temp=a[0];
for(int i=0; i<7; i++)
{
a[i]=a[i+1];
}
a[7]=0;

if(temp==1) // 溢出处理
{
int mod[8]={0, 0, 0, 1, 1, 0, 1, 1};
xor(a, mod);
}
}


将转换的二进制数组转换为两位16进制数组:

/* 将4位二进制数组转换为十进制数
* 参数b[]为要进行转换的数组
* 参数bit用于判断高低位,0为高位,1为低位
* 返回结果为转换后的十进制数
*/
int binToDec(int b[], int bit)
{
int n=8, i=0, sum=0;
if(bit==0) // 高四位二进制转换十进制
i=0;
else if(bit==1) // 低四位二进制转换十进制
i=4;
else
return -1;

// 4位二进制转换十进制,例如1011=11
while(n>=1)
{
sum+=(n*b[i]);
i++;
n/=2;
}
return sum;
}

注意,为什么每一位都是十进制数呢(例如F对应15),原因是查表时行号和列号是直接用十进制查的,这样便于迭代查表。虽然转换麻烦了点,但是在做乘法查表时不用再转换。
如果表格为两位16进制字符数组,那么结果为:

static char *sbox[16][16]={
{"00", "02", "04", "06", "08", "0A", "0C", "0E", "10", "12", "14", "16", "18", "1A", "1C", "1E"}, // 0
{"20", "22", "24", "26", "28", "2A", "2C", "2E", "30", "32", "34", "36", "38", "3A", "3C", "3E"}, // 1
{"40", "42", "44", "46", "48", "4A", "4C", "4E", "50", "52", "54", "56", "58", "5A", "5C", "5E"}, // 2
{"60", "62", "64", "66", "68", "6A", "6C", "6E", "70", "72", "74", "76", "78", "7A", "7C", "7E"}, // 3
{"80", "82", "84", "86", "88", "8A", "8C", "8E", "90", "92", "94", "96", "98", "9A", "9C", "9E"}, // 4
{"A0", "A2", "A4", "A6", "A8", "AA", "AC", "AE", "B0", "B2", "B4", "B6", "B8", "BA", "BC", "BE"}, // 5
{"C0", "C2", "C4", "C6", "C8", "CA", "CC", "CE", "D0", "D2", "D4", "D6", "D8", "DA", "DC", "DE"}, // 6
{"E0", "E2", "E4", "E6", "E8", "EA", "EC", "EE", "F0", "F2", "F4", "F6", "F8", "FA", "FC", "FE"}, // 7
{"1B", "19", "1F", "1D", "13", "11", "17", "15", "0B", "09", "0F", "0D", "03", "01", "07", "05"}, // 8
{"3B", "39", "3F", "3D", "33", "31", "37", "35", "2B", "29", "2F", "2D", "23", "21", "27", "25"}, // 9
{"5B", "59", "5F", "5D", "53", "51", "57", "55", "4B", "49", "4F", "4D", "43", "41", "47", "45"}, // A
{"7B", "79", "7F", "7D", "73", "71", "77", "75", "6B", "69", "6F", "6D", "63", "61", "67", "65"}, // B
{"9B", "99", "9F", "9D", "93", "91", "97", "95", "8B", "89", "8F", "8D", "83", "81", "87", "85"}, // C
{"BB", "B9", "BF", "BD", "B3", "B1", "B7", "B5", "AB", "A9", "AF", "AD", "A3", "A1", "A7", "A5"}, // D
{"DB", "D9", "DF", "DD", "D3", "D1", "D7", "D5", "CB", "C9", "CF", "CD", "C3", "C1", "C7", "C5"}, // E
{"FB", "F9", "FF", "FD", "F3", "F1", "F7", "F5", "EB", "E9", "EF", "ED", "E3", "E1", "E7", "E5"}  // F
//  0     1     2     3     4     5     6     7     8     9     A     B     C     D     E     F
};


2.用查表法计算乘法结果
这里沿用了移位乘法的程序框架,只是计算的方法不同而已:

//  -- 执行乘法运算 --
for(i=7; i>=0; i--)
{
if(bbin[i]==1)
{
xor(c, leftshift(abin, 7-i, temp)); // 求和
}
}

leftshift函数为:

/* 计算a[]乘上2的len次幂的结果
* 参数bin[]为要进行移位的数组
* 参数len为查表的次数
* 参数temp[]用于保存乘法运算结果
* 返回结果为乘2的运算结果
*/
int *leftshift(int bin[], int len, int temp[])
{
if(len==0)
{
state=0;
return bin;
}

int row, column;
for(state; state<len; state++)
{
row=temp[0];
column=temp[1];
temp[0]=xtable[row][column][0];
temp[1]=xtable[row][column][1];
}

decToBin(temp, bin); // 将16进制的temp转换为二进制数组bin

return bin;
}


state为全局静态变量:

static int state=0; // 用于记录左移的位数,也就是已经查表的次数


其中一次查表就相当于一次左移(包括溢出处理)运算。
这里优化程序的思想和在GF(2^8)下可做任意两个数乘法的程序(一)中的乘法思想是一样的,为了避免每次都要从头开始查表,每次运算结果都用temp保存起来,用于下一次运算的查表。
例如:求a * 00000110,从右边开始,由于b[6]=1,所以先查表一次计算得到a * 2,并用temp保存a * 2的结果,在求a * 4的时候,可以直接用temp(也就是a * 2的结果)查表一次得到结果,如果没有用到temp的话,那么* 4必须要查表2次。最后将a * 2和 a * 4异或即可。
如果a中1的个数越多,那么优化后的程序效率更高(单纯就乘法该过程看),例如如果是a * 11111111,那么不优化的情况下要查表(每次查表对应一次乘2)1+2+3+4+5+6+7=28次。如果按以上的方法优化了,只需要查表7次就可以了。

至于输入数a和数b ,异或加运算等函数和在GF(2^8)下可做任意两个数乘法的程序(一)给出的一样,不再赘述。

运行一下,比较两者的运行效率:
乘法表运算时间结果:



直接乘法运算时间结果:



首先有一点要强调的是,由于CPU每个时刻工作的速度和时钟频率等都处于动态变化当中,所以以上数据不能算绝对准确,加上程序员不同的算法也会导致程序运行的时间出现差异,因此以上数据仅仅用于对比,而且也不能算得上绝对准确。

比较之下,乘法表运算多了一些转换的工作(包括leftshift函数,因为其中的异或运算必须要使用二进制数组,而且输出结果形式也是二进制形式),也就是在乘法表的条件差于移位乘法的情况下其效率仍然高于后者,所以也得出了老师想向我们传递的信息:GF(2^8)中的乘法运算查表运算的效率高于直接运算。
当初AES的设计者就是用该方法来对乘法进行加速(另外乘法可能会受到时间分析攻击),另外表所使用的空间开销并不大,用查表计算乘法结果的方法优于直接使用乘法。

最后,小结一个问题:
对于调用函数,如果传入参数的是数组指针并且在函数中修改了参数的值,那么数组的值将相应变化。如果传入参数的是基本类型并且在函数中修改了参数的值,基本类型变量的值却不会相应发生变化。例如:

#include <iostream>
using namespace std;

void setTemp(int temp)
{
temp=1;
}

void setArr(int a[])
{
a[0]=1;
a[1]=1;
}

void setBool(bool b)
{
b=false;
}

int main()
{
int temp=0;
bool b=true;
int arr[2]={0,0};

setTemp(temp);
setArr(arr);

cout<<"temp="<<temp<<endl;
cout<<"b="<<b<<endl;
cout<<"arr[0]="<<arr[0]<<endl;
cout<<"arr[1]="<<arr[1]<<endl;
}

运行结果:



道理很简单,因为在传入指针到参数后,若参数值被修改,那么指针所指向的内存块的值也相应被修改。而基本类型变量的参数传入则是另外复制一份传值。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  CC++