双重释放问题

3
如何编写一个名为free的包装函数来解决双重释放问题,以便我无需更改源代码中的每个free调用?

相关:https://dev59.com/7XRB5IYBdhLWcg3wQVOV - dmckee --- ex-moderator kitten
同样相关的链接:https://dev59.com/GHRB5IYBdhLWcg3wc3A0 - dreamlax
抱歉,我在自吹自擂。 - dreamlax
6个回答

28

不要那样做。

真的,修复实际问题。一旦在指针上调用了free()函数,你的代码就不应该出于任何原因继续持有它。将其置为null,这样你就无法再次释放它;这将使由于过期的指针解引用而引起的任何其他问题变得明显可见。


6
将指针设置为NULL是避免重复释放内存最简单的方法,建议给这种做法点赞。如果想要更安全地实现,在自定义的my_free()函数中可以先调用free()释放内存,然后将指针设为NULL。但需要对代码进行修改。 - Joey
关于包装函数处理指针置空的观点很好。有些语言甚至包含了这些函数,使用它们总是一个好习惯。Delphi对象有一个free()方法,但最好使用FreeAndNil()全局函数。当然,它也不会像free一样在指针已经为null时抛出错误。 - Chris J
9
你认为free(NULL)会抛出一个错误,是什么让你这样想? 根据ANSI标准第4.10.3.2节:“如果ptr是空指针,则不执行任何操作。” - Christoph
“解决实际问题” - 如果你不帮他编写一个突出显示双重释放发生位置的free替代品,你认为他会如何解决这个问题? - Steve Jessop
4
如果您有两份相同的指针副本,那么通过free()或free()的封装程序将其中一份设置为null将不会防止尝试释放第二份副本。 - Jonathan Leffler
@onebyone:编写一个“free替代方案以突出双重释放的位置”不在问题的范围内 - 只需要一个不会引起问题的替代方案。通过精确跟踪指针的分配和释放位置和/或制定一些编程规则/启发式来确定代码的哪些部分负责每个操作,可以实现“修复实际问题”。 - Jason Musgrove

8

不要那样做

一个简单的方法是定义您的包装函数,并使用 #define 将 free 调用您的函数。

#undef free
#define free(x) wrapper_free(x)
/* ... */
int *data = malloc(42);
free(data); /* effectively calls wrapper_free(data) */

但是...不要那样做!


2
在定义之前应该执行 #undef free:C 标准允许 stdlib 函数被额外实现为宏。 - Christoph
@pmg 你能解释一下为什么不行吗? - Gewure
1
@gewure:free()有非常精确的定义。用其他东西替换它长期来看肯定会让你后悔(你会比想象中更快地忘记替换)。特别是如果你在头文件中进行“重新定义”。 - pmg
@pmg 嗯...我已经做了(不是为了stdlib-free,而是为了某些自定义free)-并记录了下来。这难道不是一个合适文档的问题吗?例如,不要将其包装为“free”,而是像“freeAndNull”一样 - 这样很清楚地知道正在发生什么。 - Gewure

7
以下代码拦截对 malloc(), realloc()calloc() 的调用以进行日志记录。
在调用 free() 时,它会检查内存是否先前是由这些函数之一分配的。如果不是,则程序将被终止。释放空指针将被报告,但执行将继续。
头文件 memdebug.h:
#undef free
#define free(PTR) memdebug_free(PTR, #PTR, __FILE__, __func__, __LINE__)

#undef malloc
#define malloc(SIZE) memdebug_log(malloc(SIZE))

#undef realloc
#define realloc(PTR, SIZE) memdebug_log(realloc(PTR, SIZE))

#undef calloc
#define calloc(COUNT, SIZE) memdebug_log(calloc(COUNT, SIZE))

#ifndef MEMDEBUG_H
#define MEMDEBUG_H

extern void memdebug_free(void *ptr, const char *ptr_arg, const char *file, 
    const char *func, int line);
extern void *memdebug_log(void *ptr);

#endif

来源 memdebug.c

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

#ifndef MEMDEBUG_TABLE_SIZE
// log 16k allocations by default
#define MEMDEBUG_TABLE_SIZE 0x4000
#endif

static void *alloc_table[MEMDEBUG_TABLE_SIZE];
static size_t top;

void *memdebug_log(void *ptr)
{
    assert(top < sizeof alloc_table / sizeof *alloc_table);
    alloc_table[top++] = ptr;
    return ptr;
}

void memdebug_free(void *ptr, const char *ptr_arg, const char *file,
    const char *func, int line)
{
    if(!ptr)
    {
        fprintf(stderr,
            "%s() in %s, line %i: freeing null pointer `%s` -->continue\n",
            func, file, line, ptr_arg);

        return;
    }

    for(size_t i = top; i--; )
    {
        if(ptr == alloc_table[i])
        {
            free(ptr);
            --top;
            if(i != top) alloc_table[i] = alloc_table[top];
            return;
        }
    }

    fprintf(stderr,
        "%s() in %s, line %i: freeing invalid pointer `%s`-->exit\n",
        func, file, line, ptr_arg);

    exit(EXIT_FAILURE);
}

+1,在嵌入式环境或其他无法使用Valgrind的情况下应该有效。 - Andrew Y

1

我同意其他帖子的观点,认为你不应该这样做。主要是因为当别人习惯于只看到free函数释放内存时,他们会更加困惑,试图弄清楚代码中发生了什么。

如果你正在寻找一行代码来释放指针,我建议使用宏,虽然还有其他方法可以实现这一点(例如创建一个接受指向指针的指针的方法)。

#define FREEANDCLEAR(pointer)\
{\
   free(pointer);\
   pointer = 0;\
}

编辑 正如Christoph在评论中提到的那样,您还可以通过使用do while来确保用户像使用函数一样使用宏(在行末使用分号):

#define FREEANDCLEAR(pointer)\
do {\
   free(pointer);\
   pointer = 0;\
} while(0)

这将执行一次并需要一个结束分号。


2
不需要检查 if(pointer) - free(NULL) 是有效的且什么也不做;此外,您可能希望将其包装在 do..while() 循环中以获得类似函数的语法。 - Christoph
谢谢Christoph,我去掉了if并添加了第二个实现,使用了do while! - Mark Synowiec

1

除了其他答案和与内存管理开发等相关的参考之外,当双重释放发生时,代码中会出现错误,表明事情没有按正确顺序完成等。

10年前我开始使用Java工作时,每个人都赞扬Java,因为你不必担心new/delete。

我感到困扰(因为我习惯于在C/C++中开发),因为突然间所有打开/关闭、创建/销毁、锁定/解锁模式,在malloc/free之外以许多方式显示在软件中,这些模式可能会被破坏,因为每个人都认为所有清理都是自动的。

因此,当您遇到这些问题时,您很可能会有其他问题,早晚会出现资源占用、数据库连接被消耗、文件在文件系统上被锁定等问题。

双重释放是您的程序结构不好的信号,例如在一层中分配的东西在另一层中被释放。


0

查看stackoverflow问题"编写自己的内存管理器"的答案。

使用这些答案中的工具或技术将让您集成一个内存管理器,该管理器检查此类问题,以便您可以识别并修复它们(正如您问题的许多其他答案所指出的那样,使用此类方法作为解决方法不是一个好主意)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接