如何使用Valgrind查找内存泄漏?

369

如何使用valgrind查找程序中的内存泄漏?

我正在使用Ubuntu 10.04,并且有一个名为a.c的程序。


25
使用valgrind测试编译后的程序,而不是源代码。 - Tony
15
下面@RageD给出的答案是正确的,为什么你不接受它呢? - Pratik Singhal
1
泄漏是由于您未能执行的某些操作引起的 - 即释放已分配的内存。因此,Valgrind无法向您显示泄漏的“位置” - 只有您知道不再需要分配的内存在哪里。但是,通过告诉您哪个分配没有被free(),通过跟踪该内存在程序中的使用,您应该能够确定应该在哪里将其free()。常见错误是在不释放已分配的内存的情况下出错退出函数。 - MikeW
2
相关:使用任何工具:https://dev59.com/zm025IYBdhLWcg3wAxF9 - Ciro Santilli OurBigBook.com
4个回答

794

如何运行Valgrind

首先检查您是否已安装Valgrind,如果没有:

sudo apt install valgrind  # Ubuntu, Debian, etc.
sudo yum install valgrind  # RHEL, CentOS, Fedora, etc.
sudo pacman -Syu valgrind  # Arch, Manjaro, Garuda, etc.
sudo pkg ins valgrind      # FreeBSD

Valgrind 可以方便地用于 C/C++ 代码,但是当正确配置时,甚至可以用于其他语言(请参阅this了解 Python 的配置)。
要运行 Valgrind,请将可执行文件作为参数传递(以及任何程序的参数)。
valgrind --leak-check=full \
         --show-leak-kinds=all \
         --track-origins=yes \
         --verbose \
         --log-file=valgrind-out.txt \
         ./executable exampleParam1

这些标志简而言之如下:
  • --leak-check=full:“详细显示每个单独的内存泄漏”
  • --show-leak-kinds=all:在“full”报告中显示所有“definite, indirect, possible, reachable”类型的内存泄漏。
  • --track-origins=yes:优先考虑有用的输出而非速度。此选项跟踪未初始化值的来源,对于内存错误非常有用。如果Valgrind运行过慢,可以考虑关闭此选项。
  • --verbose:可以告诉您程序的异常行为。重复使用以获得更多详细信息。
  • --log-file:将输出写入文件。当输出超出终端空间时非常有用。

最后,您希望看到一个类似以下样式的Valgrind报告:

HEAP SUMMARY:
    in use at exit: 0 bytes in 0 blocks
  total heap usage: 636 allocs, 636 frees, 25,393 bytes allocated
 
All heap blocks were freed -- no leaks are possible
 
ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

我有一个漏洞,但是在哪里

所以,你有一个内存泄漏,而Valgrind没有提供任何有意义的信息。 也许,类似这样的情况:

5 bytes in 1 blocks are definitely lost in loss record 1 of 1
   at 0x4C29BE3: malloc (vg_replace_malloc.c:299)
   by 0x40053E: main (in /home/Peri461/Documents/executable)

让我们也来看一下我写的C代码吧:
#include <stdlib.h>

int main() {
    char* string = malloc(5 * sizeof(char)); //LEAK: not freed!
    return 0;
}

嗯,有5个字节丢失了。怎么会这样?错误报告只是说mainmalloc。在一个更大的程序中,这将会非常麻烦去追踪。这是因为可执行文件是如何编译的。我们实际上可以逐行查看出错的详细信息。重新用调试标志重新编译你的程序(我这里使用的是gcc)。
gcc -o executable -std=c11 -Wall main.c         # suppose it was this at first
gcc -o executable -std=c11 -Wall -ggdb3 main.c  # add -ggdb3 to it

现在有了这个调试版本,Valgrind指向了准确的代码行,分配了泄漏的内存!(措辞很重要:它可能不是你的泄漏位置,但是泄漏的内容。追踪帮助你找到泄漏的位置。)
5 bytes in 1 blocks are definitely lost in loss record 1 of 1
   at 0x4C29BE3: malloc (vg_replace_malloc.c:299)
   by 0x40053E: main (main.c:4)

调试内存泄漏和错误的技巧
  • 充分利用cppreference!它对C和C++函数有很好的文档。还可以考虑www.cplusplus.com

  • 关于内存泄漏的一般建议:

  • 如果可以的话,使用RAII,大部分问题都会迎刃而解。

  • 确保你动态分配的内存确实被释放。

  • 不要分配内存然后忘记给指针赋值。

  • 除非旧的内存已经被释放,否则不要用新的指针覆盖旧的指针。

  • 关于内存错误的一般建议:

  • 访问和写入你确定属于自己的地址和索引。内存错误与泄漏不同,它们通常只是IndexOutOfBoundsException类型的问题。

  • 在释放内存后不要再访问或写入该内存。

  • 有时候你的泄漏/错误可能相互关联,就像一个IDE发现你还没有输入闭合括号一样。解决一个问题可能会解决其他问题,所以寻找一个看起来像罪魁祸首的问题,并应用一些这些想法:

  • 列出你的代码中依赖于/被依赖于“有问题”的代码的函数。跟踪程序的执行(也许甚至在gdb中),寻找前置条件/后置条件错误。重点是追踪程序的执行,同时关注分配内存的生命周期。

  • 尝试注释掉“有问题”的代码块(合理地注释,以便你的代码仍然可以编译)。如果Valgrind错误消失了,那么你就找到了它的位置。

  • 如果所有其他方法都失败了,尝试查阅资料。Valgrind也有文档


常见的泄漏和错误一览

注意你的指针

60 bytes in 1 blocks are definitely lost in loss record 1 of 1
   at 0x4C2BB78: realloc (vg_replace_malloc.c:785)
   by 0x4005E4: resizeArray (main.c:12)
   by 0x40062E: main (main.c:19)

而且代码:

#include <stdlib.h>
#include <stdint.h>

struct _List {
    int32_t* data;
    int32_t length;
};
typedef struct _List List;

List* resizeArray(List* array) {
    int32_t* dPtr = array->data;
    dPtr = realloc(dPtr, 15 * sizeof(int32_t)); //doesn't update array->data
    return array;
}

int main() {
    List* array = calloc(1, sizeof(List));
    array->data = calloc(10, sizeof(int32_t));
    array = resizeArray(array);

    free(array->data);
    free(array);
    return 0;
}

作为助教,我经常看到这个错误。学生使用了一个局部变量,并忘记更新原始指针。这里的错误在于没有意识到 realloc 可以将分配的内存移动到其他位置并更改指针的位置。然后,我们离开了 resizeArray 而没有告诉 array->data 数组被移动到了哪里。

无效写入

1 errors in context 1 of 1:
Invalid write of size 1
   at 0x4005CA: main (main.c:10)
 Address 0x51f905a is 0 bytes after a block of size 26 alloc'd
   at 0x4C2B975: calloc (vg_replace_malloc.c:711)
   by 0x400593: main (main.c:5)

而且代码:

#include <stdlib.h>
#include <stdint.h>

int main() {
    char* alphabet = calloc(26, sizeof(char));

    for(uint8_t i = 0; i < 26; i++) {
        *(alphabet + i) = 'A' + i;
    }
    *(alphabet + 26) = '\0'; //null-terminate the string?

    free(alphabet);
    return 0;
}

请注意,Valgrind指向了上面的代码行。大小为26的数组的索引范围是[0,25],这就是为什么*(alphabet + 26)是一个无效的写入操作——超出了边界。无效的写入是常见的一种因为差一错误而产生的结果。请查看您赋值操作的左侧。

无效读取

1 errors in context 1 of 1:
Invalid read of size 1
   at 0x400602: main (main.c:9)
 Address 0x51f90ba is 0 bytes after a block of size 26 alloc'd
   at 0x4C29BE3: malloc (vg_replace_malloc.c:299)
   by 0x4005E1: main (main.c:6)

而且代码:

#include <stdlib.h>
#include <stdint.h>

int main() {
    char* destination = calloc(27, sizeof(char));
    char* source = malloc(26 * sizeof(char));

    for(uint8_t i = 0; i < 27; i++) {
        *(destination + i) = *(source + i); //Look at the last iteration.
    }

    free(destination);
    free(source);
    return 0;
}

Valgrind指向上面的注释行。在这里看最后一次迭代, 即
*(destination + 26) = *(source + 26);。然而,*(source + 26) 再次越界,类似于无效写入。无效读取也是因为差一错误的常见结果。看一下你的赋值操作的右侧。

开源(非)乌托邦

当泄漏是我的时候,我如何知道?当我使用别人的代码时,我如何找到我的泄漏?我发现了一个不属于我的泄漏,我应该做些什么?这些都是合理的问题。首先,我们来看两个真实世界的例子,展示了两类常见的遭遇。

Jansson:一个JSON库

#include <jansson.h>
#include <stdio.h>

int main() {
    char* string = "{ \"key\": \"value\" }";

    json_error_t error;
    json_t* root = json_loads(string, 0, &error); //obtaining a pointer
    json_t* value = json_object_get(root, "key"); //obtaining a pointer
    printf("\"%s\" is the value field.\n", json_string_value(value)); //use value

    json_decref(value); //Do I free this pointer?
    json_decref(root);  //What about this one? Does the order matter?
    return 0;
}

这是一个简单的程序:它读取一个JSON字符串并解析它。在制作过程中,我们使用库调用来为我们进行解析。Jansson会动态地进行必要的内存分配,因为JSON可以包含自身的嵌套结构。然而,这并不意味着我们需要从每个函数中“释放”给予我们的内存,也不需要对其进行decref操作。实际上,我上面写的这段代码会引发“无效读取”和“无效写入”两个错误。当你删除valuedecref行时,这些错误就会消失。
为什么会这样呢?变量value在Jansson API中被认为是“借用引用”。Jansson会为您跟踪其内存,并且您只需独立地decref每个JSON结构即可。这里的教训是:阅读文档。真的。有时很难理解,但它们告诉您为什么会发生这些事情。相反,我们对这个内存错误有一些现有问题

SDL:一个图形和游戏库

#include "SDL2/SDL.h"

int main(int argc, char* argv[]) {
    if (SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO) != 0) {
        SDL_Log("Unable to initialize SDL: %s", SDL_GetError());
        return 1;
    }

    SDL_Quit();
    return 0;
}

这个代码有什么问题?对我来说,它一直泄漏大约212 KiB的内存。花点时间思考一下。我们打开SDL然后关闭它。答案是什么?没有任何问题。 起初听起来可能很奇怪。事实上,图形处理是混乱的,有时你必须接受一些泄漏作为标准库的一部分。这里的教训是:你不需要消除每一个内存泄漏。有时候你只需要抑制这些泄漏因为它们是已知的无法解决的问题。(这并不意味着你可以忽视自己的泄漏!)

回应虚空

我如何知道泄漏是我的问题?
就是你的问题。(99%确定)

如何在使用别人的代码时找到我的漏洞? 很有可能别人已经找到了。试试谷歌!如果失败了,就用我给你的技巧。如果还是失败了,而且你主要看到的是API调用而不是自己的堆栈跟踪,那就看下一个问题。
我发现了一个不属于我的漏洞,我应该做些什么? 是的!大多数API都有报告错误和问题的方式。利用它们!帮助回馈你在项目中使用的工具!

进一步阅读

感谢您一直陪伴我到现在。希望您已经学到了一些东西,因为我试图照顾到各种不同背景的人们。希望您在这个过程中有以下一些问题:C语言的内存分配器是如何工作的?什么是内存泄漏和内存错误?它们与段错误有什么区别?Valgrind是如何工作的?如果您对其中任何一个问题感兴趣,请点击以下链接满足您的好奇心:


19
更好的答案,可惜这不是被采纳的答案。 - A. Smoliak
4
我可以收藏这个答案并将其用作今后的参考吗?干得好! - Zap
默认情况下是否启用了“memcheck”工具? - abhiarora
@abhiarora:是的,根据Valgrind用户手册:要使用此工具,您可以在Valgrind命令行上指定--tool=memcheck。不过,您并不需要这样做,因为Memcheck是默认工具。 - Steven Lee
Valgrind能否在应用程序仍在运行时报告内存泄漏? - Just a little noob
显示剩余12条评论

172
请尝试以下操作: < p >< code > valgrind --leak-check=full -v ./your_program

只要安装了valgrind,它就会检查程序并告诉您出了什么问题。它可以提供指针和可能存在泄漏的大致位置。如果有段错误,请尝试在 gdb 中运行该程序。

“your_program” 是什么意思? 这是源代码位置还是应用程序名称,例如apk文件? - Bulma
14
“your_program” 指的是可执行文件的名称或者你用来运行应用程序的命令。 - RageD

35

你可以运行:

valgrind --leak-check=full --log-file="logfile.out" -v [your_program(and its arguments)]

14
你可以在 .bashrc 文件中创建别名,方法如下:
alias vg='valgrind --leak-check=full -v --track-origins=yes --log-file=vg_logfile.out'

所以,每当您想检查内存泄漏时,请简单执行以下操作

vg ./<name of your executable> <command line parameters to your executable>

这将在当前目录生成一个Valgrind日志文件。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接