用valgrind 检测内存错误

来源:互联网 发布:企业目录搜索软件 编辑:程序博客网 时间:2024/05/22 06:42
用valgrind 检测内存错误
----------------------------------------
前言:
----------------------------------------
介绍了valgrind 是什么,工作原理。
内存错误是什么,
给出了一个综合内存错误实例。看一看valgrind 如何汇报错误.
 
----------------------------------------
1. valgrind 是什么
----------------------------------------
Valgrind是一套Linux下,开放源代码(GPL V2)的仿真调试工具的集合。
Valgrind由内核(core)以及基于内核的其他调试工具组成。
内核类似于一个框架(framework),它模拟了一个CPU环境,并提供服务给其他工具;
而其他工具则类似于插件 (plug-in),利用内核提供的服务完成各种特定的内存调试任务。
Valgrind的体系结构如下图所示:  上不了图,凑合着吧
 
----------------------------------------
2. valgrind 包含的工具
----------------------------------------
Memcheck。这是valgrind应用最广泛的工具,一个重量级的内存检查器,能够发现开发中绝大多数内存错误使用情况,
          比如:使用未初始化的内存,使用已经释放了的内存,内存访问越界等。这也是本文将重点介绍的部分。
Callgrind。它主要用来检查程序中函数调用过程中出现的问题。
Cachegrind。它主要用来检查程序中缓存使用出现的问题。
Helgrind。它主要用来检查多线程程序中出现的竞争问题。
Massif。它主要用来检查程序中堆栈使用中出现的问题。
Extension。可以利用core提供的功能,自己编写特定的内存调试工具
 
 
----------------------------------------
3. valgrind 工作原理
----------------------------------------
Memcheck 能够检测出内存问题,关键在于其建立了两个全局表。并且虚拟了一个cpu 环境
 
Valid-Value 表:
对于进程的整个地址空间中的每一个字节(byte),都有与之对应的 8 个 bits;
对于 CPU 的每个寄存器,也有一个与之对应的 bit 向量。这些 bits 负责记录该字节或者寄存器值是否具有有效的、已初始化的值。
 
Valid-Address 表
对于进程整个地址空间中的每一个字节(byte),还有与之对应的 1 个 bit,负责记录该地址是否能够被读写。
 
 
----------------------------------------
4. valgrind 的使用.
----------------------------------------
valgrind --help 有介绍. man valgrind 亦可。 众多的选项,
我们可能只关心几种。
检测过程:
当要读写内存中某个字节时,首先检查这个字节对应的 A bit。如果该A bit显示该位置是无效位置,memcheck 则报告非法地址读写错误。
内核(core)类似于一个虚拟的 CPU 环境,这样当内存中的某个字节被加载到真实的 CPU 中时,该字节对应的 V bit 也被加载到虚拟的 CPU 环境中。
一旦寄存器中的值,被用来产生内存地址,或者该值能够影响程序输出,则 memcheck 会检查对应的V bits,如果该值尚未初始化,则会报告使用未初始化内存错误。
 
常见内存错误:
****************************************
甲. 使用未初始化内存问题
----------------------------------------
全局变量和静态变量初始值为0,
而局部变量和动态申请的变量,其初始值为随机值。
如果程序使用了为随机值的变量,那么程序的行为就变得不可预期。
----------------------------------------
乙. 内存读写越界
----------------------------------------
访问了你不应该/没有权限访问的内存地址空间,  
读一下可能问题还不大(也是非法访问内存),
如果是写操作,那么后果将不可预期。使用这个内存的人就惨了。
----------------------------------------
丙. 内存覆盖
----------------------------------------
C 语言的强大和可怕之处在于其可以直接操作内存,
C 标准库中提供了大量这样的函数,比如 strcpy, strncpy, memcpy, strcat 等,
这些函数有一个共同的特点就是需要设置源地址 (src),和目标地址(dst),
src 和 dst 指向的地址不能发生重叠,否则结果将不可预期
----------------------------------------
丁. 动态内存使用的常见错误
----------------------------------------
常见的内存分配方式分三种:静态存储,栈上分配,堆上分配。
全局变量属于静态存储,它们是在编译时就被分配了存储空间,
函数内的局部变量属于栈上分配,而最灵活的内存使用方式当属堆上分配,
也叫做动态内存分配了, 见下述。
 
    动态内存使用的常见错误
****************************************
    子: 申请和释放不一致
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
由于 C++ 兼容 C,而 C 与 C++ 的内存申请和释放函数是不同的,
因此在 C++ 程序中,就有两套动态内存管理函数。
一条不变的规则就是采用 C 方式申请的内存就用 C 方式释放;用 C++ 方式申请的内存,用 C++ 方式释放。
也就是用 malloc/alloc/realloc 方式申请的内存,用 free 释放;
用 new 方式申请的内存用 delete 释放。 这主要是牵扯构造和析构过程,构造和析构往往有嵌套的内存分配操作。
用 malloc 方式申请了内存却用 delete 来释放,虽然这在很多情况下不会有问题,但这绝对是潜在的问题。
用 new 申请的内存用free 释放,由于缺少必要的析够过程,一般会引发内存泄漏。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    丑: 申请和释放不匹配
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
申请了多少内存,在使用完成后就要释放多少。如果没有释放,或者少释放了就是内存泄露;
多释放了也会产生问题。,指针p和pt指向的是同一块内存,却被先后释放了两次。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    寅: 内存泄漏
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
虽属于申请,释放不匹配,这里再强调一次.
动态申请的内存,在使用完后既没有释放,又无法被程序的其他部分访问.即内存指针已经丢失,即为内存泄漏。
通常在一个函数内分配内存,使用完后再释放,不会发生内存泄漏。
但有时根据逻辑的需要,只能在一个函数内申请,在另一个函数内释放,这就容易引发内存泄漏。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    卯: 释放后仍然读写
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
本质上说,系统会在堆上维护一个动态内存链表,如果被释放,就意味着该块内存可以继续被分配给其他部分,
如果内存被释放后再访问,就可能覆盖其他部分的信息,这是一种严重的错误, 使得程序结果不可预期!
 
---------------------------------------------------------------------
小结:
这么多的内存问题,可以概括为: 内存未初始化,非法访问内存,内存泄漏三种。
或者说非法数值,非法地址,内存泄漏三种。
非法地址也称野指针是segment fault 的罪魁祸首。野指针是未初始化指针或非法修改的指针。
数值的变动有合法修改和非法修改或考虑不周失控修改。
用gdb 可以监视每次修改的过程。
但gdb 定位bug,  远没有valgrind 使用的这么简单和直观,
valgrind 是汇报者,而gdb 是需要你自己寻找bug.
所以要各取所长。
---------------------------------------------------------------------
这么多的内存问题,难道valgrind 都能检测到吗? 嗯,基本上是这样。
下面给出一个充满内存错误的综合例子。看看结果。
这是一个温和的内存错误,并没有激发segment fault,引起操作系统大怒而杀掉进程。
 
[hjj@hjj ~/test]$ cat test.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
void uninit(void)
{
    int a[5];
    a[0]=a[1]=a[2]=10;
    a[4]=a[0]+a[1]+a[2]+a[3];    // a[3] 未初始化
    printf("a[4] is %d\n", a[4]);
    a[3]=0;
    a[2]=a[6];                // 内存访问越界, memcheck 不检测栈内存越界
    printf("a[2] is %d\n", a[2]);
}
 
void overcopy(void)
{
    char src[]="123456789";
    char dst[20];
    strcat(src,"abc");        // 内存越界,memcheck 不检测栈内存越界
    strcpy(src,dst);        // 内存覆盖
}
 
void malloc_err(void)
{
    char *p1,*p2,*p3;
    p1 = (char *)malloc(10);
    p2 = p1;
    p3 = (char *)malloc(20);
    memset(p1,0,10);
    p1[10] = 5;                // 越界访问
    delete p2;                // 释放,分配不匹配
    *p1 = 10;                // 访问已释放的内存
    free(p1);                // 释放两次内存。
                            // p3 泄漏, 以后没有机会访问了
}
 
 
int main(void)
{
    uninit();
    overcopy();
    malloc_err();
    return 0;
}
----------------------------------------
编译:
[hjj@hjj ~/test]$ g++ -g -o test test.cpp
----------------------------------------
运行:
[hjj@hjj ~/test]$ valgrind  ./test  
----------------------------------------
==3007== Memcheck, a memory error detector
==3007== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==3007== Using Valgrind-3.9.0 and LibVEX; rerun with -h for copyright info
==3007== Command: ./test
==3007==  
==3007== Use of uninitialised value of size 8
==3007==    at 0x3499643D9B: _itoa_word (in /lib64/libc-2.12.so)
==3007==    by 0x3499646952: vfprintf (in /lib64/libc-2.12.so)
==3007==    by 0x349964F489: printf (in /lib64/libc-2.12.so)
==3007==    by 0x4007F8: uninit() (test.cpp:10)
==3007==    by 0x4008EF: main (test.cpp:41)
==3007==  
==3007== Conditional jump or move depends on uninitialised value(s)
==3007==    at 0x3499643DA5: _itoa_word (in /lib64/libc-2.12.so)
==3007==    by 0x3499646952: vfprintf (in /lib64/libc-2.12.so)
==3007==    by 0x349964F489: printf (in /lib64/libc-2.12.so)
==3007==    by 0x4007F8: uninit() (test.cpp:10)
==3007==    by 0x4008EF: main (test.cpp:41)
==3007==  
==3007== Conditional jump or move depends on uninitialised value(s)
==3007==    at 0x34996453E3: vfprintf (in /lib64/libc-2.12.so)
==3007==    by 0x349964F489: printf (in /lib64/libc-2.12.so)
==3007==    by 0x4007F8: uninit() (test.cpp:10)
==3007==    by 0x4008EF: main (test.cpp:41)
==3007==  
==3007== Conditional jump or move depends on uninitialised value(s)
==3007==    at 0x3499645401: vfprintf (in /lib64/libc-2.12.so)
==3007==    by 0x349964F489: printf (in /lib64/libc-2.12.so)
==3007==    by 0x4007F8: uninit() (test.cpp:10)
==3007==    by 0x4008EF: main (test.cpp:41)
==3007==  
a[4] is 30
==3007== Use of uninitialised value of size 8
==3007==    at 0x3499643D9B: _itoa_word (in /lib64/libc-2.12.so)
==3007==    by 0x3499646952: vfprintf (in /lib64/libc-2.12.so)
==3007==    by 0x349964F489: printf (in /lib64/libc-2.12.so)
==3007==    by 0x400819: uninit() (test.cpp:13)
==3007==    by 0x4008EF: main (test.cpp:41)
==3007==  
==3007== Conditional jump or move depends on uninitialised value(s)
==3007==    at 0x3499643DA5: _itoa_word (in /lib64/libc-2.12.so)
==3007==    by 0x3499646952: vfprintf (in /lib64/libc-2.12.so)
==3007==    by 0x349964F489: printf (in /lib64/libc-2.12.so)
==3007==    by 0x400819: uninit() (test.cpp:13)
==3007==    by 0x4008EF: main (test.cpp:41)
==3007==  
==3007== Conditional jump or move depends on uninitialised value(s)
==3007==    at 0x34996453E3: vfprintf (in /lib64/libc-2.12.so)
==3007==    by 0x349964F489: printf (in /lib64/libc-2.12.so)
==3007==    by 0x400819: uninit() (test.cpp:13)
==3007==    by 0x4008EF: main (test.cpp:41)
==3007==  
==3007== Conditional jump or move depends on uninitialised value(s)
==3007==    at 0x3499645401: vfprintf (in /lib64/libc-2.12.so)
==3007==    by 0x349964F489: printf (in /lib64/libc-2.12.so)
==3007==    by 0x400819: uninit() (test.cpp:13)
==3007==    by 0x4008EF: main (test.cpp:41)
==3007==  
a[2] is 4196048
==3007== Conditional jump or move depends on uninitialised value(s)
==3007==    at 0x4A0809F: strcpy (mc_replace_strmem.c:443)
==3007==    by 0x400871: overcopy() (test.cpp:21)
==3007==    by 0x4008F4: main (test.cpp:42)
==3007==  
==3007== Conditional jump or move depends on uninitialised value(s)
==3007==    at 0x4A080B7: strcpy (mc_replace_strmem.c:443)
==3007==    by 0x400871: overcopy() (test.cpp:21)
==3007==    by 0x4008F4: main (test.cpp:42)
==3007==  
==3007== Invalid write of size 1
==3007==    at 0x4008C3: malloc_err() (test.cpp:31)
==3007==    by 0x4008F9: main (test.cpp:43)
==3007==  Address 0x4c2e04a is 0 bytes after a block of size 10 alloc'd
==3007==    at 0x4A06AAA: malloc (vg_replace_malloc.c:291)
==3007==    by 0x40088A: malloc_err() (test.cpp:27)
==3007==    by 0x4008F9: main (test.cpp:43)
==3007==  
==3007== Mismatched free() / delete / delete []
==3007==    at 0x4A0606A: operator delete(void*) (vg_replace_malloc.c:502)
==3007==    by 0x4008D1: malloc_err() (test.cpp:32)
==3007==    by 0x4008F9: main (test.cpp:43)
==3007==  Address 0x4c2e040 is 0 bytes inside a block of size 10 alloc'd
==3007==    at 0x4A06AAA: malloc (vg_replace_malloc.c:291)
==3007==    by 0x40088A: malloc_err() (test.cpp:27)
==3007==    by 0x4008F9: main (test.cpp:43)
==3007==  
==3007== Invalid write of size 1
==3007==    at 0x4008D6: malloc_err() (test.cpp:33)
==3007==    by 0x4008F9: main (test.cpp:43)
==3007==  Address 0x4c2e040 is 0 bytes inside a block of size 10 free'd
==3007==    at 0x4A0606A: operator delete(void*) (vg_replace_malloc.c:502)
==3007==    by 0x4008D1: malloc_err() (test.cpp:32)
==3007==    by 0x4008F9: main (test.cpp:43)
==3007==  
==3007== Invalid free() / delete / delete[] / realloc()
==3007==    at 0x4A06484: free (vg_replace_malloc.c:468)
==3007==    by 0x4008E4: malloc_err() (test.cpp:34)
==3007==    by 0x4008F9: main (test.cpp:43)
==3007==  Address 0x4c2e040 is 0 bytes inside a block of size 10 free'd
==3007==    at 0x4A0606A: operator delete(void*) (vg_replace_malloc.c:502)
==3007==    by 0x4008D1: malloc_err() (test.cpp:32)
==3007==    by 0x4008F9: main (test.cpp:43)
==3007==  
==3007==  
==3007== HEAP SUMMARY:
==3007==     in use at exit: 20 bytes in 1 blocks
==3007==   total heap usage: 2 allocs, 2 frees, 30 bytes allocated
==3007==  
==3007== LEAK SUMMARY:
==3007==    definitely lost: 20 bytes in 1 blocks
==3007==    indirectly lost: 0 bytes in 0 blocks
==3007==      possibly lost: 0 bytes in 0 blocks
==3007==    still reachable: 0 bytes in 0 blocks
==3007==         suppressed: 0 bytes in 0 blocks
==3007== Rerun with --leak-check=full to see details of leaked memory
==3007==  
==3007== For counts of detected and suppressed errors, rerun with: -v
==3007== Use --track-origins=yes to see where uninitialised values come from
==3007== ERROR SUMMARY: 32 errors from 14 contexts (suppressed: 4 from 4)
 
----------------------------------------
结果分析:
以上可见, 除了栈内存越界它未报出外, 其它错误全报告了。 其中有的错误例如uninit,曾反复申报。
估计是每执行一次,就报告一次。
 
其实,memcheck 作者只所以不报告栈越界错误, 是因为调试信息中不包含栈变量数据大小信息。
栈变量拿来就使用,没有分配的概念。 但对于内存覆盖,则是善意提醒。 它估计这一定不是你的初衷。
当然如果你很变态,一定要写出有内存覆盖而运转正常的程序,也不是不可以,因为一切还是可控的。
对于堆错误,则报告完善。
----------------------------------------
 
----------------------------------------
使用 --leak-check=full 选项, 进一步报告内存泄漏位置
valgrind  --leak-check=full ./test  
----------------------------------------
输出基本一样, 在HEAP SUMMARY 中, 进一步指明了泄漏的位置。
==3030== 20 bytes in 1 blocks are definitely lost in loss record 1 of 1
==3030==    at 0x4A06AAA: malloc (vg_replace_malloc.c:291)
==3030==    by 0x4008A0: malloc_err() (test.cpp:29)
==3030==    by 0x4008F9: main (test.cpp:43)
 
valgrind 另外常用的选项,浅显易懂。
--leak-check=full  
--show-leak-kinds=all  
--track-origins=yes   
-v
 

0 0
原创粉丝点击