Valgrind

Valgrind包括如下一些工具:


  1. Memcheck。这是valgrind应用最广泛的工具,一个重量级的内存检查器,能够发现开发中绝大多数内存错误使用情况,比如:使用未初始化的内存,使用已经释放了的内存,内存访问越界等。这也是本文将重点介绍的部分。
  2. Callgrind。它主要用来检查程序中函数调用过程中出现的问题。
  3. Cachegrind。它主要用来检查程序中缓存使用出现的问题。
  4. Helgrind。它主要用来检查多线程程序中出现的竞争问题。
  5. Massif。它主要用来检查程序中堆栈使用中出现的问题。
  6. Extension。可以利用core提供的功能,自己编写特定的内存调试工具


Memcheck :

主要检查下面的程序错误:

  • 使用未初始化的内存 (Use of uninitialised memory)    

  • 使用已经释放了的内存 (Reading/writing memory after it has been free’d)    

  • 使用超过 malloc分配的内存空间(Reading/writing off the end of malloc’d blocks)    

  • 对堆栈的非法访问 (Reading/writing inappropriate areas on the stack)    

  • 申请的空间是否有释放 (Memory leaks – where pointers to malloc’d blocks are lost forever)    

  • malloc/free/new/delete申请和释放内存的匹配(Mismatched use of malloc/new/new [] vs free/delete/delete [])    

  • src和dst的重叠(Overlapping src and dst pointers in memcpy() and related functions)    

Callgrind:

Callgrind收集程序运行时的一些数据,函数调用关系等信息,还可以有选择地进行cache 模拟。在运行结束时,它会把分析数据写入一个文件。callgrind_annotate可以把这个文件的内容转化成可读的形式。

Cachegrind:

它模拟 CPU中的一级缓存I1,D1和L2二级缓存,能够精确地指出程序中 cache的丢失和命中。如果需要,它还能够为我们提供cache丢失次数,内存引用次数,以及每行代码,每个函数,每个模块,整个程序产生的指令数。这对优化程序有很大的帮助。在x86和amd64上,cachegrind通过CPUID自动探测计算机的cache配置,所以,在多数情况下不再需要更多的配置信息。

Helgrind:

它主要用来检查多线程程序中出现的竞争问题。Helgrind 寻找内存中被多个线程访问,而又没有一贯加锁的区域,这些区域往往是线程之间失去同步的地方,而且会导致难以发掘的错误。Helgrind实现了名为” Eraser” 的竞争检测算法,并做了进一步改进,减少了报告错误的次数。

Massif:

堆栈分析器,它能测量程序在堆栈中使用了多少内存,告诉我们堆块,堆管理块和栈的大小。Massif能帮助我们减少内存的使用,在带有虚拟内存的现代系统中,它还能够加速我们程序的运行,减少程序停留在交换区中的几率。

lackey:

lackey是一个实例程序,以其为模板可以创建自己的工具。在程序结结束后,它打印出一些基本的关于程序执行统计数据。




Memcheck 能够检测出内存问题,关键在于其建立了两个全局表。

      1.Valid-Value 表:

对于进程的整个地址空间中的每一个字节(byte),都有与之对应的 8 个 bits;对于 CPU 的每个寄存器,也有一个与之对应的 bit 向量。这些 bits 负责记录该字节或者寄存器值是否具有有效的、已初始化的值。

      2.Valid-Address 表

对于进程整个地址空间中的每一个字节(byte),还有与之对应的 1 个 bit,负责记录该地址是否能够被读写。

检测原理:

  • 当要读写内存中某个字节时,首先检查这个字节对应的 A bit。如果该A bit显示该位置是无效位置,memcheck 则报告读写错误。
  • 内核(core)类似于一个虚拟的 CPU 环境,这样当内存中的某个字节被加载到真实的 CPU 中时,该字节对应的 V bit 也被加载到虚拟的 CPU 环境中。一旦寄存器中的值,被用来产生内存地址,或者该值能够影响程序输出,则 memcheck 会检查对应的V bits,如果该值尚未初始化,则会报告使用未初始化内存错误。


Valgrind 使用

用法: valgrind [options] prog-and-args [options]: 常用选项,适用于所有Valgrind工具(Valgrind公共选项

  1. --tool=<name>                          运行 valgrind中名为toolname的工具。默认memcheck。
  2. -h --help                                     显示帮助信息。
  3. --version                                    显示valgrind内核的版本,每个工具都有各自的版本。
  4. -q --quiet                                    安静地运行,只打印错误信息。
  5. -v --verbose                               更详细的信息, 增加错误数统计。
  6. --trace-children=no|yes           跟踪子线程? 默认:[no]
  7. --track-fds=no|yes                     跟踪打开的文件描述?默认:[no]
  8. --time-stamp=no|yes                增加时间戳到LOG信息? 默认[no]
  9. --log-fd=<number>                   日志信息写入到文件描述符  默认:[2=stderr]
  10. --log-file=<file>                         日志信息(LOG)写入到文件
  11. --log-file-exactly=<file>            日志信息写入到外部文件  默认<file>
  12. --log-file-qualifier=<VAR>       取得环境变量的值来做为输出信息的文件名。 [none]
  13. --log-socket=ipaddr:port          输出LOG到socket ,ipaddr:port
Valgrind选项
1.--run-libc-freeres=no|yes                在退出时清除glibc内存  [yes]
2.--sim-hints=hint1,hint2,...                 lax-ioctls,enable-outer  [none]
3.--show-emwarns=no|yes                显示仿真限制的警告  [no]
4.--smc-check=none|stack|all            自修改代码检查,不检查,仅检查栈中的代码  [stack]
5.--kernel-variant=variant1,...             处理非标准内核变量  [none]



(错误报告工具选项

  1. --xml=yes                                  将信息以xml格式输出,只有memcheck可用
  2. --xml-user-comment=STR      逐字复制STR到XML
  3. --demangle=no|yes                  是否自动解除C++名称修饰  [yes]
  4. --show-below-main=no|yes    在main后继续跟踪栈   [no]
  5. --gen-suppressions=no|yes|all        打印错误的抑制信息   [no]
  6. --suppressions=<filename>    抑制文件中的错误描诉
  7. --num-callers=<number>        显示栈跟踪器中的number号调用者   [12]
  8. --error-limit=no|yes                   如果太多错误,则停止显示新错误?   [yes]
  9. --error-exitcode=<number>     如果发现错误则返回错误代码   [0=disable]
  10. --db-attach=no|yes                   当出现错误,valgrind会自动启动调试器gdb。 [no]
  11. --db-command=<command>  启动调试器的命令行选项   [gdb -nw %f %p]
  12. --input-fd=<number>                 用于输入的文件描述符  [0=stdin]
  13. --max-stackframe=<number>  假设因sp变化大于number字节而引起堆转变  [2000000]

适用于Memcheck工具的相关选项

  1. --leak-check=no|summary|full            要求对leak给出详细信息? [summary]
  2. --leak-resolution=low|med|high         内存泄漏检查出多少字节 [low]
  3. --show-reachable=no|yes                   显示泄漏检查中的块 [no]
  4. --undef-value-errors=no|yes               检查未定义变量错误  [yes]
  5. --partial-loads-ok=no|yes                    请参阅手册 [no]
  6. --freelist-vol=<number>                       释放块队列的序号  [5000000]
  7. --workaround-gcc296-bugs=no|yes   自解释  [no]
  8. --alignment=<number>                         设置分配的最小对其大小    [8]


Valgrind 使用举例(一)

下面是一段有问题的C程序代码test.c

#i nclude <stdlib.h>
void f(void)
{
   int* x = malloc(10 * sizeof(int));
   x[10] = 0;  //问题1: 数组下标越界
}                  //问题2: 内存没有释放

int main(void)
{
   f();
   return 0;
 }

1、 编译程序test.c
gcc -Wall test.c -g -o test
2、 使用Valgrind检查程序BUG
valgrind --tool=memcheck --leak-check=full ./test


使用未初始化内存问题

问题分析:

对于位于程序中不同段的变量,其初始值是不同的,全局变量和静态变量初始值为0,而局部变量和动态申请的变量,其初始值为随机值。如果程序使用了为随机值的变量,那么程序的行为就变得不可预期。

下面的程序就是一种常见的,使用了未初始化的变量的情况。数组a是局部变量,其初始值为随机值,而在初始化时并没有给其所有数组成员初始化,如此在接下来使用这个数组时就潜在有内存问题。

清单 3 

结果分析:

假设这个文件名为:badloop.c,生成的可执行程序为badloop。用memcheck对其进行测试,输出如下。

清单 4 

输出结果显示,在该程序第11行中,程序的跳转依赖于一个未初始化的变量。准确的发现了上述程序中存在的问题。

内存读写越界

问题分析:

这种情况是指:访问了你不应该/没有权限访问的内存地址空间,比如访问数组时越界;对动态内存访问时超出了申请的内存大小范围。下面的程序就是一个典型的数组越界问题。pt是一个局部数组变量,其大小为4,p初始指向pt数组的起始地址,但在对p循环叠加后,p超出了pt数组的范围,如果此时再对p进行写操作,那么后果将不可预期。

清单 5 

结果分析:

假设这个文件名为badacc.cpp,生成的可执行程序为badacc,用memcheck对其进行测试,输出如下。

清单 6 

输出结果显示,在该程序的第15行,进行了非法的写操作;在第16行,进行了非法读操作。准确地发现了上述问题。

内存覆盖

问题分析:

C 语言的强大和可怕之处在于其可以直接操作内存,C 标准库中提供了大量这样的函数,比如 strcpy, strncpy, memcpy, strcat 等,这些函数有一个共同的特点就是需要设置源地址 (src),和目标地址(dst),src 和 dst 指向的地址不能发生重叠,否则结果将不可预期。

下面就是一个 src 和 dst 发生重叠的例子。在 15 与 17 行中,src 和 dst 所指向的地址相差 20,但指定的拷贝长度却是 21,这样就会把之前的拷贝值覆盖。第 24 行程序类似,src(x+20) 与 dst(x) 所指向的地址相差 20,但 dst 的长度却为 21,这样也会发生内存覆盖。

清单 7 

结果分析:

假设这个文件名为 badlap.cpp,生成的可执行程序为 badlap,用 memcheck 对其进行测试,输出如下。

点击看大图 

输出结果显示上述程序中第15,17,24行,源地址和目标地址设置出现重叠。准确的发现了上述问题。

动态内存管理错误

问题分析:

常见的内存分配方式分三种:静态存储,栈上分配,堆上分配。全局变量属于静态存储,它们是在编译时就被分配了存储空间,函数内的局部变量属于栈上分配,而最灵活的内存使用方式当属堆上分配,也叫做内存动态分配了。常用的内存动态分配函数包括:malloc, alloc, realloc, new等,动态释放函数包括free, delete。

一旦成功申请了动态内存,我们就需要自己对其进行内存管理,而这又是最容易犯错误的。下面的一段程序,就包括了内存动态管理中常见的错误。

清单 9 

常见的内存动态管理错误包括:

  1.  
    • 申请和释放不一致

由于 C++ 兼容 C,而 C 与 C++ 的内存申请和释放函数是不同的,因此在 C++ 程序中,就有两套动态内存管理函数。一条不变的规则就是采用 C 方式申请的内存就用 C 方式释放;用 C++ 方式申请的内存,用 C++ 方式释放。也就是用 malloc/alloc/realloc 方式申请的内存,用 free 释放;用 new 方式申请的内存用 delete 释放。在上述程序中,用 malloc 方式申请了内存却用 delete 来释放,虽然这在很多情况下不会有问题,但这绝对是潜在的问题。

  1.  
    • 申请和释放不匹配

申请了多少内存,在使用完成后就要释放多少。如果没有释放,或者少释放了就是内存泄露;多释放了也会产生问题。上述程序中,指针p和pt指向的是同一块内存,却被先后释放两次。

  1.  
    • 释放后仍然读写

本质上说,系统会在堆上维护一个动态内存链表,如果被释放,就意味着该块内存可以继续被分配给其他部分,如果内存被释放后再访问,就可能覆盖其他部分的信息,这是一种严重的错误,上述程序第16行中就在释放后仍然写这块内存。

结果分析:

假设这个文件名为badmac.cpp,生成的可执行程序为badmac,用memcheck对其进行测试,输出如下。
清单 10 

输出结果显示,第14行分配和释放函数不一致;第16行发生非法写操作,也就是往释放后的内存地址写值;第17行释放内存函数无效。准确地发现了上述三个问题。


内存泄漏

问题描述:

内存泄露(Memory leak)指的是,在程序中动态申请的内存,在使用完后既没有释放,又无法被程序的其他部分访问。内存泄露是在开发大型程序中最令人头疼的问题,以至于有人说,内存泄露是无法避免的。其实不然,防止内存泄露要从良好的编程习惯做起,另外重要的一点就是要加强单元测试(Unit Test),而memcheck就是这样一款优秀的工具。

下面是一个比较典型的内存泄露案例。main函数调用了mk函数生成树结点,可是在调用完成之后,却没有相应的函数:nodefr释放内存,这样内存中的这个树结构就无法被其他部分访问,造成了内存泄露。

在一个单独的函数中,每个人的内存泄露意识都是比较强的。但很多情况下,我们都会对malloc/free 或new/delete做一些包装,以符合我们特定的需要,无法做到在一个函数中既使用又释放。这个例子也说明了内存泄露最容易发生的地方:即两个部分的接口部分,一个函数申请内存,一个函数释放内存。并且这些函数由不同的人开发、使用,这样造成内存泄露的可能性就比较大了。这需要养成良好的单元测试习惯,将内存泄露消灭在初始阶段。

清单 1 

清单 11.2 


清单 11.3 

结果分析:

假设上述文件名位tree.h, tree.cpp, badleak.cpp,生成的可执行程序为badleak,用memcheck对其进行测试,输出如下。



点击看大图 

该示例程序是生成一棵树的过程,每个树节点的大小为12(考虑内存对齐),共8个节点。从上述输出可以看出,所有的内存泄露都被发现。Memcheck将内存泄露分为两种,一种是可能的内存泄露(Possibly lost),另外一种是确定的内存泄露(Definitely lost)。Possibly lost 是指仍然存在某个指针能够访问某块内存,但该指针指向的已经不是该内存首地址。Definitely lost 是指已经不能够访问这块内存。而Definitely lost又分为两种:直接的(direct)和间接的(indirect)。直接和间接的区别就是,直接是没有任何指针指向该内存,间接是指指向该内存的指针都位于内存泄露处。在上述的例子中,根节点是directly lost,而其他节点是indirectly lost。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值