引子
人在桌前坐,bug天上来。昨天早上到了小组,正准备总结一下爬山之旅,东哥就给我发了一个bug,让我也帮忙瞅瞅。。。
bug描述
是一个使用Redis跳跃表的demo,可以参照
东哥在RedisDB上的求助贴
东哥在StackOverFlow上的提问
这个关于Redis的demo如下
zskiplistNode* zslGetElementByRank(zskiplist *zsl, unsigned long rank) {
zskiplistNode *x;
unsigned long traversed = 0;
int i;
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
while (x->level[i].forward && (traversed + x->level[i].span) <= rank)
{
traversed += x->level[i].span;
x = x->level[i].forward;
}
if (traversed == rank) {
return x;
}
}
return NULL;
}
int main(int argc, char **argv) {
zskiplistNode *node;
zskiplist *zsl = zslCreate(); //create a skiplist
/*插入操作*/
zslInsert(zsl, 65.5, sdsnew("tom")); //insert some data
zslInsert(zsl, 87.5, sdsnew("jack"));
zslInsert(zsl, 70.0, sdsnew("alice"));
zslInsert(zsl, 95.0, sdsnew("tony"));
node = zslGetElementByRank(zsl, 4); //get element by rank
printf("%s->%f\n", node->ele, node->score); //这里直接coredump了
return 0;
}
抽象出的核心代码如下
//在File func.c
char *func(char *array)
{ char *x;
x = array[n]//这里的意思 n < N 并没有任何数组越界等问题
return x;
}
// 在File main.c
int main(void)
{
char *array = (char *)malloc(sizeof(char) * N);
char *node = func(array);
}
而现在的问题是x的值和node的值并不相同,并且经过我在Linux上的多次实验,发现x和node的低32位相同!
而东哥则表示,若不调用zslGetElementByRank函数,而是直接展开(就是相当于手动inline)则没有问题,同时对于跳跃表的其他函数使用都很正常。。。。
更加诡异的debug
本着debug全靠灵感的思路,我选择将Redis的内存分配器由默认的jemalloc换成了libc的ptmalloc,重新编译之。
$ make MALLOC=libc
结果居然一下子就好了。。。
于是就开始纠结为什么会这样。。。
娄神的最终击杀
最后本着试试看的心态请教了娄神,在结队debug的过程中发现了问题,在make时出现了这样的警告
额。。。。
所以。。。
最终的原因
由于main函数所在的server.c并没有zslGetElementByRank的函数声明,所以编译器认为其返回的是int类型,node和x只有低32位作为一个int类型是正确的。
而用到的其他函数,像zslFirstInRange则已经被声明好,所以可以正确运行。
但是为什么我们将jemalloc换成libc的ptmalloc就好了呢?
经过早上的实验,发现在我的Linux上,对于小对象,ptmalloc返回的地址很低,比如像0x1306010这样的一个地址,而jemalloc返回的地址却很高,像0x7f3c6e215000这样的。而对于大对象,两种malloc都会返回一个大地址。
所以,由于我们的疏忽,导致只有低32位的地址正确,所以ptmalloc这样的低地址就可以跑出正确结果,而jemalloc的高地址就因为前面的高32位丢失而直接coredump了。
后记
这个bug的解决让我一下子就想起了《Effective C++》中的Item 53:不要忽视编译器的警告。 即使是很熟悉的代码,一但有warning,也要仔细看看。在学习的路上,始终都要像那个最开始抱着VC++6.0的我一样,不忽视每个warning。