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 0x1B8FF4CD: 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 c04a33040 roop->next c04a33090
==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 c04845084: malloc (vg_replace_malloc.c:380)
==10435==    by c04007BF: 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 c04845084: malloc (vg_replace_malloc.c:380)
==10435==    by c04007B3: 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)
  • 服务器ip和目地详细地址重合(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 c040F6BBCC: (within /usr/lib/libpng.so.2.1.0.9)
   by c040F6B804: (within /usr/lib/libpng.so.2.1.0.9)
   by c040B07FF4: read_png_image(QImageIO *) (kernel/qpngio.cpp:326)
   by c040AC751B: QImageIO::read() (kernel/qimage.cpp:3621)
 Address c0BFFFF0E0 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 c0403DFA94: _IO_vfprintf (_itoa.h:49)
   by c0402E8476: _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 c025A48723: __write_nocancel (in /lib/tls/libc-2.3.3.so)
     by c0259AFAD3: __libc_start_main (in /lib/tls/libc-2.3.3.so)
     by 0x8048348: (within /auto/homes/njn25/grind/head4/a.out)
   Address c025AB8028 is 0 bytes inside a block of size 10 alloc'd
     at c0259852B0: malloc (vg_replace_malloc.c:130)
     by 0x80483F1: main (a.c:5)

  Syscall param exit(error_code) contains uninitialised byte(s)
     at c025A21B44: __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 c040043249: free (vg_clientfuncs.c:171)
   by c04102BB4E: QGArray::~QGArray(void) (tools/qgarray.cpp:149)
   by c05C261C41: PptDoc::~PptDoc(void) (include/qmemarray.h:60)
   by c05C261F0E: PptXml::~PptXml(void) (pptxml.cc:44)
 Address c04BB292A8 is 0 bytes inside a block of size 64 alloc'd
   at c04004318C: operator new[](unsigned int) (vg_clientfuncs.c:152)
   by c05C21BC15: KLaola::readSBStream(int) const (klaola.cc:314)
   by c05C21C155: KLaola::stream(KLaola::OLENode const *) (klaola.cc:416)
   by c05C21788F: OLEFilter::convert(QCString const &) (olefilter.cc:272)

服务器ip和目地详细地址重合

这儿的查验只包含相近 memcpy, strcpy, strncpy, strcat, strncat 那样的有服务器ip和目地详细地址实际操作的C函数库,保证服务器ip和目地详细地址表针不容易重合。

不正确表明是那样的:

==27492== Source and destination overlap in memcpy(c0bffff294, c0bffff280, 21)
==27492==    at c040026CDC: 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 c05C2CFA7: malloc (vg_replace_malloc.c:298)
==32233==    by c0400555: foo (fishy.c:15)
==32233==    by c0400583: 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 只輸出统计分析的結果,yesfull 輸出详尽內容。

普遍的应用是:--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 c0........: malloc (vg_replace_malloc.c:...)
==3334==    by c0........: mk (leak-tree.c:11)
==3334==    by c0........: 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 赶不及輸出就挂掉。

评论(0条)

刀客源码 游客评论