Linux内存问题排查神器:Valgrind使用指南

检测维修 0 113

怎样去定位应用程序开发里的内存问题这一情况, 始终是inux应用程序开发当中的瓶颈之处。存在一款极为出色的linux下开源的内存问题检测工具, 它就是valgrind, 它能够极大程度地对你解决上述提及的问题起到帮助作用。掌握valgrind的使用方式以及它的工作原理, 能够有效地定位进而防止应用开发中的内存问题。

应用 Valgrind 发现 Linux 程序的内存问题

Valgrind 概述

体系结构

Valgrind是在Linux下的一套仿真调试工具集合, 其开放源代码遵循GPL V2, 它由内核以及基于内核的其他调试工具构成, 内核类似某个框架, 这个框架能模拟CPU环境, 还会给其他工具提供服务, 而其他工具类似插件, 凭借内核给出的服务去完成各类特定的内存调试任务, Valgrind的体系结构呈现如下面图形所示:

图 1 Valgrind 体系结构

Valgrind包括如下一些工具:

Memcheck, 它是valgrind应用最为广泛的那个工具, 是一个重量级的内存检查器, 能发现开发里绝大多数内存错误的使用情形, 像举例的使用未初始化的内存, 使用已经释放了的内存, 还有内存访问越界等情况, 这同样也是本文会重点介绍的那一部分。Callgrind, 它主要是用来检查程序当中函数调用过程期间出现的问题。Cachegrind, 它主要是用来检查程序内里缓存使用出现的问题。Helgrind, 它主要是用来检查多线程程序之中出现的竞争问题。玛西夫。其主要针对程序里堆栈启用里浮现的问题予以查看。扩展名。能够借助核心所赋予的功能, 自行编撰特定的内存验证工具。

Linux 程序内存空间布局

倘若要去发觉Linux环境之下的内存相关问题, 那么首先绝对得清楚在Linux环境当中, 内存究竟是怎样被进行分配的? 紧接着, 如下的图呈现出了一种典型的Linux C程序内存空间布局情况:

图 2: 典型内存空间布局

一个典型的Linux C程序内存空间由如下几部分组成:

内存检查原理

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

图 3 内存检查原理

检测出内存问题的 Memcheck, 其关键之处在于, 它建立了两个全局表。

Valid-Value 表:

针对进程的整个地址空间之内的每一个字节, 也就是 byte, 会有与之相对应的 8 个 bits;针对 CPU 的每个寄存器, 同样存在一个与之对应的 bit 向量。这些 bits 承担着记录该字节或者寄存器值是不是具有有效的、已初始化的值这样的职责。

Valid-Address 表

进程整个地址空间每一个字节, 都有和其对应的 1 个 bit, 用来记录该地址能不能被读写, 对这每一个字节而言都是如此。

检测原理:

Valgrind 使用

第一步:准备好程序

为了让valgrind所发现的错误变得更为精确起来, 像是可以定位到源代码行那样, 建议在编译的时候加上-g参数, 编译优化选项要选择O0, 尽管这样会使得程序的执行效率降低。

这里所运用到的示例程序文件, 其名字是sample.c, 呈现情况如下所示, 而选用起来的编译器是gcc。

生成能够被执行的程序, 使用gcc, 带有-g选项, 带有-O0选项, 对sample.c文件进行操作, 最终输出为sample。

清单 1

第二步:在valgrind下,运行可执行程序。

运用valgrind去调试内存方面的问题, 无需再次编译源程序, 它的输入乃是二进制的可执行程序。调用Valgrind的通用格式为: valgrind。

valgrind-options

your-prog

your-prog-options

Valgrind的参数被划分成两类, 其中一类是core的参数, 该参数对全部工具均适用, 另一类便是具体到某个工具类似memcheck的参数, Valgrind默认的工具是memcheck, 也能够借助“--tool=tool name”来指定别的工具, Valgrind给出了大量参数用以满足你特定的调试诉求, 具体能参照其用户手册。

此个例子会运用memcheck, 因而能够输入命令如下: valgrind。

/sample.

第三步:分析 valgrind 的输出信息。

以下是运行上述命令后的输出。

清单 2

示例程序明显存在着两个方面的问题, 一方面是, 在fun函数里头, 动态申请的堆内存, 并未进行释放;另一方面是, 对于堆内存, 出现了访问越界的情况。而这两个问题, 都被valgrind给发现了。

利用Memcheck发现常见的内存问题

进行Linux平台之上应用程序开发之际, 最为经常碰到的问题是内存使用有误, 我们归纳了常见的内存错误使用情形, 并且阐述了怎么借由valgrind把它检测出来。

使用未初始化的内存

问题分析:

于程序里不同段处的变量而言, 其初始值各异, 全局变量以及静态变量的初始值是0, 然而局部变量与动态申请的变量, 其初始值是随机值。要是程序运用了具随机值的变量, 那么程序的行为就变得不可预期了。

下述的那一套程序, 算得上是一种常常能见到的, 运用了并未初始化的变量的状况了。数组a它可是属于局部变量, 该局部变量的初始值, 是随机出来的值。然而, 在初始化的这个时候, 并没有给此数组的所有数组成员都进行初始化操作。像这样的话, 在紧接着后续使用这个数组之际, 就潜在地存在着内存方面的问题。

清单 3

结果分析:

Linux 应用程序内存问题检测_Valgrind Memcheck 使用教程_linux中的常用内存问题检测工具

假定这个文件的名称是: badloop.c, 所生成的可执行程序叫作badloop, 运用memcheck针对它展开测试, 输出的情况如下。

清单 4

显示出的输出结果表明, 于该程序的第11行里, 程序的跳转是依靠一个未曾初始化的变量。精准地察觉到了上述程序之中所存在的问题。

内存读写越界

问题分析:

指这样一种情况, 即访问了不该访问、没有权限访问的内存地址空间, 例如在访问数组时出现越界情况, 又比如在对动态内存进行访问之际超出了所申请的内存大小范围。下面的程序乃是一个具备典型性的数组越界问题。pt属于一个局部数组变量, 其大小设定为4, p起初指向pt数组的起始地址, 然而在对p进行循环叠加之后, p超出了pt数组的范围, 要是在此时再针对p展开写操作, 那么后果将会难以预期。

清单 5

结果分析:

倘若这个文件的名字是 badacc.cpp, 所生成的可执行程序称作 badacc, 运用 memcheck 对它开展测试, 得出的输出是这样的。

清单 6

程序运行输出结果表明, 于该程序的第15行之处, 开展了不符合规定的写行为;在第16行那里, 实施了不符合规定的读行为。上述这些问题被精准无误地发觉了。

内存覆盖

问题分析:

C语言有着强大且可怕之地, 在于它能够直接对内存进行操作, C标准库当中给出了大量这般的函数, 像strcpy、strncpy、memcpy还有strcat等等, 这些函数具备着同一个特点, 那就是要设置源地址简称src, 以及目标地址简称dst, src与dst所指向的地址绝不能够出现重叠的情况, 不然结果将会令人难以预期。

下面存在一个 src 跟 dst 出现重叠的实例, 于 15 行以及 17 行里, src 跟 dst 所指向的地址相互间相差 20, 然而所指定的拷贝长度却是 21, 如此一来便会将之前的拷贝值予以覆盖, 在第 24 行程序也是类似的情况: src(x + 20) 同 dst(x) 此二者所指向的地址相差 20, 不过 dst 的长度为 21, 这样同样会发生内存覆盖哟。

清单 7

结果分析:

假如这个文件的名字是 badlap.cpp, 所生成的可执行程序是 badlap, 运用 memcheck 针对它开展测试, 得出的输出是这样的。

清单 8

输出结果呈现出, 在上述程序里的第15行, 以及第17行, 还有第24行, 源地址与目标地址的设置出现了重叠, 确切地发现了上述那个问题。

动态内存管理错误

问题分析:

有一种内存分配方式, 它被划分成了三种类型, 分别是静态存储类型, 栈上分配类型, 堆上分配类型。具有这种静态存储属性的, 可以界定为全局变量, 它们具备的特点, 是在编译这个环节就已经被赋予了存储空间得以分配。函数内部所含有的具备栈上分配属性的, 则是局部变量。相比于前两者具备更强灵活性特色特征, 表示内存使用方式的, 就是堆上分配类型了, 也可称作我们所说的内存动态分配范畴。内存中进行动态分配存在一些常用函数, 比如malloc函数, alloc函数, realloc函数, new函数等, 而在内存释放环节得以动态处理的函数, 涵盖free函数, delete函数。

只要成功申请了动态内存, 那我们就得自己去做内存管理, 可这恰恰是极易出错的。接下来的一段程序,涵盖了内存动态管理里常见的错误。

清单 9

常见的内存动态管理错误包括:

于C++因兼容C, 且C同C++在内存申请和释放函数方面有差异, 所以于C++程序里面, 存在着两套用于动态内存管理的函数, 一套是专门采用C方式申请其内存的, 另一套特定是针对C++方式申请的内存的。一条始终不会改变的规则阐明的是借由C方式去申请获取的内存, 要把它采取C方式去实现释放操作;使用C++方式去申请获得的内存, 需要动用C++方式来完成释放处理。换句话讲就是以malloc这样的方式, 或者alloc这种方式, 又或者realloc此样式申请得到的内存, 采用free进行释放;经由new方式去申请获得的内存运用delete去完成释放。处于上述的程序里, 是以 malloc 这种方式去申请了内存, 之后却运用 delete 来把它释放了, 虽说这样子在多数的情况下是不会出现什么问题的, 然而这绝对切实是那种潜在着的问题。

所申请的内存数量, 在使用完毕之后便要释放掉相同数量的内存。要是没有进行释放, 又或者释放的数量不足那便是内存泄露;而倘若释放的数量过多同样会引发问题。在上述提及的程序里, 指针p以及pt所指向的乃是同一块内存, 然而却先后被释放了两次。

从本质来讲, 系统会于堆之上维护一个动态内存链表, 一旦被释放, 那就意味着该块内存能够接着被分配给其他部分, 要是内存被释放之后再去访问, 便兴许会覆盖其他部分的信息, 这可是一种相当严重的错误,下述程序的第16行当中就在释放后背仍然去写这块内存。

结果分析:

假设, 这个文件名是badmac.cpp, 所生成的可执行程序为badmac, 运用memcheck对它进行测试, 输出了这些内容。

清单 10

输出的结果那里呈现出, 第14行的那个分配以及释放函数并非是一样的;第16行出现了非法的写操作这种情况, 也就是朝着已然释放掉的内存地址去写入数值;第17行的释放内存函数显得没有效用。确切地将上述的那三个问题给发觉了。

内存泄露

问题描述:

内存泄露也就是Memory leak, 它所指的是, 程序里动态申请的内存, 在使用完毕之后, 既没有进行释放, 与此同时又不能够被程序的其他部分所访问。在开发大型程序期间, 是最让人头疼的问题, 以至于有为数不多的人说过, 内存泄露是难以避免的。但实际上并非如此, 起到防止内存泄露作用, 要从具良好的编程习惯开始做起的, 另外非常重要的一点在于, 要强化单元测试单位也就是Unit Test, 而当中就有着比如memcheck这样一款特别优秀的工具。

以下这样一个案例, 它是较为典型的内存泄露方面例子, main函数进行了mk函数的调用, 目的是制成树结点, 然而在调用结束这件事情达成之后, 却不存在对应的那个函数, 即释放内存那般的nodefr函数, 如此这般处于此内存当中现存的这个树的结构样态就不能够被其他部分予以访问, 进而导致了内存出现泄露的情况以及结果。

在单独的一个函数里头, 每个人针对于内存泄露的意识, 那都是相对说来比较强的。然而, 在不少的情形之下, 我们往往会针对malloc/free或者new/delete去做某些样子的包装, 目的是契合我们特定的需求, 没办法达成在一个函数当中, 既去使用又予以释放。这个实例, 同样也表明了内存泄露最容易出现状况的地方, 也就是: 两个部分相互交接的接口部分, 有一个函数是负责申请内存的, 另外一个函数是负责释放内存的。并且, 这些函数是由不同的人员去开发, 然后被不同的人员使用的, 如此一来, 造成内存泄露的可能性就会比较大了。这需要养成良好的单元测试习惯,将内存泄露消灭在初始阶段。

清单 11

清单 11.2

清单 11.3

结果分析:

假定上述文件名称是tree.h, tree.cpp, badleak.cpp, 被生成的可执行程序是badleak, 运用memcheck针对它开展测试, 输出情况如下。

清单 12

是生成一棵树的过程的该示例程序, 每个树节点大小是12(考虑内存对齐这一情况), 一共有8个节点。能看出所有的内存泄露都被发现了, 从上述输出当中。分为两种的是Memcheck将内存泄露, 一种是可能的内存泄露(Possibly lost), 另一种是确定的内存泄露(Definitely lost)。Possibly lost指的是仍存在某指针可访问某块内存, 不过该指针所指向的已并非这内存的首地址。明确丢失是讲已经没办法去访问这块内存了。而明确丢失又划分成两种情况: 直接的情形, 间接的情形。直接情形与间接情形的差异在于, 直接情形是不存在任何指针指向该内存, 间接情形是说指向该内存的指针都处在内存泄露的地方。在上面所举的例子里, 根节点属于直接丢失, 而其他节点属于间接丢失。

总结

这篇文章对valgrind的体系结构做了介绍, 着重介绍了其中应用最为广泛的工具, 也就是memcheck。它阐述了memcheck找出内存问题的基本原理, 基本使用方法, 还有借助memcheck怎样发现当下开发里最为广泛的五大类内存问题。在项目当中尽早地发现内存问题, 能够极大限度地提升开发效率, valgrind就是有助于你达成这一目标的出色工具。

考资料

相关推荐: