valgrind 的使用

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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容