记得在大学的刚开始学习C/C++的时候,对于内存问题一点也没有关心过,其实也是关心比较少,知道后来才慢慢注意起来,当时排查是否有内存泄漏全靠手,去看看malloc和free或者new和delete是否对应起来,这也是一种比较简单的查询是否有内存泄漏的办法,后来老师提供了一种用程序来检测是否有内存泄漏,其实就是重载new和delete的方法。
今天,我们一起来学习一下强大的内存检测工具----valgrind。
1. valgrind简介
Valgrind是一套Linux下,开放源代码(GPL V2)的仿真调试工具的集合。Valgrind由内核(core)以及基于内核的其他调试工具组成。内核类似于一个框架(framework),它模拟了一个CPU环境,并提供服务给其他工具;而其他工具则类似于插件 (plug-in),利用内核提供的服务完成各种特定的内存调试任务。Valgrind的体系结构如下图所示:
2.valgrind工具
(1)Memcheck。这是valgrind应用最广泛的工具,一个重量级的内存检查器,能够发现开发中绝大多数内存错误使用情况,比如:使用未初始化的内存,使用已经释放了的内存,内存访问越界等。这也是本文将重点介绍的部分。
(2)Callgrind。它主要用来检查程序中函数调用过程中出现的问题。
(3)Cachegrind。它主要用来检查程序中缓存使用出现的问题。
(4)Helgrind。它主要用来检查多线程程序中出现的竞争问题。
(5)Massif。它主要用来检查程序中堆栈使用中出现的问题。
(6)Extension。可以利用core提供的功能,自己编写特定的内存调试工具。
这个里面我们主要用就是第一个memcheck.
3. linux程序的内存布局
首先,我们来看看内存分配图
(1)代码段(.text)。这里存放的是CPU要执行的指令。代码段是可共享的,相同的代码在内存中只会有一个拷贝,同时这个段是只读的,防止程序由于错误而修改自身的指令。
(2)初始化数据段(.data)。这里存放的是程序中需要明确赋初始值的变量,例如位于所有函数之外的全局变量:int val=100。需要强调的是,以上两段都是位于程序的可执行文件中,内核在调用exec函数启动该程序时从源程序文件中读入。当然也少不了静态变量。
(3)未初始化数据段(.bss)。位于这一段中的数据,内核在执行该程序前,将其初始化为0或者null。例如出现在任何函数之外的全局变量:int sum;
(4)堆(Heap)。这个段用于在程序中进行动态内存申请,例如经常用到的malloc,new系列函数就是从这个段中申请内存。
(5)栈(Stack)。函数中的局部变量以及在函数调用过程中产生的临时变量都保存在此段中。
4. 内存检查的原理
Memcheck检测内存问题的原理如下图所示:
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,如果该值尚未初始化,则会报告使用未初始化内存错误。
5.valgrind的使用
为了使valgrind发现的错误更精确,如能够定位到源代码行,建议在编译时加上-g参数,编译优化选项请选择O0,虽然这会降低程序的执行效率。
1.利用valgrind调试内存问题,不需要重新编译源程序,它的输入就是二进制的可执行程序。调用Valgrind的通用格式是:valgrind [valgrind-options] your-prog [your-prog-options]
2.Valgrind 的参数分为两类,一类是 core 的参数,它对所有的工具都适用;另外一类就是具体某个工具如 memcheck 的参数。Valgrind 默认的工具就是 memcheck,也可以通过“--tool=tool name”指定其他的工具。Valgrind 提供了大量的参数满足你特定的调试需求,具体可参考其用户手册。
1:未释放内存
#include<stdio.h>
#include<stdlib.h>
int main()
{
int *array = malloc(sizeof(int));
return 0;
}
执行命令:valgrind --tool=memcheck --leak-check=full ./1
1.左边显示类似行号的数字(2847)表示的是 Process ID
2.最上面的红色方框表示的是 valgrind 的版本信息
3.中间的红色方框表示 valgrind 通过运行被测试程序,发现的内存问题。通过阅读这些信息,可以发现:
发生错误时的函数堆栈,以及具体的源代码行号。
非法写操作的具体地址空间。
4.最下面的红色方框是对发现的内存问题和内存泄露问题的总结。内存泄露的大小(4 bytes)也能够被检测出来。
2.内存越界
#include<stdio.h>
#include<stdlib.h>
int main()
{
int i;
int len = 4;
int *pt = (int*)malloc(len*sizeof(int));
int *p = pt;
for(i=0;i<len;i++)
{
p++;
}
*p = 5;
return 0;
}
执行命令:valgrind ./2
==2918== Invalid write of size 4
==2918== at 0x400570: main (2.c:16)
==2918== Address 0x5204050 is 0 bytes after a block of size 16 alloc'd
==2918== at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2918== by 0x400545: main (2.c:8)
我们发现valgrind准确的定位了问题,在16行进行非法的写操作。
3.内存覆盖
C 语言的强大和可怕之处在于其可以直接操作内存,C 标准库中提供了大量这样的函数,比如 strcpy, strncpy, memcpy, strcat 等,这些函数有一个共同的特点就是需要设置源地址 (src),和目标地址(dst),src 和 dst 指向的地址不能发生重叠,否则结果将不可预期。
下面就是一个 src 和 dst 发生重叠的例子。在 15中,src 和 dst 所指向的地址相差 20,但指定的拷贝长度却是 21,这样就会把之前的拷贝值覆盖。
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
int main()
{
char x[50];
int i;
for(i=0;i<50;i++)
{
x[i] = i+1;
}
strncpy(x+20,x,20);
strncpy(x+20,x,21);
return 0;
}
2976 Source and destination overlap in strncpy(0xfff0003f9, 0xfff0003e5, 21)
2976 at 0x4C31626: __strncpy_sse2_unaligned (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
2976 by 0x400608: main (3.c:15)
输出结果显示上述程序中第15,源地址和目标地址设置出现重叠。准确的发现了上述问题。
总结:由此可知,valgrind是一款非常强大的内存泄漏检测工具,在我们的项目和学习中有很大的作用,尤其是从事C/C++开发人员。