我正在学习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,以及Julian Seward等人的GNU GPL,
== 17649 ==使用Valgrind-3.13.0和LibVEX;用-h重新运行版权信息
== 17649 ==命令:./ a.out -v
== 17649 ==
Creation Foo
Creation Foo
Creation Foo
删除Foo
删除Foo
删除Foo
删除Foo
== 17649 ==
== 17649 == HEAP SUMMARY:
== 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
答案 0 :(得分:3)
Valgrind可能没有检测到数组“prefix”的变化,因为它是内存的有效部分。即使它不应该由用户代码直接更改,它仍然由数组构造函数代码访问和修改,而valgrind不提供这种精细的访问检查分离。另请注意,此损坏似乎不会损坏堆,因此取消分配成功。
Valgrid不会检测无效对象上的析构函数调用,因为此调用实际上不会访问无效存储。添加一些类字段将改变这种情况:
struct Foo
{
int i;
Foo(): i(0) { cout << i << "Creation Foo" << endl;}
~Foo(){ cout << i << "Deletion Foo" << endl;}
};
读取大小4无效
答案 1 :(得分:2)
Valgrind没有检测到内存问题,因为没有。
让我们一步一步地完成您的程序(这取决于实现,但它基本上是如何为gcc和其他主要编译器工作):
致电new Foo[3]
:
8+3*sizeof(Foo)
个字节的内存,让我们称之为指针p
。需要8个字节来存储数组中的元素数量。调用delete
时,我们需要此号码。p[0]=3
。Foo()
,p+8
和p+8+sizeof(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
- 我们可以从p
计算ar
。在洞free(p)
下方的某个地方被调用 - 没有人关心它拥有多少内存 - 并且使用过的operator delete(*void)
没有提供它。没有,Valgrind的观点是什么问题。
让我的观点更清晰(参见结果汇编here):
Foo f;
会导致只调用析构函数(没有内存访问)但不释放内存 - 这就是程序中对象arr[0]
,arr[1]
,arr[2]
和{{ 1}}
arr[3]
但是
call Foo::~Foo()
会导致调用析构函数和运算符delete,这将删除堆上的内存:
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;
- 对象调用析构函数。Foo
而不是指针arr
。然而,指针p
对于内存管理器是未知的(它只知道ar
),这是有问题的。正如VTT指出的那样,如果析构函数触及对象中的某些内存,您将看到对数组之外的内存的无效内存访问。
如果你的析构函数必须释放一些内存(例如将一个向量作为成员)并因此将随机内存内容解释为地址并为这些随机地址调用运算符p
,则会出现错误。