Valgrind 未检测到危险的释放内存

Valgrind does not detect dangerous freeing memory

我正在学习 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, a memory error detector

==17649== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.

==17649== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info

==17649== Command: ./a.out -v

==17649==

Creation Foo

Creation Foo

Creation Foo

Deletion Foo

Deletion Foo

Deletion Foo

Deletion Foo

==17649==

==17649== HEAP SUMMARY:

==17649== in use at exit: 72,704 bytes in 1 blocks

==17649== total heap usage: 3 allocs, 2 frees, 73,739 bytes allocated

==17649==

==17649== LEAK SUMMARY:

==17649== definitely lost: 0 bytes in 0 blocks

==17649== indirectly lost: 0 bytes in 0 blocks

==17649== possibly lost: 0 bytes in 0 blocks

==17649== still reachable: 72,704 bytes in 1 blocks

==17649== suppressed: 0 bytes in 0 blocks

==17649== Reachable blocks (those to which a pointer was found) are not shown.

==17649== To see them, rerun with: --leak-check=full --show-leak-kinds=all

==17649==

==17649== For counts of detected and suppressed errors, rerun with: -v

==17649== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

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

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

编译了 main.cpp

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

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

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

Invalid read of size 4

Valgrind 没有检测到内存问题,因为存在 none.

让我们一步一步地检查你的程序(这取决于实现,但它基本上是 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) 调用放置新运算符 Foo(),即创建了 3 个对象。
  4. ar 具有地址 p+8 并指向第一个 Foo-对象。

操作对象数量*(reinterpret_cast<int*>(ar)-2) = 4

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

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

在这种情况下,编译器优化了对析构函数的调用,因为无需执行任何操作。但是这样就不需要记住元素的个数了——所以 p 实际上是 ar 并且开头没有 offset/additional 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 因为内存管理器 "knows" 仅 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;

将导致调用析构函数和运算符删除,这将删除堆上的内存:

    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 对象调用析构函数。
  2. 程序将尝试释放指针 arr 而不是指针 p。然而内存管理器不知道指针 ar(它只知道 p),这是有问题的。

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

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