美文网首页
valgrind 的使用

valgrind 的使用

作者: liualiu | 来源:发表于2019-05-11 19:21 被阅读0次

    Valgrind

    Valgrind 原理

    valgrind 是一个提供了一些 debug 和优化的工具的工具箱,可以使得你的程序减少内存泄漏或者错误访问.
    valgrind 默认使用 memcheck 去检查内存问题.

    memcheck 检测内存问题的原理如下图所示:


    valgrind.jpg

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

    1. valid-value map:
      对于进程的整个地址空间中的每一个字节(byte),都有与之对应的 8 个 bits;对于 CPU 的每个寄存器,也有一个与之对应的 bit 向量。这些 bits 负责记录该字节或者寄存器值是否具有有效的、已初始化的值。
    2. valid-address map
      对于进程整个地址空间中的每一个字节(byte),还有与之对应的 1 个 bit,负责记录该地址是否能够被读写。

    检测原理:

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

    Quick start

    使用valgrind 很简单, 首先编译好要测试的程序 (为了使valgrind发现的错误更精确,如能够定位到源代码行,建议在编译时加上-g参数,编译优化选项请选择O0,虽然这会降低程序的执行效率。), 假设运行这个程序的命令是

    ./a.out arg1 arg2
    

    那么要使用 valgrind 的话只需要运行

    valgrind --leak-check=yes ./a.out arg1 arg2
    

    就可以了.

    valgrind的输出也很好看得懂, 例如下面这个 C 程序

      #include <stdlib.h>
    
      void f(void)
      {
         int* x = malloc(10 * sizeof(int));
         x[10] = 0;        // problem 1: heap block overrun
      }                    // problem 2: memory leak -- x not freed
    
      int main(void)
      {
         f();
         return 0;
      }
    

    valgrind 的输出为

    liu@liu ~> valgrind --leak-check=yes ./a.out 
    ==4372== Memcheck, a memory error detector
    ==4372== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
    ==4372== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
    ==4372== Command: ./a.out
    ==4372== 
    ==4372== Invalid write of size 4
    ==4372==    at 0x400504: f (in /home/liu/a.out)
    ==4372==    by 0x400523: main (in /home/liu/a.out)
    ==4372==  Address 0x51fa068 is 0 bytes after a block of size 40 alloc'd
    ==4372==    at 0x4C2EBAB: malloc (vg_replace_malloc.c:299)
    ==4372==    by 0x4004F7: f (in /home/liu/a.out)
    ==4372==    by 0x400523: main (in /home/liu/a.out)
    ==4372== 
    ==4372== 
    ==4372== HEAP SUMMARY:
    ==4372==     in use at exit: 40 bytes in 1 blocks
    ==4372==   total heap usage: 1 allocs, 0 frees, 40 bytes allocated
    ==4372== 
    ==4372== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
    ==4372==    at 0x4C2EBAB: malloc (vg_replace_malloc.c:299)
    ==4372==    by 0x4004F7: f (in /home/liu/a.out)
    ==4372==    by 0x400523: main (in /home/liu/a.out)
    ==4372== 
    ==4372== LEAK SUMMARY:
    ==4372==    definitely lost: 40 bytes in 1 blocks
    ==4372==    indirectly lost: 0 bytes in 0 blocks
    ==4372==      possibly lost: 0 bytes in 0 blocks
    ==4372==    still reachable: 0 bytes in 0 blocks
    ==4372==         suppressed: 0 bytes in 0 blocks
    ==4372== 
    ==4372== For counts of detected and suppressed errors, rerun with: -v
    ==4372== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
    
    
    • 如果一个函数内部出现了内存的访问错误或者是没有释放,那么多次调用这个函数,valgrind 会多次输出这个错误。所以当valgrind 报出大量错误的时候,不要慌张,其实可能只是很少一部份要改
    • 每一行开头的数字,比如这里的 4372 显示的是进程 id , 这个通常情况下是不需要看的
    • ==4372== Invalid write of size 4 这一行显示这里有一个错误,就是 x 分配了 10 byte 的空间,但是向第 11 个 byte 写数据, 所以就会显示 Invalid write 的错误
    • 在下面的这几行显示的是错误出现的位置, 因为是 stack trace, 所以需要先从最下面一行开始看
    • 内存泄漏显示的是如下的信息
      ==4372== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
      ==4372==    at 0x4C2EBAB: malloc (vg_replace_malloc.c:299)
      ==4372==    by 0x4004F7: f (in /home/liu/a.out)
      ==4372==    by 0x400523: main (in /home/liu/a.out)
      

    Valgrind 分析常见的内存问题

    • 使用未初始化的内存

      下面代码中 a[2] 没有初始化

      #include <stdlib.h>
      #include <stdio.h>
      
      int main(void)
      {
          int a[5];
          a[0] = a[1] = a[3] = a[4]  = 0;
          int s = 0;
          for(int i=0;i<3;i++){
                s+=a[i];
          }
          printf("%d\n",s);
          return 0;
      }
      

      valgrind 显示程序跳转依赖于未初始化的变量:

      ==7007== Conditional jump or move depends on uninitialised value(s)
      ==7007==    at 0x4E8B4A9: vfprintf (in /usr/lib64/libc-2.27.so)
      ==7007==    by 0x4E935F5: printf (in /usr/lib64/libc-2.27.so)
      ==7007==    by 0x400540: main (test.c:12)
      ==7007== 
      ==7007== Use of uninitialised value of size 8
      ==7007==    at 0x4E8792E: _itoa_word (in /usr/lib64/libc-2.27.so)
      ==7007==    by 0x4E8B225: vfprintf (in /usr/lib64/libc-2.27.so)
      ==7007==    by 0x4E935F5: printf (in /usr/lib64/libc-2.27.so)
      ==7007==    by 0x400540: main (test.c:12)
      
    • 内存读写越界

      具体例子就像是 上一节 quick start 中的例子

    • 动态内存管理错误

      • 申请和释放不一致
        由于 C++ 兼容 C,而 C 与 C++ 的内存申请和释放函数是不同的,因此在 C++ 程序中,就有两套动态内存管理函数。一条不变的规则就是采用 C 方式申请的内存就用 C 方式释放;用 C++ 方式申请的内存,用 C++ 方式释放。也就是用 malloc/alloc/realloc 方式申请的内存,用 free 释放;用 new 方式申请的内存用 delete 释放。在上述程序中,用 malloc 方式申请了内存却用 delete 来释放,虽然这在很多情况下不会有问题,但这绝对是潜在的问题。
      • 申请和释放不匹配
        申请了多少内存,在使用完成后就要释放多少。如果没有释放,或者少释放了就是内存泄露;多释放了也会产生问题。
      • 释放后仍然读写
        本质上说,系统会在堆上维护一个动态内存链表,如果被释放,就意味着该块内存可以继续被分配给其他部分,如果内存被释放后再访问,就可能覆盖其他部分的信息,这是一种严重的错误。

      例子

      #include <stdlib.h>
      #include <stdio.h>
      #include <string.h>
      #include <malloc.h>
      
      int main(void)
      {
          int *a = (int*)malloc(sizeof(int)*3);
          a[0]=1;
          a[1]=2;
          a[2]=3;
          free(a);
          printf("%d\n", a[0]);  // invalid read
          a[0]=1;    // invalid write
          free(a);  // free a two times
          return 0;
      }
      
      ==7556== Memcheck, a memory error detector
      ==7556== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
      ==7556== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
      ==7556== Command: ./a.out
      ==7556== 
      ==7556== Invalid read of size 4
      ==7556==    at 0x4005C2: main (test.c:13)
      ==7556==  Address 0x51fa040 is 0 bytes inside a block of size 12 free'd
      ==7556==    at 0x4C2FDAC: free (vg_replace_malloc.c:530)
      ==7556==    by 0x4005BD: main (test.c:12)
      ==7556==  Block was alloc'd at
      ==7556==    at 0x4C2EBAB: malloc (vg_replace_malloc.c:299)
      ==7556==    by 0x400587: main (test.c:8)
      ==7556== 
      1
      ==7556== Invalid write of size 4
      ==7556==    at 0x4005D9: main (test.c:14)
      ==7556==  Address 0x51fa040 is 0 bytes inside a block of size 12 free'd
      ==7556==    at 0x4C2FDAC: free (vg_replace_malloc.c:530)
      ==7556==    by 0x4005BD: main (test.c:12)
      ==7556==  Block was alloc'd at
      ==7556==    at 0x4C2EBAB: malloc (vg_replace_malloc.c:299)
      ==7556==    by 0x400587: main (test.c:8)
      ==7556== 
      ==7556== Invalid free() / delete / delete[] / realloc()
      ==7556==    at 0x4C2FDAC: free (vg_replace_malloc.c:530)
      ==7556==    by 0x4005EA: main (test.c:15)
      ==7556==  Address 0x51fa040 is 0 bytes inside a block of size 12 free'd
      ==7556==    at 0x4C2FDAC: free (vg_replace_malloc.c:530)
      ==7556==    by 0x4005BD: main (test.c:12)
      ==7556==  Block was alloc'd at
      ==7556==    at 0x4C2EBAB: malloc (vg_replace_malloc.c:299)
      ==7556==    by 0x400587: main (test.c:8)
      ==7556== 
      ==7556== 
      ==7556== HEAP SUMMARY:
      ==7556==     in use at exit: 0 bytes in 0 blocks
      ==7556==   total heap usage: 2 allocs, 3 frees, 1,036 bytes allocated
      ==7556== 
      ==7556== All heap blocks were freed -- no leaks are possible
      ==7556== 
      ==7556== For counts of detected and suppressed errors, rerun with: -v
      ==7556== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 0 from 0)
      
      

    Used in TM

    在 debug 的过程中,gdb 和程序崩溃的时候显示的信息都具有一定的误导性,但是valgrind 对于查找bug的帮助很大, 按照程序的逻辑,每秒种将 struct 结构序列化成 json 格式并保存到文件中去(保存一次打印一个 tick ),然后十秒之后终止。但是我们可以看到,这里第三次打印的时候就崩溃了。系统提示是(address boundary error)。

    [图片上传中...(Selection_001.png-27236b-1557573570699-0)]

    讲道理,我们现在应该使用 gdb 来看一下崩溃处的错误吧

    Selection_002.png

    然后可以发现是 malloc 函数里面崩溃了,这看不出什么东西,再继续查看一下函数调用的栈

    Selection_003.png

    然后我们就发现了其实是 jansson 库的 new 的时候的 bug,然后 debug 就自然而然的跑偏了

    在这个时候就可以使用 valgrind 了:

    valgrind --leak-check=yes ./p2p/test/addressbook_test
    
    Selection_004.png

    在这个打印出来的信息我们可以看到,json_decref invalid read 了内存了,说明 json_decref 的参数(一个 json_t 的对象)已经提前释放了。
    然后根据提示,找到了 pear_address_book.c:470 行的代码:

    pr_save_json_to_file(json, file_path);
    json_decref(json);
    g_ptr_array_unref(addr_book_json->addrs);
    

    然后进去 pr_save_json_to_file(json, file_path) 里面看了一下,里面已经 对 json 对象 调用了 json_decref 了,所以有两次释放。

    valgrind 的一些问题

    对于下列的代码,valgrind 就会报出内存内存泄漏, 但是将代码 g_ptr_array_free(b,0); 改成 g_ptr_array_unref(b); 就完全没问题,虽然没有设定释放函数,也没有释放。

    #include <glib.h>
    #include <stdio.h>
    #include <stdlib.h>
    int main()
    {
            GPtrArray* a = g_ptr_array_new_with_free_func(g_free);
            GPtrArray* b = g_ptr_array_new();
            for(int i=0;i<5;i++){
                    int * t = g_new(int,1);
                    g_ptr_array_add(a, t);
                    g_ptr_array_add(b, t);
            }
            g_ptr_array_unref(a);
            g_ptr_array_free(b,0);
            return 0;
    }
    
    
    Selection_005.png

    相关文章

      网友评论

          本文标题:valgrind 的使用

          本文链接:https://www.haomeiwen.com/subject/pkrfaqtx.html