Valgrind没有检测到危险的释放记忆
我正在学习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'd,Julian Seward等人。
== 17649 ==使用Valgrind-3.13.0和LibVEX; 用-h重新运行版权信息
== 17649 ==命令:./a.out -v
== == 17649
Creation Foo
Creation Foo
Creation Foo
删除Foo
删除Foo
删除Foo
删除Foo
== == 17649
== 17649 ==总结:
== 17649 ==在退出时使用:1块中72,704字节
== 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:我编译main.cpp
与命令g++ -g main.cpp
Valgrind没有检测到数组“前缀”的改变,因为它是内存的有效部分。 尽管它不应该被用户代码直接改变,但它仍然可以通过数组构造函数代码访问和修改,并且valgrind不提供如此精细的访问检查分离。 还要注意,这种腐败似乎并没有损坏堆,因此取消分配成功。
Valgrid没有检测到无效对象上的析构函数调用,可能是因为此调用实际上并未访问无效存储。 添加一些类字段将改变情况:
struct Foo
{
int i;
Foo(): i(0) { cout << i << "Creation Foo" << endl;}
~Foo(){ cout << i << "Deletion Foo" << endl;}
};
无效的大小为4的读取
Valgrind没有检测到内存问题,因为没有。
让我们一步一步地浏览你的程序(这是依赖于实现的,但它基本上是如何用于gcc和其他主要编译器的):
调用new Foo[3]
:
8+3*sizeof(Foo)
字节的内存被分配,我们称它为指针p
。 需要8个字节来存储数组中的元素数量。 当调用delete
时,我们将需要这个号码。 p[0]=3
。 p+8
, p+8+sizeof(Foo)
和p+8+2*sizeof(Foo)
调用放置new运算符Foo()
p+8+2*sizeof(Foo)
,即创建3个对象。 ar
的地址为p+8
并指向第一个Foo
。 操作对象数*(reinterpret_cast<int*>(ar)-2) = 4
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
p[0]=4
次,也用于arr[0], arr[1], arr[2]
和arr[3]
。 调用它的arr[3]
是未定义的行为,但没有什么不好的事情发生:调用析构函数不会释放内存(或者甚至在你的情况下触摸它)。 它只打印一些东西 - 没有错。 p
指针被释放而不是ar
因为内存管理器只“知道” p
我们可以从ar
计算p
。 被称为空洞free(p)
地方 - 没有人关心它拥有多少内存 - 并且使用的operator delete(*void)
不提供它。 没有什么,Valgrind的观点是什么问题。
为了使我的观点更清晰(请参阅此处汇编的结果):
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;
你可以看到会发生什么:
Foo
调用析构函数。 arr
而不是指针p
。 然而指针ar
对于内存管理器来说是未知的(它只知道p
),这是有问题的。 正如VTT指出的那样,如果析构函数触及对象中的某些内存,您将看到无效的内存访问超出数组的内存。
如果你的析构函数必须释放一些内存(例如有一个向量作为成员),并因此将随机存储器内容解释为地址并对这些随机地址调用操作符delete
,则会得到错误。