上一篇博文简单介绍了大整数的表示方法,这次开始介绍一些基本的算法。
★ 初始化和清除
编写大整数函数的出发点是bignum结构的初始化和清除,在其他大部分算法当中,这两个算法都会用到。
对于给定的bignum结构,初始化有两种情况:一是仅仅把bignum结构的dp指向NULL,二是初始化的时候顺便分配一定的动态内存,并让dp指针指向这块内存。其实我本来打算只用第二种方式进行初始化,不过考虑到初始内存可能分配过多导致内存浪费,于是决定两种方式一起使用。第一种方式的优点是在后面编程中你不必考虑到底要预先分配多少内存,内存分配的工作交给bn_grow函数去做,减少了工作量,不过它的缺陷也比较明显,如果一开始的内存分配过少,在后面可能需要多次的内存分配来增加精度,效率降低,要知道堆上面的操作还是挺费时间的。
如果bignum结构中的dp指针已经被初始化(NULL或者是指向某一分配好的内存),就必须对结构的其他成员进行初始化。bignum初始化后默认是0,所以sign = 1, used = 0。如果dp为NULL,alloc = 0,否则alloc = 分配的数位(nlimbs)。
为了避免内存的无限制分配,这里给数位设了一个上限值BN_MAX_LIMB, 在头文件中定义是:
#define BN_MAX_LIMB 25600
也就是说bignum结构的最大数位不能超过25600,在32位环境下,每一个数位长度32bit,所以bignum最大长度是819200bit,对于加密算法来说绰绰有余了。
为了便于移植,有如下宏被定义(参考了PolarSSL的定义,比较好理解):
#define ciL (sizeof(bn_digit)) //chars in limb (每个数位的字节大小)
#define biL (ciL << 3) //bits in limb (每个数位的bit大小)
#define biLH (ciL << 2) //half bits in limb (每个数位的bit大小的一半,这个后面的模运算会用到)
#define BN_MALLOC malloc
#define BN_FREE free
/** * bignum结构初始化,未分配内存。 */ void bn_init(bignum *x) { if(x != NULL) { x->sign = 1; x->alloc = 0; x->used = 0; x->dp = NULL; } } /** * bignum结构初始化并分配内存。 * 若初始化失败(内存分配错误),返回BN_MEMORY_ALLOCATE_FAIL * 若超出数位上限,返回BN_EXCEED_MAX_LIMB */ int bn_init_size(bignum *x, size_t nlimbs) { bn_digit *p; if(nlimbs > BN_MAX_LIMB) return BN_EXCEED_MAX_LIMB; if(x != NULL) { p = (bn_digit *)BN_MALLOC(nlimbs * ciL); if(p == NULL) return BN_MEMORY_ALLOCATE_FAIL; memset(p, 0x00, nlimbs * ciL); //初始化后bignum默认为0,所以必须把p指向的内存的所有值置为0. x->sign = 1; x->alloc = nlimbs; x->used = 0; x->dp = p; } return 0; }
当不再需要使用bignum时,就应该使用bn_free函数及时释放其分配的内存。bn_free函数有两个作用:一是清除bignum的数位和其他成员,这样即使不小心使用了已经清除的bignum也不会出问题,二是释放已经分配的内存。
将已经清除的bignum的dp置为NULL,这样后面再次调用该函数时函数就可以检测到该bignum已经被清除,避免多次内存释放。
void bn_free(bignum *x) { if(x != NULL) { if(x->dp != NULL) { memset(x->dp, 0x00, x->alloc * ciL); BN_FREE(x->dp); } x->sign = 1; x->alloc = 0; x->used = 0; x->dp = NULL; } }
★ 精度增加
当在bignum中存储一个值的时候,必须要有足够多的位数才能无损地存放一个完整的大整数。如果alloc足够大,那么只需要增加used即可,否则就需要重新分配内存增大alloc以满足要求。
bn_grow函数首先检查需要的数位(nlimbs)是否大于alloc,如果不是,那无需增加精度,函数调用结束,这样就避免了内存重新分配,节约了时间。如果nlimbs大于alloc,那么就要重新分配一段长度为nlimbs的内存空间,初始化为0后把原来dp指向的内存中的内容复制到新的空间中,并且释放原先dp指向的内存。
int bn_grow(bignum *x, size_t nlimbs) { bn_digit *p; if(nlimbs > BN_MAX_LIMB) return BN_EXCEED_MAX_LIMB; if(nlimbs > x->alloc) { p = (bn_digit *)BN_MALLOC(nlimbs * ciL); if(p == NULL) return BN_MEMORY_ALLOCATE_FAIL; memset(p, 0, nlimbs * ciL); if(x->dp != NULL) //这里判断bignum结构的dp之前是否分配了内存,如果有分配才进行复制和内存释放操作。 { memcpy(p, x->dp, x->alloc * ciL); memset(x->dp, 0, x->alloc * ciL); BN_FREE(x->dp); } x->dp = p; x->alloc = nlimbs; } return 0; }
★ 复制操作
这个就很简单了,把bignum y复制给bignum x,注意这里的x->dp指向新的内存,而不是指向y->dp。
int bn_copy(bignum *x, const bignum *y) { int ret; size_t nlimbs; if(x == y) return 0; //x == y,表明 x 跟 y 是同一个数,此时无需任何操作,直接返回。注意这里 x 跟 y 都是指针 x->sign = y->sign; x->used = y->used; nlimbs = (y->used == 0) ? 1 : y->used; //如果 y 为0,默认给 x 分配一位的空间 BN_CHECK(bn_grow(x, nlimbs)); memset(x->dp, 0x00, x->alloc * ciL); //初始化内存 if(y->dp != NULL && y->used > 0) memcpy(x->dp, y->dp, y->used * ciL); clean: return ret; }
★ 单精度赋值操作
有时候我们想把bignum设置成一个相对较小的单精度数,比如1, 2^32 - 1等等。 通过分配1个数位的内存,把单精度数赋值给数组的首个单元即可。在这里我默认的单精度数是无符号的(即非负整数),如果想要赋值一个负数,可以直接把sign设为-1即可。
int bn_set_word(bignum *x, const bn_digit word) { int ret; BN_CHECK(bn_grow(x, 1)); //分配一个数位的内存 memset(x->dp, 0, x->alloc * ciL); //初始化 x->dp[0] = word; x->used = (word != 0) ? 1 : 0; x->sign = 1; //如果是负数,在函数调用完后把sign设成-1即可 clean: return ret; }
★ 交换操作
这函数的作用很简单,就是交换两个大整数 x 和 y,具体的实现原理也很简单,交换结构体中的每个成员即可。
void bn_swap(bignum *x, bignum *y) { int tmp_sign; size_t tmp_alloc; size_t tmp_used; bn_digit *tmp_dp; tmp_sign = x->sign; tmp_alloc = x->alloc; tmp_used = x->used; tmp_dp = x->dp; y->sign = x->sign; y->alloc = x->alloc; y->used = x->used; y->dp = x->dp; x->sign = tmp_sign; x->alloc = tmp_alloc; x->used = tmp_used; x->dp = tmp_dp; }
★ 总结
本文介绍的这些算法都是大整数库中的最基本算法,虽然很简单,但它们都是其他算法的基石,后面的算法将会频繁使用到这些算法。下一篇文章将谈谈如何比较两个大整数,包括绝对值比较,有符号大整数比较和大整数与单精度数的比较。