用valgrind查找内存泄漏和非法的内存使用

  • Posted on
  • by
版权信息:原文来自cprogramming,作者:Alexander Allain。 翻译: Vincent

Valgrind是一款基于X86和AMD64第三版本构架的,Linux系统下的多功能的代码概要分析和内存调试工具。它可以模拟程序运行时候的内存使用情况,比如malloc和free调用(C++的是new和delete)。如果你使用未初始化的内存,在数组结尾后写,或者忘记释放指 针,valgrind都可以检测到。由于这些问题非常普遍,本教程将主要集中介绍使用valgrind找到这类简单的内存问题,尽管valgrind是一款可以做很多别的工作的工具。

对于Windows用户,如果你没有Linux的工作环境,或者你想要开发基于Windows系统的软件,你可能会对IBM的Purify感兴趣.Purify是和Valgrind类似的,查找内存泄漏和非法内存访问的工具,有试用版可以下载。

安装Valgrind

Valgrind下载页面。安装过程简单到只要使用bzip2解压缩和解包(以下例子中XYZ表示版本号)。
bzip2 -d valgrind-XYZ.tar.bz2
tar -xf valgrind-XYZ.tar
以上步骤会创建一个叫valgrind-XYZ的目录。进入目录并运行:
./configure
make
make install
现在,你的Valgrind已经安装好了,让我们看看如何使用。

使用Valgrind查找内存泄漏

内存泄漏是最难以检测到的bug,因为这类问题一般要到你耗尽内存时再申请内存才会表现 出来。实际上,当我们使用像C或者C++这类没有内存回收机制的语言的时候,基本上有一半的时间耗在正确的释放内存上。如果你的程序要运行相当长时间来跟 踪相应分支的代码,即使是一个错误的代价也是很高昂的。

当你运行你的代码的时候,你需要指定你所要使用的工具。现在你可以使用 valgrind。本教程中我们将集中介绍内存检查工具,因为内存检查工具可以让我们检查内存是否被正确的使用。如果没有其他参数,Valgrind会列 出free和malloc的调用的概要。请注意,以下的18515是进程号,每次运行都不一样。
% valgrind --tool=memcheck program_name
...
=18515== malloc/free: in use at exit: 0 bytes in 0 blocks.
==18515== malloc/free: 1 allocs, 1 frees, 10 bytes allocated.
==18515== For a detailed leak analysis,  rerun with: --leak-check=yes

如果有内存泄漏,申请和释放的内存数目就会不一样(你不能使用一个free来释放属于多个alloc的内存)。稍后我们将谈到错误摘要,现在,你只要知道,有一些错误是可以忽略的----因为一些错误来自标准的库函数而不是你的代码。

如果申请和释放的内存数目不一样,你就要用泄漏检查选项来检查你的程序。这样才能够列出你的所有没有对应起来的malloc/new等调用。

出于展示的目的,我将使用一个编译成名为"example1"的很简单的程序。
#include <stdlib.h>
int main()
{
    char *x = malloc(100); /* or, in C++, "char *x = new char[100] */
    return 0;
}
% valgrind --tool=memcheck --leak-check=yes example1
运行以上命令后会有出现一些程序运行的信息。最后会有调用了malloc但是没有free的明细:
==2116== 100 bytes in 1 blocks are definitely lost in loss record 1 of 1
==2116==    at 0x1B900DD0: malloc (vg_replace_malloc.c:131)
==2116==    by 0x804840F: main (in /home/cprogram/example1)

这些并没有告诉我们太多信息,尽管我们知道了内存泄漏是在主函数中调用malloc引起的,但是我们并没有相应的行号。这个问题是由于我们在用gcc编译的时候没有使用-g选项引起的。如果我们用带有调试信号的选项重新编译,我们就会得到以下所示的更加有用的输出:
==2330== 100 bytes in 1 blocks are definitely lost in loss record 1 of 1
==2330==    at 0x1B900DD0: malloc (vg_replace_malloc.c:131)
==2330==    by 0x804840F: main (example1.c:5)

现在我们知道发生了内存丢失问题的内存是在哪一行所申请的。尽管还要继续跟踪以确定需要释放内存的位置,但是至少你知道从哪里开始查找。由于每个malloc或new的内存你都要有一个管理的计划,知道内存在那里丢失可以让你知道从哪里开始。

有 时候,--leak-check=yes选项不会显示出所有的内存泄漏。为了找到绝对的没有配对的free或new调用,你需要使用--show- reachable=yes选项。这个选项的输出基本上和--leak-check=yes一样,但是会列出更多的没有释放的内存。

用valgrind查找非法的指针使用

你可以用Valgrind的内存检查工具找到非法的堆内存的使用。比如,你可以用malloc或new申请一个数组,然后试图访问超过数组结尾的内存:
char *x = malloc(10);
x[10] = 'a';

Valgrind能够检查到这个问题。比如将以下代码编译成名为example2的程序,并用valgrind运行。
#include <stdlib.h>

int main()
{
    char *x = malloc(10);
    x[10] = 'a';
    return 0;
}

运行:
valgrind --tool=memcheck --leak-check=yes example2

会有如下警告:
==9814==  Invalid write of size 1
==9814==    at 0x804841E: main (example2.c:6)
==9814==  Address 0x1BA3607A is 0 bytes after a block of size 10 alloc'd
==9814==    at 0x1B900DD0: malloc (vg_replace_malloc.c:131)
==9814==    by 0x804840F: main (example2.c:5)

以 上信息告诉我们,我们使用了一个包含有10个字节的空间,在紧跟着超出了这个数组范围的后面,有一个非法的写。如果我们试图从那块内存中读,我们会被警告 'Invalid read of size X',其中的X表示我们试图去读的内存的大小。(对于一个字符,X是1,对于整型数,X将是2或4,具体依赖于系统)。像平常一样,Valgrind输出 了函数调用的堆栈轨迹,这样我们就能够知道错误发生的确切位置。

检查未初始化变量的使用

Valgrind能够检测到的另外一种操作是条件语句中未初始化值的使用。尽管你应该养成初始化所有你创建了的变量,Valgrind可以帮你找到你没有做到的地方。将以下代码编译成名为example3的可执行程序。
#include <stdio.h>

int main()
{
    int x;
    if(x == 0)
    {
        printf("X is zero"); /* replace with cout and include
                                iostream for C++ */
    }
    return 0;
}

用Valgrind运行得到:
==17943== Conditional jump or move depends on uninitialised value(s)
==17943==    at 0x804840A: main (example3.c:6)

Valgrind甚至聪明到知道一个变量被一个未初始化的变量赋值,被赋值的变量的状态仍然是未初始化的状态。

将以下代码编译成example4:
#include <stdio.h>

int foo(int x)
{
    if(x < 10)
    {
        printf("x is less than 10\n");
    }
}

int main()
{
    int y;
    foo(y);
}

在Valgrind中运行example4会有如下警告:
==4827== Conditional jump or move depends on uninitialised value(s)
==4827==    at 0x8048366: foo (example4.c:5)
==4827==    by 0x8048394: main (example4.c:14)

你也许会认为问题在foo,其余的堆栈调用不是那么重要。但是由于main函数把未初始化的值传递给foo,因此我们必须根据符值关系追溯,直到找到那个未初始化的变量。

你必须测试了相应的分支的相应的条件语句才能够找到错误。因此测试的时候要尽可能包括程序的所有执行路径。

Valgrind还能找到什么

Valgrind还能检测到一些其他不合适的内存使用:如果你对相同的指针释放了两次,Valgrind会提示如下错误:

Invalid free()

Valgrind 还可以检测到不合适的内存释放方法。比如,在C++中有三种释放动态内存的选项:free,delete和delete[]。函数free只能和 malloc调用相对应----在一些系统,你可以不这么做,但是程序的可移植性就不好。此外,delete[]只能和new[]配对(用来申请动态数组)。 尽管有些编译器允许你不这么避开这一点,但是并无法保证所有的都是这样,因为这不是标准。

如果你犯了这些错误,valgrind会提示:
  Mismatched free() / delete / delete []

即使你的程序能够工作,这一类问题也应该及时解决。

Valgrind有什么找不到的错误?

Valgrind不会检查静态数组的边界(分配在栈),所以你必须在函数内部声明:
int main()
{
    char x[10];
    x[11] = 'a';
}

Valgrind不会警告你以上代码的错误!一个出于测试目的的可能的解决方案是用动态申请的内存代替静态数组,这样申请的内存分配在堆,valgrind就会进行边界检查,尽管这可能和未释放内存混淆。

注意事项


Valgrind 有什么缺点?它会消耗更多内存----可能多达程序正常运行时候的两倍。如果你测试的代码需要非常大的内存可能就会有问题。当你测试的时候运行程序的时间也比 较长,大多数时候这不会是个问题,这也只有在你测试的时候会有这个问题。除非你运行的程序本身已经相当慢,这才会成为困扰。

最后,Valgrind不会检查到程序中的每一个错误。如果你没有用很长的字符串进行测试,valgrind就不会提示你缓冲区溢出。Valgrind也不会不能告诉你程序在不被允许的内存写。Valgrind就像其他的工具,需要合适的使用才能够发现问题。

小结

Valgrind 是X86和AMD64构架下,运行于Linux的一个内存检查工具。它允许程序员在它所构造的环境中运行程序以检查没有配对的内存申请和其他非法内存使用 (比如未初始化内存),或是非法的内存操作(比如多次释放同一块内存或非法的内存释放命令)。Valgrind不会检查静态数组的边界。

后话:Valgrind的一些有用的参考资料。
Valgrind 使用简单说明
Linux on Power 上的调试工具和技术