【问题标题】:Valgrind does not detect dangerous freeing memoryValgrind 没有检测到危险的释放内存
【发布时间】:2018-06-22 18:29:21
【问题描述】:

我正在学习 valgrind 框架,我决定在我自己的小测试用例上运行它。这是以下程序,它强制从堆中删除额外的对象(我在 AMD64/LINUX 上运行它):

#include <iostream>
using namespace std;

struct Foo
{
    Foo(){ cout << "Creation Foo" << endl;}
    ~Foo(){ cout << "Deletion Foo" << endl;}
};

int main()
{
    Foo* ar = new Foo[3];
    *(reinterpret_cast<int*>(ar)-2) = 4;
    delete[] ar;
    return 0;
}

但是 valgrind 的执行结果真的让我很困惑:

$ valgrind --leak-check=full ./a.out -v

==17649== Memcheck,内存错误检测器

==17649== 版权所有 (C) 2002-2017 和 GNU GPL,由 Julian Seward 等人撰写。

==17649== 使用 Valgrind-3.13.0 和 LibVEX;使用 -h 重新运行以获取版权信息

==17649== 命令:./a.out -v

==17649==

创世符

创世符

创世符

删除 Foo

删除 Foo

删除 Foo

删除 Foo

==17649==

==17649== 堆摘要:

==17649== 退出时使用:72,704 个字节,1 个块

==17649== 总堆使用量:3 次分配,2 次释放,73,739 字节分配

==17649==

==17649== 泄漏摘要:

==17649== 肯定丢失:0 个块中的 0 个字节

==17649== 间接丢失:0 个块中的 0 个字节

==17649== 可能丢失:0 个块中的 0 个字节

==17649== 仍可访问:1 个块中的 72,704 个字节

==17649== 抑制:0 个块中的 0 个字节

==17649== 可达块(找到指针的那些块)未显示。

==17649== 要查看它们,请重新运行:--leak-check=full --show-leak-kinds=all

==17649==

==17649== 对于检测到和抑制的错误计数,重新运行:-v

==17649== 错误摘要:0 个上下文中的 0 个错误(已抑制:0 个来自 0)

valgrind(版本 3.13.0)似乎没有检测到任何内存损坏?

UPD:我用命令g++ -g main.cpp编译了main.cpp

【问题讨论】:

  • *(reinterpret_cast&lt;int*&gt;(ar)-2) = 4; 是一种潜在的严格别名违规,会导致未定义的行为,并且是在发布模式下优化的潜在目标,会丢弃整行。你是如何编译这段代码的?
  • @VTT,我更新了问题。我知道这是肮脏的 hack - 我只是想隐式更改数组中分配对象的数量

标签: c++ debugging memory memory-management valgrind


【解决方案1】:

Valgrind 可能不会检测到数组“前缀”更改,因为它是内存的有效部分。即使它不应该由用户代码直接更改,它仍然可以由数组构造函数代码访问和修改,并且 valgrind 不提供如此精细的访问检查分离。另请注意,这种损坏似乎不会损坏堆,因此释放成功。

Valgrid 未检测到对无效对象的析构函数调用,可能是因为此调用实际上并未访问无效存储。添加一些类字段会改变这种情况:

struct Foo
{
    int i;
    Foo(): i(0) { cout << i << "Creation Foo" << endl;}
   ~Foo(){ cout << i << "Deletion Foo" << endl;}
};

大小为 4 的读取无效

【讨论】:

  • 您使用了哪个编译命令?我添加了int i; 但什么也没发生
  • @LmTinyToon 你需要在析构函数cout &lt;&lt; i;实际访问它
【解决方案2】:

Valgrind 没有检测到内存问题,因为没有。

让我们一步一步地检查你的程序(这取决于实现,但它基本上是 gcc 和其他主要编译器的工作方式):

致电new Foo[3]

  1. 分配了8+3*sizeof(Foo)字节的内存,我们称之为指针p。需要 8 个字节来存储数组中的元素数量。当delete 被调用时,我们将需要这个号码。
  2. 数组中的对象数量保存到p[0]=3
  3. 为内存地址 p+8p+8+sizeof(Foo)p+8+2*sizeof(Foo) 调用 Placement new-operator Foo(),即创建了 3 个对象。
  4. ar 的地址为p+8,并指向第一个Foo-object。

操纵对象数量*(reinterpret_cast&lt;int*&gt;(ar)-2) = 4

  1. 好的,p[0] 现在是4。每个人都认为数组中有4 对象(但实际上只有3

注意:如果Foo 有一个微不足道的析构函数(例如int 有),情况会有点不同,访问ar-8 将是无效访问。

在这种情况下,编译器优化了析构函数的调用,因为不需要做任何事情。但是接下来就不需要记住元素的数量了——所以p实际上是ar,并且开头没有偏移/额外的8个字节。

这就是为什么大多数编译器实际上是错误代码的原因:

int *array=new int[10];
delete array;//should be delete [] array;

工作没有问题:内存管理器不需要知道指针后面有多少内存,无论它是只有一个 int 还是多个 - 它只是释放内存。

致电delete [] ar

  1. 析构函数被调用p[0]=4 次,同样用于arr[0], arr[1], arr[2]arr[3]。为arr[3] 调用它是未定义的行为,但没有什么不好的事情发生:调用析构函数不会释放内存(甚至在你的情况下触摸它)。它只会打印一些东西——这没什么问题。
  2. 释放阵列内存。实际上p-指针被释放而不是ar,因为内存管理器“知道”只有p——我们可以从ar计算p。在某个地方调用free(p) - 没有人关心它拥有多少内存 - 而使用过的operator delete(*void) 不提供它。

什么都没有,从 Valgrind 的角度来看有什么问题。


为了让我的观点更清楚(参见生成的汇编器here):

Foo f;

将导致仅调用析构函数(无内存访问)但不释放内存 - 这就是在您的程序中为对象 arr[0]arr[1]arr[2]arr[3] 发生的情况

call    Foo::~Foo()

但是

Foo *f=new Foo();
delete f;

会导致调用析构函数和操作符delete,这会删除堆上的内存:

    call    Foo::~Foo()
    movq    %rbp, %rdi
    call    operator delete(void*) ; deletes memory, which was used for f

然而,在您的情况下,并不是每个对象都调用运算符delete,因为内存也不是按位分配的,而是作为整个内存块分配的,即p


如果你打电话给delete ar; 而不是delete [] ar;,你会看到会发生什么:

  1. 仅对第一个Foo-object 调用析构函数。
  2. 程序将尝试释放指针 arr 而不是指针 p。然而,内存管理器不知道指针 ar(它只知道 p),这是有问题的。

正如 VTT 指出的那样,如果析构函数触及对象中的某些内存,您将看到对数组之外内存的无效内存访问。

如果您的析构函数必须释放一些内存(例如,将向量作为成员)并因此将随机内存内容解释为地址并为这些随机地址调用运算符 delete,则会出现错误。

【讨论】:

  • 但据我了解,运算符 delete[] 释放了错误长度的块(长度应小于 1 Foo 大小)。我认为它应该已经导致不正确的结果
  • @LmTinyToon 我们处于未定义行为的领域,所以一切都取决于实现。然而,大部分在 delete 后面有一个简单的 C-free free(void *) 没有大小。可能有内存管理器利用指向的内存大小来加快查找速度,但不确定它们会发生什么
猜你喜欢
  • 2015-01-30
  • 2013-12-23
  • 1970-01-01
  • 2011-03-30
  • 1970-01-01
  • 2013-12-23
相关资源
最近更新 更多