==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
本文只对用到的几个参数进行详细说明。
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;
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
。
debug + unstripped
有debug信息和无stripped是最理想情况,能详细列出问题函数及第几行。
Invalid write of size 1
at 0x80483BF: really (malloc1.c:20)
by 0x8048370: main (malloc1.c:9)
no debug + unstripped
无stripped但没有debug信息的情况,能指出哪个文件的哪个函数,但需要自行addr2line换算行数。
Invalid write of size 1
at 0x80483BF: really (in /auto/homes/njn25/grind/head5/a.out)
by 0x8048370: main (in /auto/homes/njn25/grind/head5/a.out)
no debug + stripped
除特殊场景之外,这差不多是最差的情况了。只有文件跟虚拟地址。
Invalid write of size 1
at 0x80483BF: (within /auto/homes/njn25/grind/head5/a.out)
by 0x8048370: (within /auto/homes/njn25/grind/head5/a.out)
by 0x42015703: __libc_start_main (in /lib/tls/libc-2.3.2.so)
by 0x80482CC: (within /auto/homes/njn25/grind/head5/a.out)
调试常驻服务
valgrind 只有在进程退出时,才会一次性打印所有的分析结果。
在我的实践中,需要用 valgrind 来统计一个常驻服务的内存泄漏。由于一些代码缺陷,服务退出的逻辑并没有完善好。所以不能正常退出服务。最终导致内存泄漏结果不能正常打印出来。
我的解决方法是,在内存使用将近达到极限时,使用 信号 让进程异常退出。这种情况下,仍可访达 类型的内存泄漏就需要仔细判断是否泄漏了。
千万不要在达到极限后,被内核 oom 来关闭,不然是打印不出任何统计结果的。因为 OOM 使用 KILL 信号杀掉进程,而这个信号是不可捕捉的,valgrind 来不及输出就挂了。