valgrind内存泄漏分析
概述
valgrind 是 Linux 业界主流且⾮常强⼤的内存泄漏检查⼯具。在其官⽹介绍中,内存检查(memcheck)只是其其中⼀个功能。由于只⽤过其内存泄漏的检查,就不拓展分享 valgrind 其他功能了。
valgrind 这个⼯具不能⽤于调试正在运⾏的程序,因为待分析的程序必须在它特定的环境中运⾏,它才能分析内存。
内存泄漏分类
valgrind 将内存泄漏分为 4 类。
明确泄漏(definitely lost):内存还没释放,但已经没有指针指向内存,内存已经不可访问
间接泄漏(indirectly lost):泄漏的内存指针保存在明确泄漏的内存中,随着明确泄漏的内存不可访问,导致间接泄漏的内存也不可访问
可能泄漏(possibly lost):指针并不指向内存头地址,⽽是指向内存内部的位置
仍可访达(still reachable):指针⼀直存在且指向内存头部,直⾄程序退出时内存还没释放。
明确泄漏
官⽅⽤户⼿册描述如下:
This means that no pointer to the block can be found. The block is classified as "lost",
because the programmer could not possibly have freed it at program exit, since no pointer to it exists.
This is likely a symptom of having lost the pointer at some earlier point in the
program. Such cases should be fixed by the programmer.
其实简单来说,就是内存没释放,但已经没有任何指针指向这⽚内存,内存地址已经丢失。定义⽐较好理解,就不举例了。
valgrind 检查到明确泄漏时,会打印类似下⾯这样的⽇志:
==19182== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==19182== at 0x1B8FF5CD: malloc (vg_replace_malloc.c:130)
==19182== by 0x8048385: f (a.c:5)
==19182== by 0x80483AB: main (a.c:11)
明确泄漏的内存是强烈建议修复的,这没啥好争辩的。
间接泄漏
官⽅⽤户⼿册描述如下:
This means that no pointer to the block can
be found. The block is classified as "lost", because the programmer could not possibly have freed it at program
exit, since no pointer to it exists. This is likely a symptom of having lost the pointer at some earlier point in the
program. Such cases should be fixed by the programmer.
间接泄漏就是指针并不直接丢失,但保存指针的内存地址丢失了。⽐较拗⼝,咱们看个例⼦:
struct list {
struct list *next;
};
int main(int argc, char **argv)
{
struct list *root;
root = (struct list *)malloc(sizeof(struct list));
root->next = (struct list *)malloc(sizeof(struct list));
printf("root %p roop->next %p\n", root, root->next);
root = NULL;
return 0;
}
丢失的是root指针,导致root存储的next指针成为了间接泄漏。
valgrind 检查会打印如下⽇志:
# valgrind --tool=memcheck --leak-check=full --show-reachable=yes /data/demo-c
==10435== Memcheck, a memory error detector
==10435== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==10435== Using Valgrind-3.17.0 and LibVEX; rerun with -h for copyright info
==10435== Command: /data/demo-c
root 0x4a33040 roop->next 0x4a33090
==10435==
==10435== HEAP SUMMARY:
==10435== in use at exit: 16 bytes in 2 blocks
==10435== total heap usage: 3 allocs, 1 frees, 1,040 bytes allocated
==10435==
==10435== 8 bytes in 1 blocks are indirectly lost in loss record 1 of 2
==10435== at 0x4845084: malloc (vg_replace_malloc.c:380)
==10435== by 0x4007BF: main (in /data/demo-c)
==10435==
==10435== 16 (8 direct, 8 indirect) bytes in 1 blocks are definitely lost in loss record 2 of 2
==10435== at 0x4845084: malloc (vg_replace_malloc.c:380)
==10435== by 0x4007B3: main (in /data/demo-c)
==10435==
==10435== LEAK SUMMARY:
==10435== definitely lost: 8 bytes in 1 blocks
==10435== indirectly lost: 8 bytes in 1 blocks
==10435== possibly lost: 0 bytes in 0 blocks
==10435== still reachable: 0 bytes in 0 blocks
==10435== suppressed: 0 bytes in 0 blocks
==10435==
==10435== For lists of detected and suppressed errors, rerun with: -sdelete in
==10435== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
默认情况下,只会打印明确泄漏和可能泄漏,如果需要同时打印间接泄漏,需要加上选项 --show-reachable=yes.
间接泄漏的内存肯定也要修复的,不过⼀般会随着明确泄漏的修复⽽修复
可能泄漏
官⽅⽤户⼿册描述如下:
This means that a chain of one or more pointers to the block has been found, but at least one
of the pointers is an interior-pointer. This could just be a
random value in memory that happens to point into a block, and so you shouldn't consider this ok unless you
know you have interior-pointers.
valgrind 之所以会怀疑可能泄漏,是因为指针已经偏移,并没有指向内存头,⽽是有内存偏移,指向内存内部的位置。
有些时候,这并不是泄漏,因为这些程序就是这么设计的,例如为了实现内存对齐,额外申请内存,返回对齐后的内存地址。但更多时候,是我们不⼩⼼p++了。
可能泄漏的情况需要我们根据代码情况⾃⼰分析确认
仍可访达
官⽅⽤户⼿册描述如下:
This covers cases 1 and 2 (for the BBB blocks) above. A start-pointer or chain of start-pointers
to the block is found. Since the block is still pointed at, the programmer could, at least in princi p le,
have freed it before program exit. "Still reachable" blocks are very common and arguably not a problem.
So, by default, Memcheck won't report such blocks individually.
仍可访达表⽰在程序退出时,不管是正常退出还是异常退出,内存申请了没释放,都属于仍可访达的泄漏类型。
如果测试的程序是正常退出的,那么这些仍可访达的内存就是泄漏,最好修复了。
如果测试是长期运⾏的程序,通过信号提前终⽌,那么这些内存就⼤概率并不是泄漏。
其他的内存错误使⽤
即使是 memcheck ⼀个⼯具,除了检查内存泄漏之外,还⽀持其他内存错误使⽤的检查。
⾮法读/写内存(Illegal read / Illegal write errors)
使⽤未初始化的变量(Use of uninitialised values)
系统调⽤传递不可访问或未初始化内存(Use of uninitialised or unaddressable values in system calls)
⾮法释放(Illegal frees)
不对应的内存申请和释放(When a heap block is freed with an inappropriate deallocation function)
源地址和⽬的地址重叠(Overlapping source and destination blocks)
内存申请可疑⼤⼩(Fishy argument values)
本⽂翻译⼏个感兴趣的错误类型。
⾮法读/写内存
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
在你要操作的内存超出边界或者⾮法地址时,就会有这个错误提⽰。常见的错误,例如访问数组边界:
int arr[4];
arr[4] = 10;
例如使⽤已经释放了的内存:
char *p = malloc(30);
...
free(p);
...
p[1] = '\0';
如果发现这样的错误,最好也修复了。因为这些错误⼤概率会导致段错误
使⽤未初始化的变量
尤其出现在局部变量未赋值,却直接读取的情况。也包括申请了内存,没有赋值却直接读取,虽然这情况会读出 '\0',不会导致异常,但更多时候是异常逻辑。
例如:
int main()
{
int x;
printf ("x = %d\n", x);
}
如果要详细列出哪⾥申请的内存未初始化,需要使⽤参数--track-origins=yes,但也会让慢很多。
错误显⽰是这样的:
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)
系统调⽤传递不可访问或未初始化内存
memcheck ⼯具会检查所有系统调⽤的参数:
1. 参数是否有初始化
2. 如果是系统调⽤读取程序提供的buffer,会产检整个buffer是否可访问和已经初始化
3. 如果是系统调⽤要往⽤户的buffer写⼊数据,会检查buffer是否可访问
错误显⽰是这样的:
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)
不对应的内存申请和释放
检查逻辑如下:
1. malloc, calloc, realloc, valloc 或者 memalign 申请的内存,必须⽤ free 释放。
2. new 申请的内存,必须⽤ delete 释放。
3. new[] 申请的内存,必须⽤ delete[] 释放。
错误显⽰是这样的:
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) (: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 (:314)
by 0x4C21C155: KLaola::stream(KLaola::OLENode const *) (:416)
by 0x4C21788F: OLEFilter::convert(QCString const &) (:272)
源地址和⽬的地址重叠
这⾥的检查只包括类似 memcpy, strcpy, strncpy, strcat, strncat 这样的有源地址和⽬的地址操作的C库函数,确保源地址和⽬的地址指针不会重叠。
错误显⽰是这样的:
==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)
内存申请可疑⼤⼩
这个问题往往出现在申请的内存⼤⼩是负数。因为申请⼤⼩往往是⾮负数和不会⼤的很夸张,但如果传递了个负数,直接导致申请⼤⼩解析为⼀个⾮常⼤的正数。
错误显⽰是这样的:
==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)
如何使⽤
执⾏
valgrind 的执⾏命令如下:
valgrind [valgrind_optons] myprog [myprog_arg1 ...]
例如:
valgrind --leak-check=full ls -al
使⽤valgrind做内存检查,程序的执⾏效率会⽐平常慢⼤约20~30倍,以及⽤更多的内存。在我的测试中,平时60M的物理内存,加上valgrind之后,直接飙升到200+M,⽽且是随着记录的增多⽽内存骤增。
valgrind 会在收到到 1000 个不同的错误,或者共计 10,000,000 个错误时⾃动停⽌继续收集错误信息。
此外,不建议直接通过 valgrind 来运⾏脚本,否则只会得到 shell 或者其他的解释器相关的错误报告。我们可以通过提供选项 --trace-children=yes 来强制解决这个问题,但是仍然有可能出现混淆。
valgrind 只有在进程退出时,才会⼀次性打印所有的分析结果。
参数
valgrind 有⾮常多的参数,可以⾃⾏通过 valgrind --help 查看⼤致说明,也可以翻阅下⾯常⽤的⽂档链接:
valgrind 核⼼命令⾏参数:
valgrind memcheck⼯具命令⾏参数:
本⽂只对⽤到的⼏个参数进⾏详细说明。
--tool=<toolname> [default: memcheck]
valgrind⽀持不少检查⼯具,都有各种功能。但⽤的更多的还是他的内存检查(memcheck)。--tool= ⽤于选择你需要执⾏的⼯具,如果不指明则默认为 memcheck。
--log-file=<filename> And --log-fd=<number> [default: 2, stderr]
valgrind 打印⽇志转存到指定⽂件或者⽂件描述符。如果没有这个参数,valgrind 的⽇志会连同⽤户程序的⽇志⼀起输出,对于⼤多数使⽤者来说,会显得⾮常乱。
Note: valgrind的⽇志输出格式⾮常有规律,我也写了个脚本来根据错误类型从混合⽇志中过滤,后⽂提供
把⽇志输出到⽂件的话,还⽀持⼀些特殊动态变量,可以实现按进程ID或者序号保存到不同⽂件。我之
前没留意到有这个功能,结果发现不同进程写⼊到同⼀个⽂件,后⾯写⼊的检查结果把其他进程的检查结果覆盖了。以下是输出到⽂件⽀持的⼀些动态变量:
%n:会重置为⼀个进程唯⼀的⽂件序列号
%p:表⽰当前进程的 ID 。多进程时且使能了trace-children=yes跟踪⼦进程时会⾮常实⽤
%q{FOO}:实⽤环境变量 FOO 的值。适⽤于那种不同进程会设置不同变量的情况。
%%:转意成⼀个百分号。
如果使⽤其他还不⽀持的百分号字符,会导致 abort。
valgrind 还⽀持把错误⽇志重定向到 socket 中,由于没⽤过,就不展开了。
--leak-check=<no|summary|yes|full> [default: summary]
这个参数决定了输出泄漏结果时,输出的是结果内容。no没有输出,summary只输出统计的结果,yes和full输出详细内容。
常见的使⽤是:--leak-check=full
--show-leak-kinds=<set> [default: definite,possible]
valgrind 有4种泄漏类型,这个参数决定显⽰哪些类型泄漏。definite indirect possible reachable 这4种可以设置多个,以逗号相隔,也可以⽤all表⽰全部类型,none表⽰啥都不显⽰。
⼤多数情况,我们直接⽤--show-reachable=yes⽽不是--show-leak-kinds=...,见下⽂。
--show-reachable=<yes | no> , --show-possibly-lost=<yes | no>
--show-reachable=no --show-possibly-lost=yes 等效于 --show-leak-kinds=definite,possible。
--show-reachable=no --show-possibly-lost=no 等效于 --show-leak-kinds=definite。
--show-reachable=yes 等效于 --show-leak-kinds=all。
需要注意的是,在使能--show-reachable=yes时,--show-possibly-lost=no会⽆效。
常见的,这个参数这么使⽤:--show-reachable=yes
--trace-children=<yes | no> [default: no]
是否跟踪⼦进程?看⾃⼰需求,如果是多进程的程序,则建议使⽤这个功能。不过单进程使能了也不会有多⼤影响。
--keep-stacktraces=alloc | free | alloc-and-free | alloc-then-free | none [default: alloc-and-free]
内存泄漏不外乎申请和释放不配对,函数调⽤栈是只在申请时记录,还是在申请释放时都记录,还是其他?如果我们只关注内存泄漏,其实完全没必要申请释放都记录,因为这会占⽤⾮常多的额外内存和更多的 CPU 损耗,让本来就执⾏慢的程序雪上加霜。
因此,建议这么使⽤:--keep-stacktraces=alloc
--track-fds=<yes | no | all> [default: no]
是否跟踪⽂件打开和关闭?很多时候,⽂件打开后没关闭也是⼀个明显的泄漏。
--track-origins=<yes | no> [default: no]
对使⽤⾮初始化的变量的异常,是否跟踪其来源。
在确定要分析使⽤未初始化内存错误时使能即可,平时使能这个会导致程序执⾏⾮常慢。
-
-keep-debuginfo=<yes | no> [default: no]
如果程序有使⽤动态加载库(dlopen),在动态库卸载时(dlclose),debug信息都会被清除。使能这个选项后,即使动态库被卸载,也会保留调⽤栈信息。
⽇志过滤脚本
实践中发现,错误类型⼀⼤堆,错误⽇志更多。⼈⼯⼀个个分类检查太慢了,于是⼲脆写了个脚本来⾃动过滤:
#!/bin/bash
# dump_lost <log_file> <key words>
dump_lost()
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论