linux下c/c++的内存泄漏分析

使用valgrind进行内存分析

简介

官网地址:http://valgrind.org
主要提供以下工具:

Memcheck 是一个内存错误的检测工具,帮助你的程序,尤其是c/c++程序出现更少内存的问题。
Cachegrind 是一个缓存和分支预测探查工具,帮助你的程序运行的更快。
Callgrind 也是一个和缓存相关的调用图工具,和Cachegrind有一部分重叠,但也生成一些Cachegrind不提供的信息。
Helgrind 是一个线程错误的检测工具,在多线程场景下能派上用场。
DRD 也是一个线程错误的检测工具,与Helgrind功能一样,但是使用了不同的分析技术,可能会发现不同的问题。
Massif 是一个堆分析工具,帮助程序使用更少的内存。
DHAT 是不同与Massif的堆分析工具,帮助你理解块的生命周期, 块的利用率, 以及layout的低效。
SGcheck 是一个实验性的工具,帮助检测栈的超支和全局数组。是Memcheck的功能方面的补充:可以检测出Memcheck无法发现的问题,反之亦然。
BBV 是一个实验SimPoint基本块向量生成器。对于进行计算机体系结构研究和开发的人来说,这是有用的。

这里记录的只是Memcheck的使用,其他的使用可以参考上述的官网的网址。

ubuntu下的安装

sudo apt-get install valgrind

使用方法

valgrind [valgrind-options] your-prog [your-prog-options]

例如,对ls -l进行分析:

valgrind –tool=memcheck ls -l

valgrind的默认工具就是memcheck,所以使用memcheck工具时可以省略–tool参数

注意事项:

1、valgrind工具会减慢程序的运行速度
2、程序编译时需要开启-g,帮助valgrind可以更精准的定位到错误
3、程序编译时最好关闭优化,否则可能产生不正确的未初始化错误信息,以及遗漏未初始化错误。
4、程序编译时最好使用-Wall,帮助valgrind在高优化等级的程序中精准识别一些甚至是全部的问题

具体使用说明

Valgrind会记录下一些注释,文本流,具体的错误报告以及其他的重要的事件。类似与以下的格式:

==12345== some-message-from-Valgrind

12345代表进程Id,这个格式方便区分程序输出和Valgrind的注释输出,以及区分多个进程的输出。Valgrind只会输出最重要的信息,如果需要一些次要的信息,可以使用-v参数。

你可以使用三种方式去导出这些错误

1、默认情况:会直接在控制台打印出来
2、使用文件记录,这个时候需要使用参数–log-file=filename,filename代表存储的文件名
3、通过socket发送:使用参数–log-socket=192.168.0.1:12345,不加端口号会使用默认的1500端口,Valgrind提供了一个叫Valgrind-listener的工具去监听这个网络流。

读懂memcheck工具产生的错误信息

1.非法读/非法写的错误(Illegal read/Illegal write errors)

例如:

Invalid read of size 4
at 0x40F6BBCC: (within /usr/lib/libpng.so.2.1.0.9)
by 0x40F6B804: (within /usr/lib/libpng.so.2.1.0.9)
by 0x40B07FF4: read_png_image(QImageIO *) (kernel/qpngio.cpp:326)
by 0x40AC751B: QImageIO::read() (kernel/qimage.cpp:3621)
Address 0xBFFFF0E0 is not stack’d, malloc’d or free’d

出现这个错误是因为程序读或写了Valgrind认为不应该读写的内存区域

2.使用了为初始化的值

例如:

Conditional jump or move depends on uninitialised value(s)
at 0x402DFA94: _IO_vfprintf (_itoa.h:49)
by 0x402E8476: _IO_printf (printf.c:36)
by 0x8048472: main (tests/manuel1.c:8)
这样一段错误可能就是由以下的代码产生

int main()
{
int x;
printf ("x = %d\n", x);
}

Valgrind会跟踪变量x,直到x被使用时才会报错。在这里x被传入了printf,进而进入_IO_printf,但是这些都不会报错,只有当x被传递到_IO_vfprintf,_IO_vfprintf开始检查x是否可以被转换为ASCII码时才报错。

未初始化值一般有两种情况:

  • 1、局部变量没有被初始化,就像上面一样。
  • 2、The contents of heap blocks (allocated with malloc, new, or a similar function) before you (or a constructor) write something there.

为了找到未初始化变量一开始的位置,可以使用–track-origins=yes参数。当然这会减慢Valgrind的使用速度。

3.在系统调用中使用了未初始化或者不可寻址的值

Valgrind会检查所有系统调用的参数,一般有以下3类:

  • 1、检查所有直接调用的参数,即使已经初始化了。
  • 2、如果系统调用需要你的程序申请的缓冲区,Valgrind会检查所有的缓冲区内容,看它是否可寻址,内容是否初始化了。
  • 3、如果系统调用需要写入用户提供的缓冲,Valgrind会检查是否可寻址。

下面是两个使用了无效参数的系统调用的例子:

#include
#include
int main( void )
{
char* arr = malloc(10);
int* arr2 = malloc(sizeof(int));
write( 1 /* stdout */, arr, 10 );
exit(arr2[0]);
}

得到这样的错误信息:

Syscall param write(buf) points to uninitialised byte(s)
at 0x25A48723: __write_nocancel (in /lib/tls/libc-2.3.3.so)
by 0x259AFAD3: __libc_start_main (in /lib/tls/libc-2.3.3.so)
by 0x8048348: (within /auto/homes/njn25/grind/head4/a.out)
Address 0x25AB8028 is 0 bytes inside a block of size 10 alloc’d
at 0x259852B0: malloc (vg_replace_malloc.c:130)
by 0x80483F1: main (a.c:5)

Syscall param exit(error_code) contains uninitialised byte(s)
at 0x25A21B44: __GI__exit (in /lib/tls/libc-2.3.3.so)
by 0x8048426: main (a.c:8)

write(a)和exit(b)都是错误的,a从堆中向标准输出中写入了未初始化的arr。b向exit传递了为初始化的值。注意a的错误在于arr指向的内存区域,而b的错误直接是arr2[0]。

4.非法的释放(Illegal frees)

例如:

Invalid free()
at 0x4004FFDF: free (vg_clientmalloc.c:577)
by 0x80484C7: main (tests/doublefree.c:10)
Address 0x3807F7B4 is 0 bytes inside a block of size 177 free’d
at 0x4004FFDF: free (vg_clientmalloc.c:577)
by 0x80484C7: main (tests/doublefree.c:10)

这个例子中,一块区域被free了两次,所以出现Illegal frees的错误。

5.使用不合适的释放函数去释放堆区域的内存

例如:

Mismatched free() / delete / delete []
at 0x40043249: free (vg_clientfuncs.c:171)
by 0x4102BB4E: QGArray::~QGArray(void) (tools/qgarray.cpp:149)
by 0x4C261C41: PptDoc::~PptDoc(void) (include/qmemarray.h:60)
by 0x4C261F0E: PptXml::~PptXml(void) (pptxml.cc:44)
Address 0x4BB292A8 is 0 bytes inside a block of size 64 alloc’d
at 0x4004318C: operator new[](unsigned int) (vg_clientfuncs.c:152)
by 0x4C21BC15: KLaola::readSBStream(int) const (klaola.cc:314)
by 0x4C21C155: KLaola::stream(KLaola::OLENode const *) (klaola.cc:416)
by 0x4C21788F: OLEFilter::convert(QCString const &) (olefilter.cc:272)

这个错误是因为使用new[]开辟内存空间,却使用了free去释放内存。
使用malloc, calloc, realloc, valloc or memalign,必须使用free释放内存。
使用new, 必须使用delete释放内存。
使用new[],必须使用delete[]释放内存。

6.源内存区域和目标内存区域重叠

memcpy, strcpy, strncpy, strcat, strncat这些函数可以从源内存区域复制内容到目标内存区域。这两块区域是不可以重叠的。POSIX标准规定这种行为是未定义的。
例如

==27492== Source and destination overlap in memcpy(0xbffff294, 0xbffff280, 21)
==27492== at 0x40026CDC: memcpy (mc_replace_strmem.c:71)
==27492== by 0x804865A: main (overlap.c:40)

7.Fishy argument values

所以的内存分配函数都指定了分配的内存大小,这个大小必定为正数,或者一般情况下不会极度的大。例如在64位的机器上,不会申请分配2^63大小的内存。这种为负数的或者过于大的参数被成为Fishy argument。
例如:

==32233== Argument ‘size’ of function malloc has a fishy (possibly negative) value: -3
==32233== at 0x4C2CFA7: malloc (vg_replace_malloc.c:298)
==32233== by 0x400555: foo (fishy.c:15)
==32233== by 0x400583: main (fishy.c:23)

8.内存泄漏检测

Valgrind会跟踪所有由malloc或new申请的内存,所以当程序退出时,Valgrind知道哪些内存没有被主动释放。
如果–leak-check参数设置得当,对于每一个未被释放的内存块,Valgrind判断从root-set中的指针是否能到达这些内存块。root-set包含(a)普通的所有线程使用的寄存器,(b)初始化的, 对齐的, 指针大小的数据块,包括栈。一个数据块有两种方式可到达,第一种是“start-pointer”,即指针在数据块的开头;第二种是“interior-pointer”,即指针在数据块的中间。“interior-pointer”有多种方式出现:

  • 指针一开始是“start-pointer”,被程序有意或无意的移动到中间。

  • 可能只是巧合。

  • std::string中的char的指针。

  • 有些代码分配块内存,使用前8个去存储作为64位的数。例如sqlite3MemMalloc就是这样做的。

  • 可能是一个c++对象(具有析构函数)数组的指针,由new[]来分配内存。这种情况下,有些编译器存储一个“magic cookie”,包含数组长度存储在分配的块开头。

  • 可能是一个多重继承产生的c++对象的内部部分的指针。

在使用了启发式(heuristics)的情况下,stdstring, length64, newarray and multipleinheritance情况下的“interior-pointer”会被当成“start-pointer”对待。

考虑下面这九种情况:

Pointer chain AAA Leak Case BBB Leak Case


(1) RRR ————> BBB DR
(2) RRR —> AAA —> BBB DR IR
(3) RRR BBB DL
(4) RRR AAA —> BBB DL IL
(5) RRR ——?—–> BBB (y)DR, (n)DL
(6) RRR —> AAA -?-> BBB DR (y)IR, (n)DL
(7) RRR -?-> AAA —> BBB (y)DR, (n)DL (y)IR, (n)IL
(8) RRR -?-> AAA -?-> BBB (y)DR, (n)DL (y,y)IR, (n,y)IL, (_,n)DL
(9) RRR AAA -?-> BBB DL (y)IL, (n)DL

Pointer chain legend:
– RRR: a root set node or DR block(一个root set或者直接可达的块)
– AAA, BBB: heap blocks(堆块)
– —>: a start-pointer (头指针)
– -?->: an interior-pointer (内部指针)

Leak Case legend:
– DR: Directly reachable (直接可达)
– IR: Indirectly reachable (不直接可达)
– DL: Directly lost (直接丢失)
– IL: Indirectly lost (不直接丢失)
– (y)XY: it’s XY if the interior-pointer is a real pointer (内部指针是一个真实的指针)
– (n)XY: it’s XY if the interior-pointer is not a real pointer (内部指针不是一个真实的指针)
– (_)XY: it’s XY in either case (任意一个情况)

任意一种情况都可以被归为上述9种情况之一,Valgrind合并其中一些情况,得出4种可能

  • “Still reachable(依然可达)”。 这包含情况 1 和 2 (for the BBB blocks) 。 一个内存块的头指针的或者头指针的链被发现,程序员至少在原理上释放了这块内存在程序退出之前。这是一个非常普遍并且不算是一个问题,Valgrind默认不报告这个问题。

  • “Definitely lost(绝对丢失)”。 这包含情况3 (for the BBB blocks) 。这意味着这个数据块没有指针可达。数据块被归为丢失,因为程序员在程序结束时不能主动释放它,原因是没有指针指向这块内存。 这可能是在较早之前丢失了指向内存区域的指针。

  • “Indirectly lost(非直接丢失)”。这包含情况4和9 (for the BBB blocks)。这意味着数据块丢失不是因为没有指针指向它,而是因为所有的指向数据块的指针自己丢失了。 举例来说,如果你有一个二叉树,根节点丢失,所有的他的子节点都变成非直接丢失。因为根节点的直接丢失问题被解决,子节点的非直接丢失问就会消失。Valgrind默认不报告这个问题。

  • “Possibly lost(可能丢失)”。 这包含情况5、6、7、8 (for the BBB blocks) 。 这意味着一个或多个数据块指针被发现,但是至少一个指针是内部指针。这可能只是一个内存中的随机值,刚好指向一个数据块,所以你不需要考虑这个情况除非你知道你的代码中出现了内部指针。

下面是一个内存泄漏的总结的例子

LEAK SUMMARY:
definitely lost: 48 bytes in 3 blocks.
indirectly lost: 32 bytes in 2 blocks.
possibly lost: 96 bytes in 6 blocks.
still reachable: 64 bytes in 4 blocks.
suppressed: 0 bytes in 0 blocks.

如果开启的启发式的选项,类似于以下输出

LEAK SUMMARY:
definitely lost: 4 bytes in 1 blocks
indirectly lost: 0 bytes in 0 blocks
possibly lost: 0 bytes in 0 blocks
still reachable: 95 bytes in 6 blocks
of which reachable via heuristic:
stdstring : 56 bytes in 2 blocks
length64 : 16 bytes in 1 blocks
newarray : 7 bytes in 1 blocks
multipleinheritance: 8 bytes in 1 blocks
suppressed: 0 bytes in 0 blocks

如果 –leak-check=full 被指定, Memcheck 会给出每一个绝对丢失或可能丢失块的详细情况,包括他们在哪里被分配。它不能告诉你何时、如何、为何指向一个泄露内存块的指针丢失了;这个需要自己解决。通常,你需要保证在程序退出时,你的程序没有任何的绝对丢失或者可能丢失的内存块。

例如

8 bytes in 1 blocks are definitely lost in loss record 1 of 14
at 0x……..: malloc (vg_replace_malloc.c:…)
by 0x……..: mk (leak-tree.c:11)
by 0x……..: main (leak-tree.c:39)

88 (8 direct, 80 indirect) bytes in 1 blocks are definitely lost in loss record 13 of 14
at 0x……..: malloc (vg_replace_malloc.c:…)
by 0x……..: mk (leak-tree.c:11)
by 0x……..: main (leak-tree.c:25)

第一条信息描述了一种简单的情况,一个8byte的内存块绝对丢失了。第二种情况描述了另外一个8byte内存块绝对丢失;不同在于第二种情况会引起在另外内存块中的更多的80bytes内存非直接丢失了。loss number没有任何特殊的含义。这个loss number可以在Valgrind gdbserver中用来列出泄漏内存块的地址,或者给出更多的信息关于为何一个内存块仍然可达。

当 –leak-check=full 被指定时,选项–show-leak-kinds= 控制显示的泄漏类型。

有下面几种类型:

  • 单独指定一或多个: definite indirect possible reachable。
  • all代表所有。
  • none 代表空集合。

例如使用 –show-leak-kinds=definite,possible 来只显示绝对或者可能的内存丢失。

注意事项

在调试php时,因为php自己实现了内存管理机制,所有使用valgrind时,会检测出很多php上的内存泄露,我们可以通过export USE_ZEND_ALLOC=0来让php直接向内存申请内存,这样有助于发现问题