绝大部分编程语言都提供类似for、while等关键词的循环语句。这可能是我们使用得最频繁的语句之一。但是使用不当,也可能造成死循环,并且某些死循环是非常难以察觉和重现的。比如接下来会说到的情况。 最近在阅读Skynet的源代码(Skynet是由云风编写的一套游戏服务器框架,采用Actor模式),在/lualib-src/lua-bson.c中有如下函数:
static inline void
bson_reserve(struct bson *b, int sz) {
if (b->size + sz <= b->cap)
return;
do {
b->cap *= 2;
} while (b->cap <= b->size + sz);
if (b->ptr == b->buffer) {
b->ptr = malloc(b->cap);
memcpy(b->ptr, b->buffer, b->size);
} else {
b->ptr = realloc(b->ptr, b->cap);
}
}
这个函数的目的是,根据传入的sz增长b指向的内存大小。首先这个函数如果是用作外部接口使用,漏洞非常多。即使仅仅用作内部调用也有很大风险。
最后我们回到文章的主题。看函数中的do{}while()循环语句,大意是当b的内存容量翻倍后,如果b的内存容量仍然小于等于新的内存大小,则b的内存容量继续翻倍,直到b的容量可以满足新的内存大小,则退出循环。但是,这里的do{}while()循环可能会陷入死循环。当b->cap大于INT_MAX/2时,b->cap *= 2;
执行结束后,b->cap整数越界(此时一定会有new b->cap小于old b->cap),则while()中的条件判断一定成立。到此逻辑已经出现错误。更坏的情况是,某些情况下甚至会导致死循环。 接下来,我们构造一个更加简单的例子来说明,循环变量可能会如何引起死循环。写出这样的代码的几率一定不会低。
#include <stdio.h>
#include <stdint.h>
int main() {
uint8_t max_uint8 = (uint8_t)~0;// 255
uint8_t i;
for (i = 0; i < max_uint8; i += 10) {
printf("i:%u\n", i);
}
return 0;
}
你可以在在你的PC上执行此代码,但请注意此程序会陷入死循环。在终端中请使用Ctrl+c强制终止此程序。引起死循环的原因就是整数越界。当i等于250时,i满足for循环的执行条件,可继续执行。执行i += 10
,此时i越界了变成了4,是满足for循环的执行条件的,循环得以继续执行,但已经出错了。 那么我们如何避免因为循环变量越界而导致的错误甚至死循环呢?既然是因为在边界条件判断时出了错,那么我们给其限定执行次数不就行了吗,保证在限定的执行次数内一定是不会发生循环变量越界的,就像strncpy()之于strcpy()。那么这个执行次数应该如何确定呢?我们以上面的死循环代码为例,对其进行改进:
#include <stdio.h>
#include <stdint.h>
int main() {
uint8_t max_uint8 = (uint8_t)~0;// 255
uint8_t i;
uint8_t max_count = max_uint8 / 10;
for (i = 0; i <= max_count; i++) {
printf("i:%u\n", i * 10);
}
return 0;
}
我们先求得原来的for循环在不发生循环变量越界的前提下,最多能循环的次数。这样就修改为了我们最为常见的循环变量自增1的for循环,就不会在出现因循环变量越界而导致错误和死循环了。这个例子非常简单,那如果遇到bson_reserve()函数中乘法递增的循环变量又该如何确定其最大执行次数呢?我们构造一个例子:
#include <stdio.h>
#include <stdint.h>
int main() {
uint8_t max_uint8 = (uint8_t)~0;// 255
uint8_t capacity = 5;
do {
if (max_uint8 / capacity < 2) {
printf("capacity:%u即将越界,请做异常处理\n", capacity);
break;
}
printf("capacity:%u\n", capacity);
capacity *= 2;
} while (capacity <= 200);
return 0;
}
在do{}while()循环中,去检测当前capacity是否能够进行下一次的翻倍,如果不能则报告错误或者做异常处理,当然例子里只是简单的跳出循环而已。其实上个例子同样可以使用这样的判断方式来界定循环变量会否越界,只是我一开始思考的思路是这样的——如何安全地确定其最大执行次数。现在我们将其修改如下:
#include <stdio.h>
#include <stdint.h>
int main() {
uint8_t max_uint8 = (uint8_t)~0;// 255
uint8_t i;
for (i = 0; i < max_uint8; i += 10) {
printf("i:%u\n", i);
// 检测循环变量下一次自增是否能成功
if (max_uint8 - i < 10) {
printf("再次循环将越界,请做异常处理\n");
break;
}
}
return 0;
}
关键在于如何安全地提前判断下一次循环是否会发生越界,如果会就做异常处理,否则继续循环。记住这个陷阱,写出更加健壮的代码。