文章

使用 Valgrind 检测内存泄漏

使用 Valgrind 检测内存泄漏

介绍

Valgrind 是一个开源的内存调试、内存泄漏检测和性能分析工具,主要用于 C/C++ 程序 的调试。它在程序运行时对其进行“动态二进制插桩”,可以捕捉各种内存问题。Valgrind 常用于查找:

  • 内存泄漏(memory leaks)
  • 未初始化内存的使用(use of uninitialized memory)
  • 越界访问(buffer overflows/underflows)
  • 重复释放(double free)
  • 未释放内存
  • 非法访问已释放内存

Valgring包含以下常用工具组件

工具名功能
Memcheck默认工具,检测内存错误
Callgrind性能分析,检查函数调用过程中出现的问题
Cachegring分析CPU的cache命中率、丢失率,用于代码优化
Massif堆内存使用分析(heap profiler)
Helgrind多线程数据竞争检测
DRD多线程程序中的同步错误检测

安装

  • 编译安装
1
2
3
4
5
6
7
8
9
10
11
wget https://sourceware.org/pub/valgrind/valgrind-3.25.1.tar.bz2
# 最新版链接参考:https://valgrind.org/downloads/current.html

tar -jxvf valgrind-3.25.1.tar.bz2
# -j表示解压格式为bz2

cd valgrind-3.25.1.tar.bz2

./configure

make && make install

安装完成后可以用valgrind --version检查是否安装成功

使用

memcheck

最常用的工具,用来检测程序中出现的内存问题,所有对内存的读写都会被检测到,一切对mallocfreenewdelete的调用都会被捕获。

使用方法

1
2
3
4
valgrind ./program

# 完整写法,memcheck为默认工具,可以忽略
# valgrind --tool=memcheck

参数选项

选项作用
–tool=memcheck指定工具,默认为memcheck
–leak-check=full详细输出内存泄漏信息
–show-leak-kinds=all显示每一处泄漏的栈信息
–undef-value-errors=no关闭“未定义值使用”警告
–log-file=log将输出保存到文件

在log文件最后会有个summary,其中对内存泄露进行了分类,总共有五类:

(1)definitely lost 意味着你的程序一定存在内存泄露;

(2)indirectly lost意味着你的程序一定存在内存泄露,并且泄露情况和指针结构相关

(3)possibly lost 意味着你的程序一定存在内存泄露,除非你是故意进行着不符合常规的操作,例如将指针指向某个已分配内存块的中间位置。

(4)still reachable 意味着你的程序可能是没问题的,但确实没有释放掉一些本可以释放的内存。这种情况是很常见的,并且通常基于合理的理由。

(5)suppressed 意味着有些泄露信息被压制了。在默认的 suppression 文件中可以看到一些 suppression 相关设置。

Callgrind

Callgrind收集程序运行时的一些数据,函数调用关系等信息,还可以有选择地进行cache模拟。在运行结束时,它会把分析数据写入一个文件。

callgrind_annotate可以把这个文件的内容转化成可读的形式。

Cachegrind

它模拟 CPU中的一级缓存I1,D1和L2二级缓存,能够精确地指出程序中 cache的丢失和命中。如果需要,它还能够为我们提供cache丢失次数,内存引用次数,以及每行代码,每个函数,每个模块,整个程序产生的指令数。这对优化程序有很大的帮助。

Helgrind

它主要用来检查多线程程序中出现的竞争问题。Helgrind寻找内存中被多个线程访问,而又没有一贯加锁的区域,这些区域往往是线程之间失去同步的地方,而且会导致难以发掘的错误。Helgrind实现了名为Erase的竞争检测算法,并做了进一步改进,减少了报告错误的次数。

Massif

堆栈分析器,它能测量程序在堆栈中使用了多少内存,告诉我们堆块,堆管理块和栈的大小。

Massif能帮助我们减少内存的使用,在带有虚拟内存的现代系统中,它还能够加速我们程序的运行,减少程序停留在交换区中的几率。

使用案例

C变量未定义和缺少参数

程序例子如下:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main() {
        int age = 10;
        int height;

        printf("I am %d years old.\n");
        printf("I am %d inches tall.\n", height);

        return 0;
}

编译并检查

1
2
make example
valgrind ./example

有以下的输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
==554832== Memcheck, a memory error detector
==554832== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==554832== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info
==554832== Command: ./ex3
==554832==
I am -16776008 years old.
==554832== Conditional jump or move depends on uninitialised value(s)
==554832==    at 0x48DAAD6: __vfprintf_internal (vfprintf-internal.c:1516)
==554832==    by 0x48C479E: printf (printf.c:33)
==554832==    by 0x109188: main (ex3.c:8)
==554832==
==554832== Use of uninitialised value of size 8
==554832==    at 0x48BE2EB: _itoa_word (_itoa.c:177)
==554832==    by 0x48D9ABD: __vfprintf_internal (vfprintf-internal.c:1516)
==554832==    by 0x48C479E: printf (printf.c:33)
==554832==    by 0x109188: main (ex3.c:8)
==554832==
==554832== Conditional jump or move depends on uninitialised value(s)
==554832==    at 0x48BE2FC: _itoa_word (_itoa.c:177)
==554832==    by 0x48D9ABD: __vfprintf_internal (vfprintf-internal.c:1516)
==554832==    by 0x48C479E: printf (printf.c:33)
==554832==    by 0x109188: main (ex3.c:8)
==554832==
==554832== Conditional jump or move depends on uninitialised value(s)
==554832==    at 0x48DA5C3: __vfprintf_internal (vfprintf-internal.c:1516)
==554832==    by 0x48C479E: printf (printf.c:33)
==554832==    by 0x109188: main (ex3.c:8)
==554832==
==554832== Conditional jump or move depends on uninitialised value(s)
==554832==    at 0x48D9C05: __vfprintf_internal (vfprintf-internal.c:1516)
==554832==    by 0x48C479E: printf (printf.c:33)
==554832==    by 0x109188: main (ex3.c:8)
==554832==
I am 0 inches tall.
==554832==
==554832== HEAP SUMMARY:
==554832==     in use at exit: 0 bytes in 0 blocks
==554832==   total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated
==554832==
==554832== All heap blocks were freed -- no leaks are possible
==554832==
==554832== Use --track-origins=yes to see where uninitialised values come from
==554832== For lists of detected and suppressed errors, rerun with: -s
==554832== ERROR SUMMARY: 5 errors from 5 contexts (suppressed: 0 from 0)

Use of uninitialised value of size 8的意思是大小为8的未初始化的值,表示程序直接使用了未初始化的值进行计算或操作

Conditional jump or move depends on uninitialised value(s),这个错误表示程序的控制流(如if语句、循环、函数调用的参数传递等)依赖于未初始化的值。

C检测内存泄漏

编写一个存在未释放内存的例子:

1
2
3
4
5
6
#include <stdlib.h>
int main()
{
    int *array = malloc(sizeof(int));
    return 0;
}

编译程序,注意编译时要加上-g选项

1
gcc -g -o error error.c

接着使用valgrind去检查程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ valgrind --leak-check=full ./errro

==517874== Memcheck, a memory error detector
==517874== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==517874== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info
==517874== Command: ./errro
==517874==
==517874==
==517874== HEAP SUMMARY:
==517874==     in use at exit: 4 bytes in 1 blocks
==517874==   total heap usage: 1 allocs, 0 frees, 4 bytes allocated
==517874==
==517874== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==517874==    at 0x484880F: malloc (vg_replace_malloc.c:446)
==517874==    by 0x10915E: main (error.c:4)
==517874==
==517874== LEAK SUMMARY:
==517874==    definitely lost: 4 bytes in 1 blocks
==517874==    indirectly lost: 0 bytes in 0 blocks
==517874==      possibly lost: 0 bytes in 0 blocks
==517874==    still reachable: 0 bytes in 0 blocks
==517874==         suppressed: 0 bytes in 0 blocks
==517874==
==517874== For lists of detected and suppressed errors, rerun with: -s
==517874== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

先看看输出信息中的HEAP SUMMARY,它表示程序在堆上分配内存的情况,其中的1 allocs表示程序分配了 1 次内存,0 frees表示程序释放了 0 次内存,4 bytes allocated表示分配了 4 个字节的内存。

另外,Valgrind 也会报告程序是在哪个位置发生内存泄漏。by 0x10915E: main (error.c:4)表示错误发生在error.c的第四行

LEAK SUMMARY则会显示内存泄漏的总体情况

检测越界访问

编写以下程序:

1
2
3
4
5
6
7
8
#include <vector>
#include <iostream>
int main()
{
    std::vector<int> v(10, 0);
    std::cout << v[10] << std::endl;
    return 0;
}

编译并执行分析:

1
2
g++ -g -o main main.cpp
valgrind --tool=memcheck --leak-check=full ./main

会有以下的分析结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
==533948== Memcheck, a memory error detector
==533948== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==533948== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info
==533948== Command: ./main
==533948==
==533948== Invalid read of size 4
==533948==    at 0x1092EE: main (main.cpp:6)
==533948==  Address 0x4dd6ca8 is 0 bytes after a block of size 40 alloc'd
==533948==    at 0x4848F95: operator new(unsigned long) (vg_replace_malloc.c:487)
==533948==    by 0x109B67: __gnu_cxx::new_allocator<int>::allocate(unsigned long, void const*) (new_allocator.h:127)
==533948==    by 0x109A85: std::allocator_traits<std::allocator<int> >::allocate(std::allocator<int>&, unsigned long) (alloc_traits.h:464)
==533948==    by 0x1099B7: std::_Vector_base<int, std::allocator<int> >::_M_allocate(unsigned long) (stl_vector.h:346)
==533948==    by 0x10982A: std::_Vector_base<int, std::allocator<int> >::_M_create_storage(unsigned long) (stl_vector.h:361)
==533948==    by 0x109636: std::_Vector_base<int, std::allocator<int> >::_Vector_base(unsigned long, std::allocator<int> const&) (stl_vector.h:305)
==533948==    by 0x109488: std::vector<int, std::allocator<int> >::vector(unsigned long, int const&, std::allocator<int> const&) (stl_vector.h:524)
==533948==    by 0x1092D0: main (main.cpp:5)
==533948==
0
==533948==
==533948== HEAP SUMMARY:
==533948==     in use at exit: 0 bytes in 0 blocks
==533948==   total heap usage: 3 allocs, 3 frees, 73,768 bytes allocated
==533948==
==533948== All heap blocks were freed -- no leaks are possible
==533948==
==533948== For lists of detected and suppressed errors, rerun with: -s
==533948== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Invalid read of size 4表示越界读取 4 个字节,这个操作出现在main.cpp文件的第 6 行。

另外可以看到,vector分配了一块 40 字节的内存,程序越界访问紧急着这块内存之后的 4 个字节。

检测未初始化的内存

程序如下:

1
2
3
4
5
6
7
8
9
10
#include <iostream>
int main()
{
    int x;
    if (x == 0)
    {
        std::cout << "X is zero" << std::endl;
    }
    return 0;
}

编译后进行分析,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
==534112== Memcheck, a memory error detector
==534112== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==534112== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info
==534112== Command: ./main
==534112==
==534112== Conditional jump or move depends on uninitialised value(s)
==534112==    at 0x1091B9: main (main.cpp:5)
==534112==
X is zero
==534112==
==534112== HEAP SUMMARY:
==534112==     in use at exit: 0 bytes in 0 blocks
==534112==   total heap usage: 2 allocs, 2 frees, 73,728 bytes allocated
==534112==
==534112== All heap blocks were freed -- no leaks are possible
==534112==
==534112== Use --track-origins=yes to see where uninitialised values come from
==534112== For lists of detected and suppressed errors, rerun with: -s
==534112== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

输出中提示了main.cpp文件的第 5 行访问了未初始化的内存

如果使用--undef-value-errors=no选项,会跳过这个错误

本文由作者按照 CC BY 4.0 进行授权