12

我正在维护一个用 C++ 编写的遗留应用程序。它时不时地崩溃,Valgrind 告诉我它对某个对象的双重删除。

在您不完全理解并且太大而无法重写的应用程序中找到导致双重删除的错误的最佳方法是什么?

请分享您的最佳提示和技巧!

4

6 回答 6

4

以下是一些在这种情况下对我有帮助的一般性建议:

  1. 如果您使用的是记录器,请将您的日志记录级别提高到完全调试。在输出中寻找可疑的东西。如果您的应用程序没有记录指针分配和可疑对象/类的删除,那么是时候cout << "class Foo constructed, ptr= " << this << endl;在您的代码中插入一些语句(以及相应的delete/destructor 打印)。
  2. 使用 --db-attach=yes 运行 valgrind。我发现这非常方便,虽然有点乏味。Valgrind 会在每次检测到显着的内存错误或事件时向您显示堆栈跟踪,然后询问您是否要调试它。如果您的应用程序很大,您可能会发现自己多次反复按“n”,但请继续寻找第一次(和第二次)删除相关对象的代码行。
  3. 只需搜索代码。寻找相关对象的构造/删除。可悲的是,有时它最终会出现在第 3 方库中:-(。
  4. 更新:最近才发现:显然 gcc 4.8 及更高版本(如果您可以在系统上使用 GCC)具有一些用于检测内存错误的新内置功能,即“地址清理程序”。也可用于LLVM 编译器系统
于 2012-04-09T18:00:34.360 回答
2

是的。@OliCharlesworth 说了什么。没有万无一失的方法来测试指针以查看它是否指向已分配的内存,因为它实际上只是内存位置本身。

您的问题暗示的最大问题是缺乏可重复性。考虑到这一点,您将无法将简单的“删除”结构更改为delete foo;foo = NULL;.

即使那样,最好的情况也是“它似乎发生得更少”,直到你真正把它压下来。

我还想问 Valgrind 有什么证据表明这是一个双重删除问题。可能是一个更好的线索在那里徘徊。

这是更简单的真正令人讨厌的问题之一。

于 2012-04-09T17:53:17.643 回答
2

这可能适合您,也可能不适合您。

很久以前,我正在开发 1M+ 行的程序,当时只有 15 岁。面临完全相同的问题 - 使用庞大的数据集进行双重删除。有了这样的数据,任何开箱即用的“内存分析器”都是不行的。

我身边的事情:

  1. 这是非常可重现的——我们有宏语言并运行相同的脚本,每次都以完全相同的方式重现它
  2. 在项目历史的某个时候,有人认为“#define malloc my_malloc”和“#define free my_free”有一些用处。这些只是调用内置的 malloc() 和 free() 并没有做更多的事情,但是项目已经编译并以这种方式工作。

现在的技巧/想法:

my_malloc(int size)
{
   static int allocation_num = 0;  // it was single threaded

   void* p = builtin_malloc(size+16);

   *(int*)p = ++allocation_num;
   *((char*)p+sizeof(int)) = 0; // not freed

   return (char*)p+16;  // check for NULL in order here
}

my_free(void* p)
{
    if (*((char*)p+sizeof(int)))
    {
        // this is double free, check allocation_number
        // then rerun app with this in my_alloc
        //    if (alloc_num == XXX) debug_break();
    }

    *((char*)p+sizeof(int)) = 1; // freed

    //built_in_free((char*)p-16);  // do not do this until problem is figured out
}

使用 new/delete 可能会更棘手,但仍然使用 LD_PRELOAD 您可以替换 malloc/free,甚至无需重新编译您的应用程序。

于 2012-04-09T18:54:05.137 回答
0

您可能是从一个处理 delete 的版本与新版本不同的版本升级。

可能以前的版本所做的是在delete被调用时进行静态检查if (X != NULL){ delete X; X = NULL;},然后在新版本中它只是执行delete操作。

您可能需要检查指针分配,并从构造到删除跟踪对象名称的引用。

于 2012-04-09T18:30:38.363 回答
0

我发现这很有用:backtrace() on linux。(您必须使用 -rdynamic 进行编译。)这可以让您通过在所有内存操作(新/删除)周围放置一个 try/catch 块然后在 catch 块中打印出您的堆栈跟踪来找出双重释放的来源。

这样,您可以比运行 valgrind 更快地缩小嫌疑人范围。

我将 backtrace 包装在一个方便的小类中,这样我就可以说:

try {
  ...
} catch (...) {
  StackTrace trace;
  std::cerr << "Double free!!!\n" << trace << std::endl;
  throw;
} 
于 2012-04-09T20:31:45.983 回答
0

在 Windows 上,假设应用程序是使用 MSVC++ 构建的,您可以利用标准库调试版本中内置的大量堆调试工具。

同样在 Windows 上,您可以使用Application Verifier。如果我没记错的话,它有一种模式可以将每个分配强制到一个单独的页面上,中间有受保护的保护页面。它在查找缓冲区溢出方面非常有效,但我怀疑它对于双重释放情况也很有用。

您可以(在任何平台上)做的另一件事是制作已转换的源的副本(可能使用宏),以便每个实例:

delete foo;

替换为:

{ delete foo; foo = nullptr; }

(大括号在许多情况下都有帮助,尽管它并不完美。)这会将许多 double-free 实例转换为空指针引用,从而更容易检测到。它并不能捕获所有内容。您可能有一个过时指针的副本,但它可以帮助压缩许多常见的删除后使用场景。

于 2012-04-10T16:47:29.777 回答