valgrind 内存泄漏分析
概述
valgrind 官网 https://www.valgrind.org/
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
==10435==
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: -s
==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 principle,
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)
memcheck 工具的支持的错误类型可看官方文档:https://www.valgrind.org/docs/manual/mc-manual.html#mc-manual.errormsgs
本文翻译几个感兴趣的错误类型。
非法读/写内存
例如:
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 工具会检查所有系统调用的参数:
- 参数是否有初始化
- 如果是系统调用读取程序提供的buffer,会产检整个buffer是否可访问和已经初始化
- 如果是系统调用要往用户的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)
不对应的内存申请和释放
检查逻辑如下:
- malloc, calloc, realloc, valloc 或者 memalign 申请的内存,必须用 free 释放。
- new 申请的内存,必须用 delete 释放。
- 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) (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)
源地址和目的地址重叠
这里的检查只包括类似 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 官方用户手册目录:https://www.valgrind.org/docs/manual/manual.html
valgrind QuickStart:https://www.valgrind.org/docs/manual/quick-start.html
执行
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 核心命令行参数:https://www.valgrind.org/docs/manual/manual-core.html#manual-core.basicopts
- valgrind memcheck工具命令行参数:https://www.valgrind.org/docs/manual/mc-manual.html#mc-manual.options
本文只对用到的几个参数进行详细说明。
--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()
{
echo "====== $2 ======"
awk "
BEGIN {
cnt=0
};
/$2/ {
printf \"=== %d ===\\n\", ++cnt;
print \$0;
getline;
while (\$2 != NULL) {
print \$0;
getline;
};
print \"\"
}
END {
printf \"====== $2 Total: %d ======\\n\", cnt;
};
" $1
}
dump_lost valgrind.log "definitely lost" > 0.definitely_lost.log
dump_lost valgrind.log "indirectly lost" > 1.indirectly_lost.log
dump_lost valgrind.log "possibly lost" > 2.possibly_lost.log
dump_lost valgrind.log "still reachable" > 3.still_reachable.log
dump_lost valgrind.log "Invalid read" > 4.invalid_used.log
dump_lost valgrind.log "Invalid write" >> 4.invalid_used.log
dump_lost valgrind.log "Invalid free" >> 4.invalid_used.log
dump_lost valgrind.log "Conditional jump or move depends on uninitialised value" > 5.uninitialised_used.log
dump_lost valgrind.log "Syscall param write(buf) points to uninitialised byte" >> 5.uninitialised_used.log
dump_lost valgrind.log "Source and destination overlap in memcpy" > 6.overlap_used.log
内存泄漏日志解析
这里只讲解使能 --leak-check=full
时打印出来的泄漏细节。
例如:
==3334== 8 bytes in 1 blocks are definitely lost in loss record 1 of 14
==3334== at 0x........: malloc (vg_replace_malloc.c:...)
==3334== by 0x........: mk (leak-tree.c:11)
==3334== by 0x........: main (leak-tree.c:39)
上述日志表示,在进程号 3334 的进程中,发现了8字节的确切泄漏(definitely lost)。泄漏记录的编号并不表示任何东西(我刚开始也是误解为申请顺序),只用于在 gdb 调试时定位泄漏的内存块。
紧跟着标题的,是具体的泄漏调用栈。
valgrind 会合并相同的泄漏,因此这里看到的内存泄漏大小,往往指在统计结束时的总泄漏大小。我们如果加上 -v
选项,则会显示更多细节,例如泄漏出现次数。
其他使用经验
编译参数
为了在出问题时能详细打印出来栈信息,其实我们最好在编译时添加 -g
选项,以及不要 strip
掉符号表。
如果有动态加载的库,需要加上 --keep-debuginfo=yes ,否则如果发现是动态加载的库出现泄漏,由于动态库被卸载了,导致找不到符号表,泄漏细节的调用栈只能是 ???。
代码编译优化,不建议使用 -O2
既以上。-O0
可能会导致运行更慢,建议使用-O1
。
调试常驻服务
valgrind 只有在进程退出时,才会一次性打印所有的分析结果。
在我的实践中,需要用 valgrind 来统计一个常驻服务的内存泄漏。由于一些代码缺陷,服务退出的逻辑并没有完善好。所以不能正常退出服务。最终导致内存泄漏结果不能正常打印出来。
我的解决方法是,在内存使用将近达到极限时,使用 信号 让进程异常退出。这种情况下,仍可访达 类型的内存泄漏就需要仔细判断是否泄漏了。
千万不要在达到极限后,被内核 oom 来关闭,不然是打印不出任何统计结果的。因为 OOM 使用 KILL 信号杀掉进程,而这个信号是不可捕捉的,valgrind 来不及输出就挂了。
valgrind 内存泄漏分析的更多相关文章
- 内存泄漏分析工具tMemMonitor (TMM)使用简介
C/C++由于灵活.高效的优点一直以来都是主流的程序设计语言之一,但是其内存的分配与释放均由程序员自己管理,当由于疏忽或错误造成程序未能释放不再使用的内存时就会造成内存泄漏.在大型.复杂的应用程序中, ...
- Java内存泄漏分析与解决方案
Java内存泄漏是每个Java程序员都会遇到的问题,程序在本地运行一切正常,可是布署到远端就会出现内存无限制的增长,最后系统瘫痪,那么如何最快最好的检测程序的稳定性,防止系统崩盘,作者用自已的亲身经历 ...
- Android内存泄漏分析及调试
尊重原创作者,转载请注明出处: http://blog.csdn.net/gemmem/article/details/13017999 此文承接我的另一篇文章:Android进程的内存管理分析 首先 ...
- Android 内存泄漏分析与解决方法
在分析Android内存泄漏之前,先了解一下JAVA的一些知识 1. JAVA中的对象的创建 使用new指令生成对象时,堆内存将会为此开辟一份空间存放该对象 垃圾回收器回收非存活的对象,并释放对应的内 ...
- Java内存泄漏分析系列之五:常见的Thread Dump日志案例分析
原文地址:http://www.javatang.com 症状及解决方案 下面列出几种常见的症状即对应的解决方案: CPU占用率很高,响应很慢 按照<Java内存泄漏分析系列之一:使用jstac ...
- Java内存泄漏分析系列之二:jstack生成的Thread Dump日志结构解析
原文地址:http://www.javatang.com 一个典型的thread dump文件主要由一下几个部分组成: 上图将JVM上的线程堆栈信息和线程信息做了详细的拆解. 第一部分:Full th ...
- Javascript的内存泄漏分析
作为程序员(更高大尚的称谓:研软件研发)的我们,无论是用Javascript,还是.net, java语言,肯定都遇到过内存泄漏的问题.只不过他们都有GC机制来帮助程序员完成内存回收的事情,如果你是C ...
- Android内存泄漏分析实战
内存泄漏简单介绍 java能够保证当没有引用指向对象的时候,对象会被垃圾回收器回收.与c语言自己申请的内存自己释放相比,java程序猿轻松了非常多.可是并不代表java程序猿不用操心内存泄漏.当jav ...
- (转)Android内存泄漏分析及调试
http://blog.csdn.net/gemmem/article/details/13017999 此文承接我的另一篇文章:Android进程的内存管理分析 首先了解一下dalvik的Ga ...
随机推荐
- 走进docker-初识
什么是Docker容器? 容器是打包代码及其所有依赖项的软件的标准单元,因此应用程序可以从一个计算环境快速可靠地运行到另一个计算环境.Docker容器映像是一个轻量级的,独立的,可执行的软件软件包,其 ...
- Reverse 高校网络信息安全运维挑战赛
Reverse 高校网络信息安全运维挑战赛 1 signed int sub_403CC0() 2 { 3 unsigned int v0; // eax 4 int key_lens; // eax ...
- 在docker容器中使用cplex-python37
技术背景 线性规划是常见的问题求解形式,可以直接跟实际问题进行对接,包括目标函数的建模和各种约束条件的限制等,最后对参数进行各种变更,以找到满足约束条件情况下可以达到的最优解.Cplex是一个由IBM ...
- javascript 取自己
var own=docment.currentScript;
- day-08-文件管理
文件的操作的初识 利用python代码写一个很low的软件,去操作文件. 文件路径:path 打开方式:读,写,追加,读写,写读...... 编码方式:utf-8,gbk ,gb2312...... ...
- 利用查询条件对象,在Asp.net Web API中实现对业务数据的分页查询处理
在Asp.net Web API中,对业务数据的分页查询处理是一个非常常见的接口,我们需要在查询条件对象中,定义好相应业务的查询参数,排序信息,请求记录数和每页大小信息等内容,根据这些查询信息,我们在 ...
- MySQL8开启ssl加密
1 概述 MySQL从5.7开始默认开启SSL加密功能,进入MySQL控制台后输入status可以查看ssl的状态,出现下图表示在使用ssl: 另外,ssl加密需要密钥与证书,可以使用openssl手 ...
- (七)docker-compose 安装
一.Docker-Compose简介 Docker-Compose项目是Docker官方的开源项目,负责实现对Docker容器集群的快速编排. Docker-Compose将所管理的容器分为三层,分别 ...
- Linux 文件系统和目录结构
1. Linux 文件系统 2. linux 目录结构 3. 磁盘分区.文件系统和目录的关系 1. Linux 文件系统 Linux 支持多种的文件系统种类,除了 linux 通常使用的 ext 系列 ...
- 持续集成 简介&环境搭建
1. 持续集成简介 2. 持续集成环境搭建(Jenkins) 2.1 Git 安装 2.2 JDK 1.8 安装 2.3 Tomcat 安装 2.4 Maven 简介和安装 2.5 Jenkins 安 ...