如何追踪“双重释放或损坏”错误

120

当我运行我的(C++)程序时,它会崩溃并显示以下错误:

 

* glibc detected * ./load: double free or corruption (!prev):   0x0000000000c6ed50 ***

如何跟踪错误?

我尝试使用打印语句 (std::cout),但没有成功。是否可以使用gdb 程序来更轻松地进行调试?


8
我想知道为什么每个人都建议将指针设置为 NULL(这样会掩盖本应被捕获的错误,正如这个问题中所展示的那样),但没有人建议完全避免手动内存管理,而这在 C++ 中是完全可行的。我已经好几年没有写过 delete 了。(是的,我的代码对性能要求很高,否则它就不会用 C++ 编写。) - sbi
2
@sbi:堆损坏等问题很少被捕获,至少不会在它们发生的地方被捕获。将指针置为NULL可能会使您的程序更早崩溃。 - Hasturkun
@Hasturkun:我强烈不同意。将指针设为 NULL 的主要动机是为了防止第二个 delete ptr; 导致程序崩溃 - 这会掩盖一个错误,因为这个第二个 delete 操作本来就不应该发生。(这也用于检查指针是否仍然指向有效对象。但这只会引发一个问题,为什么在作用域内有一个没有对象可以指向的指针。) - sbi
1
我认为下面所有建议手动查找指针问题并将其置空等的答案都是不好的临时措施。学会使用任何真正的工具来查找这些问题是非常宝贵的。在Valgrind的情况下,只需执行apt-get install valgrind命令,并在程序前加上valgrind和一个选项即可(如下面的答案之一所示)。 - BjornW
9个回答

79
如果你正在使用glibc,可以设置环境变量MALLOC_CHECK_2,这将使glibc使用一个容错版本的malloc,并在双重释放发生的点导致程序中止。
你可以在运行程序之前使用

3
设置MALLOC_CHECK_2实际上解决了我的双重释放问题(虽然仅在非调试模式下才可以解决)。 - puk
4
@puk 我也遇到了同样的问题,将MALLOC_CHECK_设置为2可以避免我的双重释放问题。还有哪些选项可以注入尽可能少的代码来重现问题并提供回溯? - Wei Zhong
还可以设置MALLOC_CHECK_来避免这个问题。其他评论者/任何人...你们有没有找到展示这个问题的不同方法? - pellucidcoder
1
当MALLOC_CHECK_设置为非零值时,将使用一种特殊(效率较低)的实现,旨在容忍简单的错误,例如使用相同参数双重调用free或单字节溢出(off-by-one bugs)。因此,MALLOC_CHECK_似乎仅用于避免简单的内存错误,而不是检测它们。 - pellucidcoder
实际上,https://support.microfocus.com/kb/doc.php?id=3113982 看起来将 MALLOC_CHECK_ 设置为 3 是最有用的,并且可以用于检测错误。 - pellucidcoder

38

存在至少两种可能的情况:

  1. 您正在删除相同的实体两次
  2. 您正在删除未分配的内容

对于第一种情况,我强烈建议将所有已删除指针设为 NULL。

您有三个选项:

  1. 重载 new 和 delete 并跟踪分配
  2. 是的,请使用 gdb--然后您将从崩溃中获取回溯,这可能非常有帮助
  3. 如建议的那样,使用 Valgrind--虽然不容易上手,但它将在未来为您节省大量时间...

  1. 会导致破坏,但我认为这个消息一般不会出现,因为健全性检查仅在堆上进行。然而,我认为3. 堆缓冲区溢出是可能的。
- Matthew Flaschen
不错。确实我忘了将指针设为NULL,结果遇到了这个错误。吸取教训! - hrushi

31
你可以使用gdb,但我建议先尝试Valgrind。请参阅快速入门指南
简而言之,Valgrind会对你的程序进行仪器化处理,以便能够检测出在使用动态分配内存时发生的多种错误,例如双重释放和写入超出已分配内存块的末尾(这可能会破坏堆)。它会在错误发生时立即检测并报告错误,从而直接指向问题的原因。

1
@SMR,就这个问题而言,答案的重要部分是整个大型链接页面。因此,仅在答案中包含链接是完全可以的。关于作者为什么更喜欢Valgrind而不是gdb,以及他如何解决_具体_问题的一些话,在我看来确实是答案真正缺少的。 - ndemou
如果用户更喜欢 LLVM 而不是 GNU,他们也可以使用 lldb。 - Nikita Demodov
对我来说,错误在于释放了正确设置(非NULL)并且只释放了一次的指针所分配的内存... Valgrind显示另一个指针分配了不正确的内存量,导致了上述错误信息... - 修复了完全不同指针的错误分配,问题得到解决。非常感谢有用的答案 :-) - tom

25

三个基本规则:

  1. 释放后将指针设置为 NULL
  2. 释放之前检查指针是否为 NULL
  3. 在开始时将指针初始化为 NULL

这三个规则的结合效果非常好。


1
我不是C语言专家,但通常能够应付自如。为什么要使用#1?这是为了让程序在尝试访问已释放指针时彻底崩溃,而不是默默地出现错误吗? - Daniel Harms
1
@精度:是的,这就是重点。这是一个好的实践:拥有指向已删除内存的指针是一种风险。 - ereOn
@ereOn:谢谢。我只上过几节C语言课,从未与他人一起编写代码,所以从未真正遇到过这个问题。不过这似乎是一个非常好的实践。 - Daniel Harms
11
严格来说,我认为#2是不必要的,因为大多数编译器允许您尝试删除一个空指针而不会出现问题。如果我错了,有人肯定会纠正我。 :) - Component 10
12
我认为根据C标准,释放空指针(NULL)应该不会有任何操作。 - Demi
3
@Demetri:是的,你说得对。*"如果 delete 的操作数是空指针,则该操作无效。"(ISO/IEC 14882:2003(E) 5.3.5.2)* - Component 10

19

使用现代C++编译器,您可以使用sanitizer来跟踪。

示例:

我的程序:

$cat d_free.cxx 
#include<iostream>

using namespace std;

int main()

{
   int * i = new int();
   delete i;
   //i = NULL;
   delete i;
}

使用地址检查器进行编译:

# g++-7.1 d_free.cxx -Wall -Werror -fsanitize=address -g

执行:

# ./a.out 
=================================================================
==4836==ERROR: AddressSanitizer: attempting double-free on 0x602000000010 in thread T0:
    #0 0x7f35b2d7b3c8 in operator delete(void*, unsigned long) /media/sf_shared/gcc-7.1.0/libsanitizer/asan/asan_new_delete.cc:140
    #1 0x400b2c in main /media/sf_shared/jkr/cpp/d_free/d_free.cxx:11
    #2 0x7f35b2050c04 in __libc_start_main (/lib64/libc.so.6+0x21c04)
    #3 0x400a08  (/media/sf_shared/jkr/cpp/d_free/a.out+0x400a08)

0x602000000010 is located 0 bytes inside of 4-byte region [0x602000000010,0x602000000014)
freed by thread T0 here:
    #0 0x7f35b2d7b3c8 in operator delete(void*, unsigned long) /media/sf_shared/gcc-7.1.0/libsanitizer/asan/asan_new_delete.cc:140
    #1 0x400b1b in main /media/sf_shared/jkr/cpp/d_free/d_free.cxx:9
    #2 0x7f35b2050c04 in __libc_start_main (/lib64/libc.so.6+0x21c04)

previously allocated by thread T0 here:
    #0 0x7f35b2d7a040 in operator new(unsigned long) /media/sf_shared/gcc-7.1.0/libsanitizer/asan/asan_new_delete.cc:80
    #1 0x400ac9 in main /media/sf_shared/jkr/cpp/d_free/d_free.cxx:8
    #2 0x7f35b2050c04 in __libc_start_main (/lib64/libc.so.6+0x21c04)

SUMMARY: AddressSanitizer: double-free /media/sf_shared/gcc-7.1.0/libsanitizer/asan/asan_new_delete.cc:140 in operator delete(void*, unsigned long)
==4836==ABORTING

要了解更多有关消毒剂的信息,您可以查看这个这个视频,或任何现代C++编译器(例如GCC,Clang等)的文档。


18

您可以使用 valgrind 进行调试。

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

int main()
{
 char *x = malloc(100);
 free(x);
 free(x);
 return 0;
}

[sand@PS-CNTOS-64-S11 testbox]$ vim t1.c
[sand@PS-CNTOS-64-S11 testbox]$ cc -g t1.c -o t1
[sand@PS-CNTOS-64-S11 testbox]$ ./t1
*** glibc detected *** ./t1: double free or corruption (top): 0x00000000058f7010 ***
======= Backtrace: =========
/lib64/libc.so.6[0x3a3127245f]
/lib64/libc.so.6(cfree+0x4b)[0x3a312728bb]
./t1[0x400500]
/lib64/libc.so.6(__libc_start_main+0xf4)[0x3a3121d994]
./t1[0x400429]
======= Memory map: ========
00400000-00401000 r-xp 00000000 68:02 30246184                           /home/sand/testbox/t1
00600000-00601000 rw-p 00000000 68:02 30246184                           /home/sand/testbox/t1
058f7000-05918000 rw-p 058f7000 00:00 0                                  [heap]
3a30e00000-3a30e1c000 r-xp 00000000 68:03 5308733                        /lib64/ld-2.5.so
3a3101b000-3a3101c000 r--p 0001b000 68:03 5308733                        /lib64/ld-2.5.so
3a3101c000-3a3101d000 rw-p 0001c000 68:03 5308733                        /lib64/ld-2.5.so
3a31200000-3a3134e000 r-xp 00000000 68:03 5310248                        /lib64/libc-2.5.so
3a3134e000-3a3154e000 ---p 0014e000 68:03 5310248                        /lib64/libc-2.5.so
3a3154e000-3a31552000 r--p 0014e000 68:03 5310248                        /lib64/libc-2.5.so
3a31552000-3a31553000 rw-p 00152000 68:03 5310248                        /lib64/libc-2.5.so
3a31553000-3a31558000 rw-p 3a31553000 00:00 0
3a41c00000-3a41c0d000 r-xp 00000000 68:03 5310264                        /lib64/libgcc_s-4.1.2-20080825.so.1
3a41c0d000-3a41e0d000 ---p 0000d000 68:03 5310264                        /lib64/libgcc_s-4.1.2-20080825.so.1
3a41e0d000-3a41e0e000 rw-p 0000d000 68:03 5310264                        /lib64/libgcc_s-4.1.2-20080825.so.1
2b1912300000-2b1912302000 rw-p 2b1912300000 00:00 0
2b191231c000-2b191231d000 rw-p 2b191231c000 00:00 0
7ffffe214000-7ffffe229000 rw-p 7ffffffe9000 00:00 0                      [stack]
7ffffe2b0000-7ffffe2b4000 r-xp 7ffffe2b0000 00:00 0                      [vdso]
ffffffffff600000-ffffffffffe00000 ---p 00000000 00:00 0                  [vsyscall]
Aborted
[sand@PS-CNTOS-64-S11 testbox]$


[sand@PS-CNTOS-64-S11 testbox]$ vim t1.c
[sand@PS-CNTOS-64-S11 testbox]$ cc -g t1.c -o t1
[sand@PS-CNTOS-64-S11 testbox]$ valgrind --tool=memcheck ./t1
==20859== Memcheck, a memory error detector
==20859== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al.
==20859== Using Valgrind-3.5.0 and LibVEX; rerun with -h for copyright info
==20859== Command: ./t1
==20859==
==20859== Invalid free() / delete / delete[]
==20859==    at 0x4A05A31: free (vg_replace_malloc.c:325)
==20859==    by 0x4004FF: main (t1.c:8)
==20859==  Address 0x4c26040 is 0 bytes inside a block of size 100 free'd
==20859==    at 0x4A05A31: free (vg_replace_malloc.c:325)
==20859==    by 0x4004F6: main (t1.c:7)
==20859==
==20859==
==20859== HEAP SUMMARY:
==20859==     in use at exit: 0 bytes in 0 blocks
==20859==   total heap usage: 1 allocs, 2 frees, 100 bytes allocated
==20859==
==20859== All heap blocks were freed -- no leaks are possible
==20859==
==20859== For counts of detected and suppressed errors, rerun with: -v
==20859== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 4 from 4)
[sand@PS-CNTOS-64-S11 testbox]$


[sand@PS-CNTOS-64-S11 testbox]$ valgrind --tool=memcheck --leak-check=full ./t1
==20899== Memcheck, a memory error detector
==20899== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al.
==20899== Using Valgrind-3.5.0 and LibVEX; rerun with -h for copyright info
==20899== Command: ./t1
==20899==
==20899== Invalid free() / delete / delete[]
==20899==    at 0x4A05A31: free (vg_replace_malloc.c:325)
==20899==    by 0x4004FF: main (t1.c:8)
==20899==  Address 0x4c26040 is 0 bytes inside a block of size 100 free'd
==20899==    at 0x4A05A31: free (vg_replace_malloc.c:325)
==20899==    by 0x4004F6: main (t1.c:7)
==20899==
==20899==
==20899== HEAP SUMMARY:
==20899==     in use at exit: 0 bytes in 0 blocks
==20899==   total heap usage: 1 allocs, 2 frees, 100 bytes allocated
==20899==
==20899== All heap blocks were freed -- no leaks are possible
==20899==
==20899== For counts of detected and suppressed errors, rerun with: -v
==20899== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 4 from 4)
[sand@PS-CNTOS-64-S11 testbox]$

一个可能的修复方法:

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

int main()
{
 char *x = malloc(100);
 free(x);
 x=NULL;
 free(x);
 return 0;
}

[sand@PS-CNTOS-64-S11 testbox]$ vim t1.c
[sand@PS-CNTOS-64-S11 testbox]$ cc -g t1.c -o t1
[sand@PS-CNTOS-64-S11 testbox]$ ./t1
[sand@PS-CNTOS-64-S11 testbox]$

[sand@PS-CNTOS-64-S11 testbox]$ valgrind --tool=memcheck --leak-check=full ./t1
==20958== Memcheck, a memory error detector
==20958== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al.
==20958== Using Valgrind-3.5.0 and LibVEX; rerun with -h for copyright info
==20958== Command: ./t1
==20958==
==20958==
==20958== HEAP SUMMARY:
==20958==     in use at exit: 0 bytes in 0 blocks
==20958==   total heap usage: 1 allocs, 1 frees, 100 bytes allocated
==20958==
==20958== All heap blocks were freed -- no leaks are possible
==20958==
==20958== For counts of detected and suppressed errors, rerun with: -v
==20958== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 4 from 4)
[sand@PS-CNTOS-64-S11 testbox]$

看看使用 Valgrind 的博客 链接


我的程序运行大约需要30分钟,但在Valgrind上可能需要18到20个小时才能完成。 - Kemin Zhou

5

您是否正在使用像Boost的shared_ptr等智能指针?如果是,请检查是否直接通过调用get()使用了原始指针。我发现这是一个相当常见的问题。

例如,假设将原始指针(可能作为回调处理程序)传递给您的代码。您可能会决定将其分配给智能指针以处理引用计数等。大错特错:除非进行深度复制,否则您的代码不拥有此指针。当您的代码完成智能指针的使用后,它将销毁它并试图销毁它所指向的内存,因为它认为没有其他人需要它,但是调用代码将尝试删除它,并出现双重释放问题。

当然,这可能不是您在这里遇到的问题。这里最简单的例子说明了如何出现这种情况。第一个删除操作没问题,但编译器检测到已经删除了该内存,从而导致问题。那就是为什么在删除后立即将指针赋值为0是一个好主意。

int main(int argc, char* argv[])
{
    char* ptr = new char[20];

    delete[] ptr;
    ptr = 0;  // Comment me out and watch me crash and burn.
    delete[] ptr;
}

编辑:将delete改为delete[],因为ptr是一个char数组。


我的程序中没有使用任何删除命令。这仍然可能是问题吗? - neuromancer
1
@Phenom:为什么你不使用删除(delete)操作?是因为你正在使用智能指针(smart pointers)吗?假设你在代码中使用new关键字来创建堆(heap)上的对象,如果两者的答案都是肯定的,那么你是否在智能指针上使用get / set来复制原始指针(raw pointers)?如果是这样,请不要这样做!因为你会破坏引用计数(reference counting)。或者你可能将从调用的库代码(library code)中分配的指针赋值给了智能指针。如果你没有“拥有”所指向的内存,则不要这样做,因为库和智能指针都会尝试删除它。 - Component 10

0
在我的情况下,我将我的程序链接到了 CUDA 10.0,而我的程序的一个依赖项则链接到了 CUDA 10.2 (cudart.10.2.so)。这种不一致性导致了 "double free or corruption" 错误。
您可以使用 ldd <your program> 命令查看依赖项中是否有多个版本的 CUDA 库。

-4

我知道这是一个非常老的帖子,但它是谷歌搜索此错误的顶部结果,而且没有任何回复提到错误的常见原因。

那就是关闭你已经关闭的文件。

如果你不注意,并且有两个不同的函数关闭同一个文件,那么第二个函数将生成此错误。


1
你是错误的,这个错误是由于双重释放而引发的,正如错误所述。你两次关闭同一个文件导致了双重释放,因为close方法明显试图两次释放相同的数据。 - Geoffrey

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