背景

自己在工作中,遇到过一次double free的问题,在申请了一段堆内存之后,经过复杂的业务逻辑,有两个指针指向了同一块内存,当我对两个指针都调用free方法的时候,错误就发生了,我把这个错误进行了简化,并把代码放在下面:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int* p=(int*)malloc(sizeof(int));
    char* q=(char*)p;

    free(p);
    free(q);

    return 0;
}

很明显,调用了2次free方法,释放同一块内存,我们可以看到程序crash了,并且系统提示:
free(): double free detected in tcache 2
Aborted (core dumped)
这个例子是比较简单的,但是在实际的工程中,成千万行代码的复杂逻辑中,我们往往很难判断多个指针是否指向相同的地址。

思路

如果我们在所有调用free的地方增加log,把要释放的指针给记录下来,就会比较有助于分析和定位问题。的确有这种手段,那就是Linux 的 preload机制。我们知道Loader的原理是:对于未定义的引用,动态连接器要先进行解析,它会搜索LD_PRELOAD目录下的动态库,然后再搜索其他的库,所以我们就有办法对malloc 和 free 函数进行替换。比如自己提供free的实现:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

void free(void* ptr)
{
    void(*freep)() = NULL;

    printf("ready to do free: %p\n",ptr);
    freep = dlsym(RTLD_NEXT,"free");
    freep(ptr);
    printf("free done: %p\n",ptr);
}

int main()
{
    int* p=(int*)malloc(sizeof(int));
    char* q=(char*)p;

    free(p);
    free(q);

    return 0;
}

来分析一下上面的代码:
1.第一行的 #define _GNU_SOURCE 是一定要添加的,因为后面的 RTLD_NEXT宏依赖这个宏。
2.定义了一个函数指针freep 指向真正的free的实现。然后分别在free前后加log打印内存地址。
3.在通过dlsym打开glic中的free方法,dlsym的作用是通过符号名找到符号对应的地址。
4.需要注意的是,使用RTLD_NEXT,就是告诉ld-linux.so 不要在当前文件中free这个符号,而是按照动态库的搜索顺序找到下一个动态库,并在它里面寻找free函数,实际上,这里找到的就是 glibc 里的free函数了。
5.实际上,我们自己定义的free函数,只不过是glibc里的free方法的一个包装。

gcc -shared -fpic -o myfree.so main.c -ldl
 LD_PRELOAD=“./myfree.so” ./main.outready to do free: 0x55f5d0b6a2a0
 free done: 0x55f5d0b6a2a0
 ready to do free: 0x55f5d0b6a2a0
 free(): double free detected in tcache 2
 Aborted (core dumped)

通过这种方式,我们就重载了free方法。也可以自己实现一个malloc方法。