一、前言
上面我们将了很多 内存调试优化 的方法,但这些方法都是需要手动加入代码来实现。事实上,已经有很多成熟的工具可以让我们直接调试代码,不需要在代码中加入其它的调试代码。 valgrind 就是用于调试内存的一款工具。
二、valgrind
valgrind 的使用一般为:valgrind [选项] [程序路径] [程序选项]
2.1 工具分类
valgind 是一个工具集,它里面包含了许多个工具,每个工具的作用都不同。下面我们简单说一下:
- memcheck:用于探测程序中内存管理存在的问题。它检查所有对内存的读/写操作,并截取所有的 malloc/free 调用。其能够探测到以下问题:
- 使用未初始化的内存
- 读/写已经被释放的内存
- 读/写内存越界
- 读/写不恰当的内存栈空间
- 内存泄漏
- 使用 malloc 和 free 不匹配。
注意:memcheck 加入代码检查每一 片内存的访问和进行值计算,代码大小至少增加 12 倍,运行速度要比平时慢 25 到 50 倍。
cachegrind:这是一个 cache 剖析器。它模拟执行 CPU 中的 L1, D1 和 L2 cache,因此它能很精确的指出代码中的 cache 未命中。它可以显示出 cache 未命中的次数,内存引 用和发生 cache 未命中的每一行代码,每一个函数,每一个模块和整个程序的摘要。甚至可以显示出每一行机器码的未命中次数。
helgrind:该工具可以查找多线程程序中的竞争数据。那些被多于一条线程访问的内存地址,但是没有使用一致的锁就会被查出,即表示这些地址在多线程间访问的时候没有进行同步,很容易引发问题。
callgrind:该工具收集程序运行时的一些数据,函数调用关系等信息,还可以有选择地进行 cache 模拟。在运行结束时,它会把分析数据写入一个文件。可以通过另外一句工具把这个文件的内容转化成图像化的形式。
本文主要讲述 memcheck,它也是使用最广泛的工具之一了
2.2 选项
在使用 valgrind 之前先看看它的部分常用选项,这样理解起来更加容易
-
基本选项:该选项对于任何工具都可以使用
- -tool=
:指定 valgrind 使用的工具。默认为 memcheck。 - –help: 显示帮助信息。
- -version:显示 valgrind 内核的版本,其中各个工具都有各自的版本。
- –quiet:只打印错误信息。
- –verbose:显示更加详细的信息, 增加错误数统计。
- -trace-children=no|yes:指定是否跟踪子线程,默认为 no。
- -track-fds=no|yes:指定是否跟踪打开的文件描述,默认为 no。
- -time-stamp=no|yes:指定是否增加 时间戳 到 log,默认为 no。
- -log-fd=
:指定 log 输出到的描述符文件,默认为 stderr。 - -log-file=
:将输出的信息写入到文件,该文件的文件名为 filename.PID,PID 为跟踪的进程号。 - -log-file-exactly=
:指定输出 log 到的文件,该文件的文件名不加 PID。 - -log-file-qualifier=:指定某个环境变量的值来做为输出信息的文件名。
- -log-socket=ipaddr:port:指定输出 log 到 socket ,其网络地址为 ipaddr:port。
- -tool=
-
进阶选项:该选项对于任何工具都可以使用
- -num-callers=
:指定调用者的栈回溯信息的数量。 - -error-limit=no|yes:如果太多错误,则停止显示新错误默认为 yes。
- -error-exitcode=
:如果发现错误则返回错误代码,如果 number = 0 则是关闭该功能。 - -db-attach=no|yes:当出现错误,valgrind会自动启动调试器gdb。默认为 no。
- -db-command=
:启动调试器的命令行选项。
- -num-callers=
- 适用于Memcheck工具的相关选项:该选项仅对 Memcheck 有效。
- -leak-check=no|summary|full:指定是否对 内存泄露 给出详细信息
- -show-reachable=no|yes 指定是否显示在 内存泄漏检查 中可以检测到的块。默认为 no。
- -xml=yes:将 log 以 xml 格式输出,只有 memcheck 可用。
2.3 memcheck
2.3.1. 使用方法
下面以一个例程来讲述,使用 memcheck工具 对下面这段代码进行分析:
#include
#include
int main(int argc, char *argv[])
{
char *ptr;
ptr = (char*) malloc(10);
strcpy(ptr, "01234567890");
return 0;
}
如下结果如下:
2.3.2. log分析
2.3.2.1 内存泄露类型
- definitely lost:指确定泄露的内存,应尽快修复。当程序结束时如果一块动态分配的内存没有被释放且通过程序内的指针变量均无法访问这块内存则会报这个错误。
- indirectly lost:指间接泄露的内存,其总是与 definitely lost 一起出现,只要修复 definitely lost 即可恢复。当使用了含有指针成员的类或结构时可能会报这个错误
- possibly lost:指可能泄露的内存,大多数情况下应视为与 definitely lost 一样需要尽快修复。当程序结束时如果一块动态分配的内存没有被释放且通过程序内的指针变量均无法访问这块内存的起始地址,但可以访问其中的某一部分数据,则会报这个错误。
- still reachable:指可以访问但也未释放的内存。它可能不会造成程序崩溃,但长时间运行有可能小号完系统资源。
- suppressed:已被解决,出现了内存泄露但系统自动处理了。一般可以忽略该报告
2.3.2.2 报告格式
问题报告模板如下,该模板一般可以用在大部分的 log 中,包括 内存泄露、非法访问 等等
{问题描述}
at {地址、函数名、模块或代码行}
by {地址、函数名、代码行}
by ...{逐层依次显示调用堆栈,格式同上}
Address 0xXXXXXXXX {描述地址的相对关系}
报告输出文档格式 如下:
- copyright 版权声明
- 异常读写报告
2.1 主线程异常读写
2.2 线程A异常读写报告
2.3 线程B异常读写报告
2... 其他线程- 堆内存泄露报告
3.1 堆内存使用情况概述(HEAP SUMMARY)
3.2 确信的内存泄露报告(definitely lost)
3.3 可疑内存操作报告 (show-reachable=no关闭)
3.4 泄露情况概述(LEAK SUMMARY)
结合上面所说,我们拆分前面的例程报告,分析如下:
2.2.1 第一处如下所示:
- 问题:无效的 4 字节写入
- 代码行:例程第 8 行
我们看看代码内容:
strcpy(ptr, "01234567890");
可以发现这里 越界访问了,ptr 指向的内存块只有 10字节,而字符串一共有 12字节。
2.2.2 第二处如下所示:
- 问题:10 字节的内存块确定泄露了
- 代码行:例程第 7 行
这个再简单不过了,我们在第 7 行开辟了一块内存没有释放掉。
以上的例子只是让读者们理解 valgrind 的报告格式,还有很多种报告这里就不一一描述了。我们知道如何去看懂 valgrind 的报告格式,针对具体问题看 log 就好分析多了。
2.4 callgrind
callgrind 是一款可以对 程序的调用关系 及 运行时间 进行统计的工具。同时可以使用 gprof2dot 和 graphviz 对统计结果生成可视化的图像
2.4.1 使用准备
在使用前我们先使用下面的命令安装 gprof2dot。gprof2dot 是 python 脚本,支持将统计结果转换为 dot文件。
pip install gprof2dot
然后再使用下面的命令安装 graphviz,这样我们就可以使用 dot 工具来获取可视化的结果
sudo apt-get install graphviz
2.4.2 工具说明
2.4.2.1 callgrind
- 选项:常用的 callgrind选项 如下:
- --instr-atstart:指定是否从程序头开始模拟和分析。在 main函数 之前还有许多步骤要执行,该选项则是选择是否从这些步骤开始统计。默认为 yes。A指定是否希望Callgrind从程序开头开始模拟和分析。
- --callgrind-out-file:指定数据的输出文件,而不是默认输出文件 callgrind.out.
。 - *--dump-line:指定使用以 源码行 为粒度来执行事件计数,需要在编译时加 -g选项。默认为 yes。
- --dump-instr:指定使用以 指令 粒度来执行事件计数。结果只能在 KCachegrind 中显示,默认为 no。
- --separate-threads:指定每个线程单独生成配置文件。如果配置,则 文件名 将附加 -threadID。默认为 no。
2.4.2.2 gprof2dot
1. 输出图像格式
在说明 gprof2dot 的选项之前,先需要知道 gprof2dot 输出图像格式。先看一个比较简单的例子,如下所示,:
输出分为:
-
节点:如图所示,节点 是一个一个的 方框。
- 总时间占比:指该函数及其所有调用函数所花费的运行时间的百分比。
- 自运行时间占比:指仅在此函数上花费的运行时间的百分比。
- 总调用次数:指调用该函数的总次数,包括递归调用。
-
边:如果所示,边 是一个 箭头。
- 总时间占比:指该 边指向的 子函数 所占运行时间的百分比。
- 调用次数: 指该 边 指向是 子函数 被 父函数 调用的总次数。
一般情况,每个 节点及其边 有关系如下:
节点总时间比 = 自运行时间占比 + 所有的边总时间占比
节点和边 的颜色会根据 总时间占比 而变化。将花费 最多时间 的函数标记为红色,将花费 很少时间 的函数标记为 深蓝色。
2. 工具选项
- -h:显示帮助信息。
- -o:指定 输出文件。
- -n:指定输出节点的阈值,按百分比算。比如 -n5 表示 运行时间 占比超过 5% 的 函数 将作为节点 输出。
- -e: 指定输出节点的边,按百分比算。比如 -e5 表示 运行时间 占比超过 5% 的 函数调用 将作为 边 输出。
- -f:指定输入文件的格式
- -s:指定去掉函数参数名,模板参数名等
- --root:指定某个函数作为 根函数。只显示根函数以下的调用关系
- --leaf:指定某个函数作为 叶子函数。只显示叶子函数以上的调用关系
2.4.3 例程
代码如下:
#include
#include
#include
void f1(){
int* p = NULL;
for(int i = 0; i < 10000; i ++)
{
p = malloc(sizeof(int));
free(p);
}
}
void f2(){
int* p = NULL;
for(int i = 0; i < 10000; i ++)
{
p = malloc(sizeof(int));
free(p);
}
}
void func3() {
int* p = NULL;
for(int i = 0; i < 10000; i ++)
{
p = malloc(sizeof(int));
free(p);
}
}
int main() {
f1();
f2();
func3();
return 0;
}
注意:在编译代码时似乎需要加入 -g 选项。
将执行文件拷贝到运行设备上,使用以下指令:
./valgrind/bin/valgrind --tool=callgrind your_file
运行结果如下:
其中 106 就是运行时进程的 PID,同时会生成文件 callgrind.out.PID(PID根据程序运行时给出而定)。将 callgrind.out.PID 拷贝到宿主机,使用以下的命令生成 dot文件:
sudo gprof2dot -f callgrind -n5 -e0 --root=main ./callgrind.out.106 > valgrind.dot
最后使用下面的命令将 dot文件 转换为图像:
sudo dot -Tpng valgrind.dot -o valgrind.png
结果如图所示:
这里有个问题,虽然节点的各个数据显示都是正常的,但是其调用关系是错误。从代码可以看出 3 个函数是平行的调用关系,但图表示的是 func3 是 f2 的子函数,f2 是 f1 的子函数,这里笔者不甚了解,还请知道的读者不吝赐教
我们还可以使用一个工具来获取每一行代码的 指令数量。在一定程度上,指令数量越多需要执行的时间就越长,优化指令数量可以有效提高程序速度。
我们使用以下的命令:
sudo callgrind_annotate your_callgrind.out your_sourcefile
运行结果如下:
其中 Ir 这一列表示的就是指令数量,我们可以看到每一行代码都有其对应的指令数量,这样我们也可以到明显需要优化的地方
三、参考链接
- gprof2dot的github仓库:https://github.com/jrfonseca/gprof2dot
- valgrind arm-linux 交叉编译:https://www.cnblogs.com/CodingTheFuture/p/9864960.html
- Valgrind学习总结:https://blog.csdn.net/andylauren/article/details/93189740
- DEBUG神器valgrind之memcheck报告分析:https://blog.csdn.net/jinzeyu_cn/article/details/45969877
- valgrind的使用与输出结果分析:https://www.cnblogs.com/kuangsyx/p/8043526.html
- 笨办法学Chttps://www.jianshu.com/p/1e423e3f5ed5
- valgrind用户手册:http://valgrind.org/docs/manual/manual.html