Valgrind没有发现危险的释放记忆

时间:2018-01-13 14:18:05

标签: c++ debugging memory memory-management 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,以及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

2 个答案:

答案 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]

  1. 分配8+3*sizeof(Foo)个字节的内存,让我们称之为指针p。需要8个字节来存储数组中的元素数量。调用delete时,我们需要此号码。
  2. 数组中的对象数保存到p[0]=3
  3. 为内存地址Foo()p+8p+8+sizeof(Foo)调用了新的操作符p+8+2*sizeof(Foo),即创建了3个对象。
  4. ar的地址为p+8,并指向第一个Foo - 对象。
  5. 操纵对象数量*(reinterpret_cast<int*>(ar)-2) = 4

    1. 好的,p[0]现在是4。每个人都认为数组中有4个对象(但实际上只有3
    2. 注意:如果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 - 我们可以从p计算ar。在洞free(p)下方的某个地方被调用 - 没有人关心它拥有多少内存 - 并且使用过的operator delete(*void)没有提供它。
      3. 没有,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;,你可以看到会发生什么:

        1. 仅为第一个delete [] ar; - 对象调用析构函数。
        2. 程序将尝试释放指针Foo而不是指针arr。然而,指针p对于内存管理器是未知的(它只知道ar),这是有问题的。
        3. 正如VTT指出的那样,如果析构函数触及对象中的某些内存,您将看到对数组之外的内存的无效内存访问。

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