Valgrind
Valgrind 原理
valgrind 是一个提供了一些 debug 和优化的工具的工具箱,可以使得你的程序减少内存泄漏或者错误访问.
valgrind 默认使用 memcheck 去检查内存问题.
memcheck 检测内存问题的原理如下图所示:
valgrind.jpg
Memcheck 能够检测出内存问题,关键在于其建立了两个全局表。
- valid-value map:
对于进程的整个地址空间中的每一个字节(byte),都有与之对应的 8 个 bits;对于 CPU 的每个寄存器,也有一个与之对应的 bit 向量。这些 bits 负责记录该字节或者寄存器值是否具有有效的、已初始化的值。 - 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
网友评论