易栈 · 一盏

塞外秋来,衡阳雁去

valgrind详解与使用实例

        valgrind是运行在Linux上的一套基于仿真技术的程序调试和分析工具。valgrind包含一系列的工具:

  • Memcheck:检测内存问题
  • Callgrind:检测函数调用问题,提供函数调用次数等信息
  • Cachegrind:检测缓存问题,提供缓存丢失和命中信息
  • Helgrind:检测多线程资源竞争
  • Massif:检测堆栈问题

        运行valgrind的时候,可以通过--tool参数指定使用哪个工具,在该参数缺省的情况下,valgrind默认使用Memcheck工具。

1、Memcheck介绍

        Memcheck是valgrind最常用的一个工具。Memcheck可以检查多种内存问题:

  • 使用未初始化的内存。如果在定义一个变量时没有赋初始值,后边即使赋值了,使用这个变量的时候Memcheck也会报"uninitialised value"错误。使用中会发现,valgrind提示很多这个错误,由于关注的是内存泄漏问题,所以可以用--undef-value-errors=选项把这个错误提示屏蔽掉,具体可以看后面的选项解释。
  • 内存读写越界(数组访问越界/访问已经释放的内存)
  • 不正确的内存释放(重复释放/使用不匹配的分配和释放函数)
  • 内存覆盖(memcpy的src和dst指针有重叠)
  • 内存泄漏

        这里主要介绍的是内存泄漏检测功能。
        Memcheck会跟踪所有在堆上分配的内存,当程序退出的时候,可以知道哪些是没有释放的。如果设置了--leak-check选项,Memcheck就会去统计这些没有释放的内存块中,哪些是存在指针,可以通过指针访问的(reachable from pointer),哪些是已经找不到指向的指针,也就是丢失了(lost)。
        指向内存块的指针有两种情况。一种是开始指针(start-pointer),指向内存块的开始位置。另一种是内部指针(interior-pointer),指向内存块的中间位置。
内部指针在几种情况下会出现:

  • 编程的时候出于程序实现的目的,把指针移动到所分配内存块的某个位置。
  • 该指针没有被初始化,是一个随机值。虽然指向该内存块,但是相互没关联,只是巧合。
  • stdstring:该指针可能是C++ std::string的内部char数组的指针。一些编译器会在std::string内部数组的开头保留3个字节,用来存储字符串长度、容量等信息,然后返回指向第4个字节的指针。
  • length64:有一些代码会分配一块内存,然后把开头的8个字节用来存储一个64位的数字。例如sqlite3MemMalloc。
  • newarray:该指针可能是用new[]来分配的一个C++对象数组。一些编译器会在这些内存块的开头存一些数据,然后返回指向这段数据之后的空间的指针。
  • multipleinheritance:该指针可能是指向一个多重继承的C++对象内部。

        通过启发式检测(heuristics),Memcheck可以识别3~4的情况,不把这些报告成内存泄漏。通过--leak-check-heuristics选项可以设置启发式检测,具体见后面的选项解释。
        Memcheck生成的报告中,把指针总结为几种类型(leak kinds):

  • Still reachable:可以找到指向该内存块的"开始指针"。理论上程序退出前,可以通过这些指针来释放内存。
  • Definitely lost:找不到指向该块的指针。这些块定义为"丢失(lost)",因为程序找不到这些块,也无法去释放。
  • Indirectly lost:块间接丢失。例如一个二叉树的根节点指针丢失了,那它的所有后代节点也间接丢失。
  • possibly lost:存在指向该块的"内部指针"。如上所述,如果是第1、2中情况的内部指针,Memcheck无法确定是否为合法,所以程序员要自己确认是不是自己设计的内部指针。

        下面是Memcheck生成内存泄漏的报告:

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.

        如果开启了启发式检测,则Memcheck会把一部分"possibly lost"识别成"still reachable"。生成的报告如下:

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会给出详细的每个块是在哪里分配,并且给出分配时函数调用堆栈(编译的时候使用-g选项和去掉-o优化选项,就可以得到更详细的函数信息,可以精确到代码的某一行)。可以通过--show-leak-kinds选项来选择要详细报告哪几种类型的错误。Memcheck会把函数调用堆栈相同或相似的内存块信息,放到同一个条目来显示,可以通过--leak-resolution来控制这个"相似"判断的力度。

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)

2、valgrind选项详解

        valgrind有公有的选项和每个工具自己的选项。这里只介绍少量公有选项,主要介绍Memcheck工具的选项。

--tool=<toolname> [default: memcheck]
        指定要运行的工具。

--log-file=<filename>
        指定日志输出的文件名。

--leak-check=<no|summary|yes|full> [default: summary]
        如果使能了该选项,在程序退出的时候,会统计内存泄漏的情况。如果设置为summary,会指出有多少内存泄漏;如是设置为full或者yes,会给出每条泄漏详细信息。

--leak-resolution=<low|med|high> [default: high]
        内存泄漏的详细信息包括该块内存在哪里分配的,分配时的函数调用堆栈。Memcheck会根据这个选项合并函数调用堆栈相似的条目。如果设置为low,只有开始的两个函数调用需要一致;如果设置为med,则需要开始的4个函数调用一致;如果为high,则所有的函数调用需要一致。

--show-leak-kinds=<set> [default: definite,possible]
        选择要显示哪几种类型的详细信息,可选值为all,none,definite,indirect,possible,reachable。

--errors-for-leak-kinds=<set> [default: definite,possible]
        指定哪几种类型的详细信息要视为错误信息。

--leak-check-heuristics=<set> [default: none]
        启发式检测,具体的作用见上面的分析。可选值为all,none,stdstring,length64,newarray,multipleinheritance.

--show-reachable=<yes|no> , --show-possibly-lost=<yes|no>
        类似--show-leak-kinds。

--undef-value-errors=<yes|no> [default: yes]
        控制是否要显示"uninitialised value"的错误。禁用这个选项可以加速Memcheck的运行。

--track-origins=<yes|no> [default: no]
        控制是否要跟踪"uninitialised values"的来源。默认情况下,Memcheck不会去跟踪,所以Memcheck会提示有"uninitialised value" ,但是不会告诉你来源(块是在哪里分配的,由那个函数分配)。
        开启这个选项会增加系统资源消耗(cpu,内存),所以建议只在需要追踪这类问题的时候开启。

--partial-loads-ok=<yes|no> [default: no]
        用来控制字节对齐情况下的,判断从非对齐字节中加载数据是合法还是不合法?这个选项的含义有待学习。

--keep-stacktraces=alloc|free|alloc-and-free|alloc-then-free|none [default: alloc-then-free]
        可以选择记录malloc/free时的堆栈信息(stack trace)。
        设为alloc-then-free时,Memcheck会记录分配时的堆栈信息,然后在释放时,用释放时的堆栈信息覆盖之前的记录。在这种情况下,报"use after free"错误的时候,只会打印free时的堆栈打印,看不到分配时候的信息。
        其他几种选项比较好理解,省略解释。

--freelist-vol=<number> [default: 20000000]
        一块内存被释放之后,并不马上用来重新分配。它只是被标注成"inaccessible"然后放到空闲队列里。这样做的目的是让最先释放的内存,最后被使用,延长内存块重新分配的时间。在这段时间间隔中,Memcheck可以检测出对已释放空间的非法访问。
        这个选项用来指定空闲队列的大小。增大这个值,可能会让Memcheck检测出更多对已释放空间的非法访问。

--freelist-big-blocks=<number> [default: 1000000]
        每次从空闲队列里取内存块的时候,会优先使用那些大小大于或者等于--freelist-big-blocks的块。这个保证"小"的块不会很快被用掉。这样Memcheck更可能检测出小的块的空悬指针之类的问题。
        如果这个值设成0,那块将会以FIFO顺序被取出。

--workaround-gcc296-bugs=<yes|no> [default: no]
        用来避免GCC 2.96中的一个错误,但是我们现在使用的GCC一般4.X版本了,所以可以不用考虑这个选项

--show-mismatched-frees=<yes|no> [default: yes]
        是否显示"使用不匹配的内存分配和释放方式"错误。
        在一种情况下,这个不匹配错误是不能避免的。就是当用户使用malloc/free来自定义new/new[]和delete/delete[]的实现,但是自定义的分配和释放函数的inline属性不匹配时(一个是inline,另一个不是)。例如delete是inline但是new不是,那么Memcheck会把所有的delete当成是free,这样就和new不匹配了。

--ignore-ranges=0xPP-0xQQ[,0xRR-0xSS]
        指定一个地址区间让Memcheck不用去检查。可以设置多个区间,用逗号分隔。

--malloc-fill=<hexnumber>
        指定一个16进制数用来填充新分配的内存空间(使用calloc分配的空间除外)。注意这些空间被自动填充了字节,但是Memcheck仍把它作为未初始化的来看待。

--free-fill=<hexnumber>
        指定一个16进制数用来填充被释放了的内存空间。注意这些空间仍被视作无效,无法访问。

3、valgrind编译

        README文件中给出的编译步骤如下:

./configure
make
make install

        为了能在嵌入式设备上跑,需要配置一些交叉编译的选项。
        配置之前先修改一下configure文件,将"armv7*)"改成"armv7*|arm)"。
        执行./configure --help可以看到选项,根据所用的编译工具进行配置:

./configure --host=arm-linux CC=arm-none-linux-gnueabi-gcc CPP=arm-none-linux-gnueabi-cpp CXX=arm-none-linux-gnueabi-g++ --prefix=/mnt/valgrind

        这里要注意的是--prefix的路径需要和valgrind放到嵌入式设备中的路径一致,否则不能正常运行。
        配置完之后,依次执行make;make install,然后把生成的文件拷到设备的/mnt/valgrindm目录下,就可以在嵌入式设备下使用valgrind。

4、valgrind应用实例

        假设可执行文件路径为/usr/sbin/myapp。
        根据上述内容可以设置valgrind命令为:

valgrind --log-file=/tmp/valgrind.log --undef-value-errors=no --leak-check=full --leak-check-heuristics=all /usr/sbin/myapp

        运行一段时间之后,终止程序,就会在/tmp/valgrind.log中看到关于内存泄漏的详细报告信息。

        本文只是关注valgrind检测内存泄漏的功能,valgrind还有其他很强大的功能有待挖掘。更多的信息可以查看官方的User Manual。

        第一篇博客,不知道写什么,把之前写的一篇valgrind学习笔记放上来。

本文链接:易栈 - valgrind详解与使用实例

 评论(4)

  • 千禾不酱油:这么长竟然是你自己写的啊,挺专业的。
    2015年10月6日
  • 夏目Lily:哎哟不错哦~(╯3╰)
    2015年10月7日
  • 人火言十:拜读,三伏首
    2015年10月9日
  • 夏目Lily:加油↖(^ω^)↗
    2015年10月27日