欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 旅游 > 深入内存调试:Valgrind工具的终极指南(转)

深入内存调试:Valgrind工具的终极指南(转)

2025/2/24 7:45:25 来源:https://blog.csdn.net/oushaojun2/article/details/145715018  浏览:    关键词:深入内存调试:Valgrind工具的终极指南(转)

在软件开发的世界里,代码质量就是生命线,而内存管理又是这条生命线中最脆弱的一环。内存泄漏,哪怕只是微小的一处,日积月累,都可能对整个系统造成灾难性的打击,无论是大型企业级应用、实时性要求极高的嵌入式系统,还是对性能锱铢必较的游戏开发。此时,掌握一款强大的内存检测工具至关重要,Valgrind 便是这样的利器。它以其精准、全面的检测能力,成为众多开发者捍卫代码质量的 “秘密武器”。今天,我们就深入探索 Valgrind,看看它如何帮助我们规避内存泄漏,打造坚如磐石的代码。

一、Valgrind 是什么?

在编程的世界里,代码就像是一座宏伟的建筑,而内存管理则是这座建筑的基石。一个小小的内存错误,可能就会引发程序崩溃、数据丢失等灾难性后果。这时候,Valgrind 就像是一位专业的建筑质检员,能够帮助我们找出代码中的内存问题,确保程序的稳定性和可靠性。

Valgrind 是一款用于内存调试、内存泄漏检测和性能分析的软件开发工具,堪称程序员的得力助手。它最初由 Julian Seward 设计,2006 年因其在 Linux x86 平台上的免费内存调试工具上的卓越贡献,荣获第二届 Google-O'Reilly 开放源码奖,并且遵循 GNU 通用公共许可证,是自由软件中的明星产品。

对于 C/C++ 程序员来说,Valgrind 更是不可或缺。在 C/C++ 编程中,内存的分配与释放需要程序员手动管理,稍有不慎就会出现各种问题,比如使用未初始化的内存、内存泄漏、越界访问等。这些问题往往难以察觉,可能在程序运行一段时间后才突然爆发,给调试带来极大的困难。而 Valgrind 就如同一个敏锐的侦探,能够精准地发现这些隐藏的内存 “陷阱”,让我们及时修复问题,避免程序在关键时刻 “掉链子”。

Valgrind的体系结构以下图所示:

二、Valgrind 的强大功能

Valgrind 之所以如此强大,是因为它包含了一系列各有所长的工具,就像一个多功能的瑞士军刀,能够从不同角度剖析我们的程序。接下来,让我们深入了解一下这些工具的独特魅力。

⑴Memcheck:内存问题的 “放大镜”

Memcheck 是 Valgrind 中当之无愧的明星工具,也是使用最为广泛的一个。它就像一个高倍放大镜,能够精准地检测出程序中各种各样的内存问题。无论是使用未初始化的内存、读写已释放的内存块,还是数组下标越界、内存泄漏等,统统都逃不过它的 “法眼”。在开发过程中,这些内存问题往往隐藏得很深,可能在特定的条件下才会暴露出来,给调试带来极大的困扰。而 Memcheck 能够在程序运行时实时监测内存的使用情况,一旦发现问题,立即给出详细的错误报告,包括错误发生的位置、涉及的内存地址等信息,让我们能够迅速定位并修复问题。

因此,它能检测如下问题:

  • 未初始化内存的使用;
  • 读/写释放后的内存块;
  • 读/写超出malloc分配的内存块;
  • 读/写不适当的栈中内存块;
  • 内存泄漏,指向一块内存的指针永远丢失;
  • 不正确的malloc/free或new/delete匹配;
  • memcpy()相关函数中的dst和src指针重叠。

⑵Cachegrind:优化缓存的 “指南针”

Cachegrind 则专注于程序的缓存使用情况,为我们优化程序性能提供了有力的支持。它就像是一个精准的指南针,能够帮助我们找到代码中与缓存相关的问题,指引我们优化的方向。在现代计算机体系结构中,CPU 的速度远远快于内存的速度,缓存的作用就显得尤为重要。如果程序不能有效地利用缓存,频繁地从内存中读取数据,就会导致 CPU 等待,从而大大降低程序的性能。Cachegrind 通过模拟 CPU 的缓存行为,详细地统计缓存的命中和未命中情况,为我们提供诸如指令计数、缓存未命中次数、内存引用次数等关键信息。这些信息就像是宝藏地图上的标记,让我们能够清楚地看到程序中哪些部分的缓存利用率不高,进而针对性地进行优化,比如调整数据结构、优化算法,以提高缓存命中率,提升程序的运行速度。

⑶Callgrind:函数调用的 “透视镜”

Callgrind 主要用于分析程序中函数的调用过程,如同一个透视镜,让函数调用的细节一览无余。它能够收集函数调用的相关数据,建立起函数调用关系图,清晰地展示出各个函数之间的调用层次和频率。这对于理解程序的执行流程、发现潜在的性能瓶颈非常有帮助。在大型项目中,函数之间的调用关系错综复杂,很难直观地看出哪些函数的调用开销较大,哪些函数被频繁调用但实际上可以进行优化。Callgrind 不仅可以告诉我们这些信息,还能提供每个函数执行的指令数、缓存使用情况等详细数据。通过分析这些数据,我们可以找出那些占用大量 CPU 资源的 “热点” 函数,对它们进行优化,比如减少不必要的函数调用、优化函数内部的算法,从而提升整个程序的性能。

⑷Helgrind:多线程程序的 “守护者”

在多线程编程的世界里,线程之间的同步与竞争问题就像是隐藏在暗处的 “幽灵”,随时可能导致程序出现难以捉摸的错误。Helgrind 就是专门用来驱赶这些 “幽灵” 的 “守护者”。它致力于检查多线程程序中出现的竞争问题,通过先进的算法,仔细监测内存中被多个线程访问的区域,一旦发现没有正确加锁或同步的情况,就会及时发出警报。这些竞争问题往往会导致程序出现死锁、数据不一致等严重错误,而且由于它们的出现具有不确定性,很难通过常规的调试手段发现。Helgrind 的出现,为多线程程序的调试带来了极大的便利,让我们能够提前发现并解决这些潜在的问题,确保多线程程序的正确性和稳定性。

⑸Massif:内存使用的 “分析师”

Massif 是一位专业的 “分析师”,专注于程序的堆栈内存使用情况。它能够精确地测量程序在运行过程中堆栈内存的使用量,详细地告诉我们堆块、堆管理块和栈的大小,以及内存的分配和释放情况。对于那些需要严格控制内存使用的程序,比如嵌入式系统开发、服务器端程序等,Massif 的作用尤为突出。通过它提供的信息,我们可以深入了解程序的内存使用行为,发现内存泄漏、内存过度分配等问题,并进行针对性的优化。例如,我们可以根据 Massif 的报告,调整数据结构的大小、优化内存分配策略,以减少内存的占用,提高程序的运行效率,避免因内存不足而导致的程序崩溃或性能下降。

三、安装与配置 Valgrind

3.1不同系统下的安装方法

安装 Valgrind 其实并不复杂,不过不同的操作系统下,安装方式还是略有差异的。下面,我就来给大家详细介绍一下。

在 Linux 系统下,安装 Valgrind 就像是一场轻松的旅行。以常见的 Ubuntu 系统为例,我们只需打开终端,输入以下几条命令:

sudo apt-get update
sudo apt-get install valgrind

简单几步,就能轻松搞定安装,是不是超级方便?这就好比在应用商店里一键下载安装软件一样便捷,让你快速拥有这款强大的工具。

对于 Windows 用户来说,由于 Valgrind 本身是基于 Linux 开发的,所以不能直接在 Windows 上安装原生版本。不过别担心,我们可以借助 Windows 下的 Linux 子系统(WSL)来使用它。首先,按照微软官方的教程安装 WSL,安装完成后,在 WSL 的终端中,使用和 Linux 系统下类似的命令安装 Valgrind。就像是在 Windows 系统里开辟了一块 “Linux 小天地”,让 Valgrind 在其中顺畅运行,为我们的 Windows 编程保驾护航。

Mac 用户也有自己的安装方式。我们可以使用 Homebrew 这个强大的包管理器来安装 Valgrind,只需在终端中输入:

brew install valgrind

这就像是用一把万能钥匙打开了软件安装的大门,Homebrew 会自动帮我们处理好所有的依赖关系,轻松完成安装,让我们在 Mac 上也能尽情享受 Valgrind 带来的便利。

3.2配置要点

安装好 Valgrind 后,还需要进行一些简单的配置,才能让它更好地发挥作用。在编译我们的程序时,记得要打开调试模式,这就像是给程序戴上了一个 “智能手环”,可以记录更多的运行信息,方便 Valgrind 进行分析。以 gcc 编译器为例,我们需要加上 “-g” 选项,像这样:

gcc -g -o myprog myprog.c

另外,为了避免编译优化影响 Valgrind 的检测结果,最好关闭编译优化选项。因为有些优化可能会改变程序的执行顺序,让 Valgrind 难以准确找到问题所在。在 gcc 中,我们可以使用 “-O0” 选项来关闭优化,就像这样:

gcc -g -O0 -o myprog myprog.c

完成这些配置后,我们就可以让 Valgrind 闪亮登场,开启代码的 “体检” 之旅啦。

3.3检测内存泄漏

终端进入可执行文件所在的文件夹,输入

valgrind --tool=memcheck 
--leak-check=full 
--show-leak-kinds=all 
--undef-value-errors=no 
--log-file=log ./可执行文件名

即可在终端所在文件夹下生成log文件,在log文件最后会有个summary,其中对内存泄露进行了分类,总共有五类:

  1. “definitely lost” 意味着你的程序一定存在内存泄露;
  2. ”indirectly lost”意味着你的程序一定存在内存泄露,并且泄露情况和指针结构相关
  3. “possibly lost” 意味着你的程序一定存在内存泄露,除非你是故意进行着不符合常规的操作,例如将指针指向某个已分配内存块的中间位置。
  4. “still reachable” 意味着你的程序可能是没问题的,但确实没有释放掉一些本可以释放的内存。这种情况是很常见的,并且通常基于合理的理由。
  5. ”suppressed” 意味着有些泄露信息被压制了。在默认的 suppression 文件中可以看到一些 suppression 相关设置。

其中,如果二叉树的根节点被判定为”definitely lost”,则其所有子节点将被判定为”indirectly lost”,而如果你正确修复了类型为 “definitely lost” 的根节点泄露,那么类型为 “indirectly lost” 的子节点泄露也会随着消失。

对于以上的情况,posslbly lost其实并没有造成内存上的影响,如果想要过滤掉该类报告信息,可以加入--show-possibly-lost=no ,而对于”still reachable” ,同样可以通过--show-reachable=yes来控制是否输出相应的信息。如果某些需要的库没有找到,用指令进行添加:

export LD_LIBRARY_PATH=/usr/local/mysql/lib:$LD_LIBRARY_PATH

查看发生泄露的具体位置

在log中由summary往上翻即可看到对应的错误,错误是不断细化的,比如:

这样的是一个错误,先告诉你出现了多少的内存泄露,然后从最里层不断往外部函数显示:先说是calloc造成的错误,然后不断往外部函数显示。可以从下往上进行查看,比如先说main()函数发生了泄露,往上看到是main()中的init()函数,再往上init()中的init_detectionmodel,如此不断细定位泄露位置。

四、Valgrind 工作原理

Memcheck 能够检测出内存问题,关键在于其建立了两个全局表。Valid-Value 表对于进程的整个地址空间中的每一个字节(byte),都有与之对应的 8 个 bits;对于CPU的每个寄存器,也有一个与之对应的bit向量。这些bits负责记录该字节或者寄存器值是否具有有效的、已初始化的值。

Valid-Address表:对于进程整个地址空间中的每一个字节(byte),还有与之对应的1个bit,负责记录该地址是否能够被读写。

检测原理:当要读写内存中某个字节时,首先检查这个字节对应的 A bit。如果该A bit显示该位置是无效位置,memcheck则报告读写错误。

内核(core)类似于一个虚拟的 CPU 环境,这样当内存中的某个字节被加载到真实的 CPU 中时,该字节对应的 V bit 也被加载到虚拟的 CPU 环境中。一旦寄存器中的值,被用来产生内存地址,或者该值能够影响程序输出,则 memcheck 会检查对应的V bits,如果该值尚未初始化,则会报告使用未初始化内存错误。

五、Valgrind 命令介绍

用法valgrind[options] prog-and-args [options]常用选项,适用于所有Valgrind工具:

  1. -tool=<name> 最常用的选项。运行 valgrind中名为toolname的工具。默认memcheck。
  2. h –help 显示帮助信息。
  3. -version 显示valgrind内核的版本,每个工具都有各自的版本。
  4. q –quiet 安静地运行,只打印错误信息。
  5. v –verbose 更详细的信息, 增加错误数统计。
  6. -trace-children=no|yes 跟踪子线程? [no]
  7. -track-fds=no|yes 跟踪打开的文件描述?[no]
  8. -time-stamp=no|yes 增加时间戳到LOG信息? [no]
  9. -log-fd=<number> 输出LOG到描述符文件 [2=stderr]
  10. -log-file=<file> 将输出的信息写入到filename.PID的文件里,PID是运行程序的进行ID
  11. -log-file-exactly=<file> 输出LOG信息到 file
  12. -log-file-qualifier=<VAR> 取得环境变量的值来做为输出信息的文件名。[none]
  13. -log-socket=ipaddr:port 输出LOG到socket ,ipaddr:port

LOG信息输出:

  1. -xml=yes 将信息以xml格式输出,只有memcheck可用
  2. -num-callers=<number> show <number> callers in stack traces [12]
  3. -error-limit=no|yes 如果太多错误,则停止显示新错误? [yes]
  4. -error-exitcode=<number> 如果发现错误则返回错误代码 [0=disable]
  5. -db-attach=no|yes 当出现错误,valgrind会自动启动调试器gdb。[no]
  6. -db-command=<command> 启动调试器的命令行选项[gdb -nw %f %p]

适用于Memcheck工具的相关选项:

  1. -leak-check=no|summary|full 要求对leak给出详细信息? [summary]
  2. -leak-resolution=low|med|high how much bt merging in leak check [low]
  3. -show-reachable=no|yes show reachable blocks in leak check? [no]

六、使用 Valgrind 进行内存调试

6.1基本命令参数解析

了解了 Valgrind 的安装和配置,下面我们就来看看如何在实战中使用它。使用 Valgrind 的基本命令格式如下:

valgrind [options] program [arguments]

其中,[options]是一系列的参数,用来控制 Valgrind 的行为,program是我们要检测的程序,[arguments]则是程序运行所需的参数。

下面,给大家介绍几个常用的参数。首先是 “--tool=memcheck”,这个参数指定使用 Memcheck 工具,它是 Valgrind 中最常用的工具,用于检测各种内存问题,如果你不确定程序具体存在哪种内存问题,使用这个参数准没错。

“--leak-check” 参数用于检测内存泄漏,它有几个可选的值,“no” 表示不检查内存泄漏,“summary” 仅显示内存泄漏的摘要信息,而 “full” 则会显示所有内存泄漏的详细信息,包括泄漏的内存位置、大小等,方便我们深入排查问题,一般在调试阶段,建议使用 “full” 模式,以获取最全面的信息。

还有 “--track-origins=yes”,这个参数非常实用,它可以帮助我们追踪未初始化内存的使用情况,让我们清楚地知道未初始化的内存是在哪里被创建的,以及在哪些地方被使用,对于找出那些因使用未初始化内存而导致的诡异问题特别有帮助。

6.2应用实践

下面通过介绍几个范例来说明如何使用Memcheck ,示例仅供参考,更多用途可在实际应用中不断探索。

⑴数组越界/内存未释放

#include<stdlib.h>
void k(void)
{
int *x = malloc(8 * sizeof(int));
x[9] = 0;              //数组下标越界
}                        //内存未释放int main(void)
{k();
return 0;
}

①编译程序test.c

gcc -Wall test.c -g -o test#Wall提示所有告警,-g gdb,-o输出

②使用Valgrind检查程序BUG

valgrind --tool=memcheck --leak-check=full ./test
#--leak-check=full 所有泄露检查

③运行结果如下:

==2989== Memcheck, a memory error detector==2989== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Sewardet al.==2989== Using Valgrind-3.8.1 and LibVEX; rerun with -h forcopyright info==2989== Command: ./test==2989====2989==  Invalid write of size 4==2989==    at 0x4004E2: k (test.c:5)==2989==    by 0x4004F2: main (test.c:10)==2989==  Address 0x4c27064 is 4 bytes after a block of size 32 alloc'd==2989==    at 0x4A06A2E: malloc (vg_replace_malloc.c:270)==2989==    by 0x4004D5: k (test.c:4)==2989==    by 0x4004F2: main (test.c:10)==2989====2989====2989== HEAP SUMMARY:==2989==    in use at exit: 32 bytes in 1 blocks==2989==  total heap usage: 1 allocs, 0 frees, 32 bytes allocated==2989====2989== 32 bytes in 1 blocks are definitely lost in loss record 1of 1==2989==    at 0x4A06A2E: malloc (vg_replace_malloc.c:270)==2989==    by 0x4004D5: k (test.c:4)==2989==    by 0x4004F2: main (test.c:10)==2989====2989== LEAK SUMMARY:==2989==    definitely lost: 32 bytes in 1 blocks==2989==    indirectly lost: 0 bytes in 0 blocks==2989==      possibly lost: 0 bytes in 0 blocks==2989==    still reachable: 0 bytes in 0 blocks==2989==suppressed: 0 bytes in 0 blocks==2989====2989== For counts of detected and suppressed errors, rerun with: -v==2989== ERROR SUMMARY: 2 errors from 2 contexts(suppressed: 6 from 6)

⑵内存释放后读写

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *p = malloc(1);    //分配
*p = 'a';
char c = *p;
printf("\n [%c]\n",c);
free(p);        //释放c = *p;        //取值
return 0;
}

①编译程序t2.c

gcc -Wall t2.c -g -o t2

②使用Valgrind检查程序BUG

valgrind --tool=memcheck --leak-check=full ./t2

③运行结果如下:

==3058== Memcheck, a memory error detector
==3058== Copyright (C) 2002-2012, and GNU GPL'd, by Julian
Seward et al.
==3058== Using Valgrind-3.8.1 and LibVEX; rerun with -h
for copyright info
==3058== Command: ./t2
==3058==[a]==3058== Invalid read of size 1==3058==    at 0x4005A3: main (t2.c:14)==3058==  Address 0x4c27040 is 0 bytes inside a block of size1 free'd==3058==    at 0x4A06430: free (vg_replace_malloc.c:446)==3058==    by 0x40059E: main (t2.c:13)==3058====3058====3058== HEAP SUMMARY:==3058==    in use at exit: 0 bytes in 0 blocks==3058==  total heap usage: 1 allocs, 1 frees, 1 bytes allocated==3058====3058== All heap blocks were freed -- no leaks are possible==3058====3058== For counts of detected and suppressed errors, rerun with:-v==3058== ERROR SUMMARY: 1 errors from 1 contexts(suppressed: 6 from 6)从上输出内容可以看到,Valgrind检测到无效的读取操作然后输出“Invalid read of size 1”。

⑶无效读写

#include <stdio.h>
#include <stdlib.h>
int main(void){char *p = malloc(1);    //分配1字节*p = 'a';char c = *(p+1);        //地址加1printf("\n [%c]\n",c); free(p);return 0;
}

①编译程序t3.c

gcc -Wall t3.c -g -o t3

②使用Valgrind检查程序BUG

valgrind --tool=memcheck --leak-check=full ./t3

③运行结果如下:

==3128== Memcheck, a memory error detector==3128== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.==3128== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info==3128== Command: ./t3==3128====3128==  Invalid read of size 1        #无效读取==3128==at 0x400579: main (t3.c:9)==3128==Address 0x4c27041 is 0 bytes after a block of size 1 alloc'd==3128==at 0x4A06A2E: malloc (vg_replace_malloc.c:270)==3128==by 0x400565: main (t3.c:6)==3128==[]
==3128==
==3128== HEAP SUMMARY:
==3128==in use at exit: 0 bytes in 0 blocks
==3128==total heap usage: 1 allocs, 1 frees, 1 bytes allocated
==3128==
==3128== All heap blocks were freed -- no leaks are possible
==3128==
==3128== For counts of detected and suppressed errors, rerun with: -v
==3128== ERROR SUMMARY: 1 errors from 1 contexts
(suppressed: 6 from 6)

⑷内存泄露

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p = malloc(1);
*p = 'x';
char c = *p;
printf("%c\n",c);        //申请后未释放return 0;
}

①编译程序t4.c

gcc -Wall t4.c -g -o t4

②使用Valgrind检查程序BUG

valgrind --tool=memcheck --leak-check=full ./t4

③运行结果如下:

==3221== Memcheck, a memory error detector==3221== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.==3221== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info==3221== Command: ./t4==3221====3221== Invalid write of size 4==3221==at 0x40051E: main (t4.c:7)==3221==Address 0x4c27040 is 0 bytes inside a block of size 1 alloc'd==3221==at 0x4A06A2E: malloc (vg_replace_malloc.c:270)==3221==by 0x400515: main (t4.c:6)==3221====3221== Invalid read of size 4==3221==at 0x400528: main (t4.c:8)==3221==Address 0x4c27040 is 0 bytes inside a block of size 1 alloc'd==3221==at 0x4A06A2E: malloc (vg_replace_malloc.c:270)==3221==by 0x400515: main (t4.c:6)==3221==x==3221====3221== HEAP SUMMARY:==3221==in use at exit: 1 bytes in 1 blocks==3221==total heap usage: 1 allocs, 0 frees, 1 bytes allocated==3221====3221== 1 bytes in 1 blocks are definitely lost in loss record 1 of 1==3221==at 0x4A06A2E: malloc (vg_replace_malloc.c:270)==3221==by 0x400515: main (t4.c:6)==3221====3221== LEAK SUMMARY:==3221==definitely lost: 1 bytes in 1 blocks==3221==indirectly lost: 0 bytes in 0 blocks==3221==      possibly lost: 0 bytes in 0 blocks==3221==still reachable: 0 bytes in 0 blocks==3221==        suppressed: 0 bytes in 0 blocks==3221====3221== For counts of detected and suppressed errors, rerun with: -v==3221== ERROR SUMMARY: 3 errors from 3 contexts(suppressed: 6 from 6)

从检查结果看,可以发现内存泄露。

⑸内存多次释放

#include <stdio.h>
#include <stdlib.h>
int main(void) { char *p;p=(char *)malloc(100);   if(p)printf("Memory Allocated at: %s/n",p); elseprintf("Not Enough Memory!/n"); free(p);                          //重复释放free(p);free(p);return 0;
}

①编译程序t5.c

gcc -Wall t5.c -g -o t5

②使用Valgrind检查程序BUG

valgrind --tool=memcheck --leak-check=full ./t5

③运行结果如下:

==3294== Memcheck, a memory error detector==3294== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Sewardet al.==3294== Using Valgrind-3.8.1 and LibVEX; rerun with -h forcopyright info==3294== Command: ./t5==3294====3294== Conditional jump or move depends on uninitialised value(s)==3294==    at 0x3CD4C47E2C: vfprintf (in /lib64/libc-2.12.so)==3294==    by 0x3CD4C4F189: printf (in /lib64/libc-2.12.so)==3294==    by 0x400589: main (t5.c:9)==3294====3294== Invalid free() / delete / delete[] / realloc()==3294==    at 0x4A06430: free (vg_replace_malloc.c:446)==3294==    by 0x4005B5: main (t5.c:13)==3294==  Address 0x4c27040 is 0 bytes inside a block of size100 free'd==3294==   at 0x4A06430: free (vg_replace_malloc.c:446)==3294==    by 0x4005A9: main (t5.c:12)==3294====3294== Invalid free() / delete / delete[] / realloc()==3294==    at 0x4A06430: free (vg_replace_malloc.c:446)==3294==    by 0x4005C1: main (t5.c:14)==3294==  Address 0x4c27040 is 0 bytes inside a block of size100 free'd==3294==    at 0x4A06430: free (vg_replace_malloc.c:446)==3294==    by 0x4005A9: main (t5.c:12)==3294==Memory Allocated at: /n==3294====3294== HEAP SUMMARY:==3294==    in use at exit: 0 bytes in 0 blocks==3294==  total heap usage: 1 allocs, 3 frees, 100 bytes allocated

从上面的输出可以看到(标注), 该功能检测到我们对同一个指针调用了3次释放内存操作。

⑹内存动态管理

常见的内存分配方式分三种:静态存储,栈上分配,堆上分配。全局变量属于静态存储,它们是在编译时就被分配了存储空间,函数内的局部变量属于栈上分配,而最灵活的内存使用方式当属堆上分配,也叫做内存动态分配了。常用的内存动态分配函数包括:malloc, alloc, realloc, new等,动态释放函数包括free, delete。

一旦成功申请了动态内存,我们就需要自己对其进行内存管理,而这又是最容易犯错误的。下面的一段程序,就包括了内存动态管理中常见的错误。

#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
int i;
char* p = (char*)malloc(10);
char* pt=p;
for(i = 0;i < 10;i++){
p[i] = 'z';}
free(p);
pt[1] = 'x';
free(pt);
return 0;
}

①编译程序t6.c

gcc -Wall t6.c -g -o t6

②使用Valgrind检查程序BUG

valgrind --tool=memcheck --leak-check=full ./t6

③运行结果如下:

==3380== Memcheck, a memory error detector
==3380== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.
==3380== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==3380== Command: ./t6
==3380==
==3380==  Invalid write of size 1
==3380==at 0x40055C: main (t6.c:14)
==3380==Address 0x4c27041 is 1 bytes inside a block of size 10 free'd
==3380==at 0x4A06430: free (vg_replace_malloc.c:446)
==3380==by 0x400553: main (t6.c:13)
==3380==
==3380==  Invalid free() / delete / delete[] / realloc()
==3380==at 0x4A06430: free (vg_replace_malloc.c:446)
==3380==by 0x40056A: main (t6.c:15)
==3380==Address 0x4c27040 is 0 bytes inside a block of size 10 free'd
==3380==at 0x4A06430: free (vg_replace_malloc.c:446)
==3380==by 0x400553: main (t6.c:13)
==3380==
==3380==
==3380== HEAP SUMMARY:
==3380==in use at exit: 0 bytes in 0 blocks
==3380==total heap usage: 1 allocs, 2 frees, 10 bytes allocated

申请内存在使用完成后就要释放。如果没有释放,或少释放了就是内存泄露;多释放也会产生问题。上述程序中,指针p和pt指向的是同一块内存,却被先后释放两次。系统会在堆上维护一个动态内存链表,如果被释放,就意味着该块内存可以继续被分配给其他部分,如果内存被释放后再访问,就可能覆盖其他部分的信息,这是一种严重的错误,上述程序第14行中就在释放后仍然写这块内存。

输出结果显示,第13行分配和释放函数不一致;第14行发生非法写操作,也就是往释放后的内存地址写值;第15行释放内存函数无效。

七、多线程程序调试

在多线程编程的世界里,线程之间的同步与竞争问题就像是隐藏在暗处的 “幽灵”,随时可能导致程序出现难以捉摸的错误。Valgrind 中的 Helgrind 和 DRD 工具,就是专门用来驱赶这些 “幽灵” 的 “守护者”。

Helgrind 致力于检查多线程程序中出现的竞争问题,它通过先进的算法,仔细监测内存中被多个线程访问的区域,一旦发现没有正确加锁或同步的情况,就会及时发出警报。比如说,我们有一个多线程程序,多个线程同时对一个共享变量进行读写操作,却没有使用任何锁来保护这个共享变量,这就很可能导致数据不一致的问题。运行 Helgrind,它就能精准地检测到这种潜在的风险,输出类似这样的报告:

==12345== Possible data race during write of size 4 at 0x... by thread #1
==12345==    at 0x... increment_counter
==12345==    by 0x... start_thread...

这份报告清晰地指出了在哪个线程、哪个函数中发生了可能的数据竞争,让我们能够迅速定位问题。

DRD 同样是检测多线程程序问题的得力助手,它专注于查找数据竞争、死锁等并发错误。它会对程序的执行过程进行全面的 “扫描”,一旦发现可疑的并发问题,就会给出详细的提示。例如,在一个复杂的多线程程序中,多个线程之间存在复杂的锁依赖关系,如果不小心出现了死锁的情况,DRD 就能及时察觉,帮助我们找出导致死锁的锁获取顺序,让我们能够调整代码,避免死锁的发生。

下面,我们通过一个具体的例子来看看它们的实战效果。假设我们有以下一段多线程 C++ 代码:

#include <thread>
#include <iostream>
#include <vector>
#include <mutex>std::vector<int> shared_data;
std::mutex mtx;void thread_function() {while (true) {std::unique_lock<std::mutex> lock(mtx);shared_data.push_back(rand());lock.unlock();}
}int main() {std::thread t1(thread_function);std::thread t2(thread_function);t1.join();t2.join();return 0;
}

在这段代码中,两个线程都在向共享的shared_data向量中添加随机数,虽然使用了互斥锁mtx来保护共享数据的访问,但在实际复杂的多线程环境下,可能还存在一些隐藏的问题。

我们使用 Helgrind 来检测这个程序,在终端中输入:

valgrind --tool=helgrind./test

Helgrind 运行后,可能会给出一些关于锁使用的建议,比如是否存在锁竞争、锁的粒度是否合适等信息,帮助我们进一步优化代码,确保多线程程序的稳定性。

如果我们使用 DRD 来检测,输入:

valgrind --tool=drd./test

DRD 可能会从不同的角度发现一些潜在的并发问题,比如是否存在某个线程长时间持有锁,导致其他线程阻塞,影响程序的并发性能等。通过这两个工具的双重保障,我们能够更加全面地排查多线程程序中的问题,让程序在多线程环境下稳定高效地运行。

7.1性能剖析功能

除了内存调试,Valgrind 在性能剖析方面也有着出色的表现,能帮我们深挖程序性能瓶颈,让程序 “跑” 得更快。

Callgrind 是性能剖析的得力工具,它就像程序的 “动态心电图”,能详细记录程序运行时函数的调用情况与 CPU 指令执行信息。运行程序时加上 “--tool=callgrind” 参数,它会生成一个包含丰富数据的文件,像函数调用次数、每个函数执行的指令数等。借助callgrind_annotate命令或KCachegrind图形界面工具查看分析结果,那些 “吃” CPU 资源多的函数便无所遁形。比如开发一个图形渲染程序,用 Callgrind 分析后发现某个复杂的光照计算函数占用大量 CPU 时间,对其算法优化或采用更高效的数学库后,程序渲染速度大幅提升。

Cachegrind 则专注于缓存使用分析,是优化缓存命中率的 “好帮手”。它模拟 CPU 缓存行为,精确统计缓存命中、未命中次数,还涵盖指令计数、内存引用次数等关键指标。执行程序加上 “--tool=cachegrind” 参数,会得到详细记录缓存信息的文件,用cg_annotate工具查看,能清晰知晓程序哪些部分缓存利用率低。如处理大规模图像数据时,发现频繁从内存加载数据导致缓存未命中率高,通过调整数据结构为连续存储,或优化算法减少数据跨缓存行访问,就能提高缓存命中率,让程序运行如 “闪电” 般迅速。

7.2Valgrind 的局限性

尽管 Valgrind 如此强大,但它也并非十全十美,存在一些局限性。就像再好的医生也有棘手的病症一样,Valgrind 在某些复杂的情况下,也可能会 “力不从心”。

比如说,对于一些静态分配或在堆栈上分配的数组的超出范围的读取或写入,Valgrind 可能无法检测到。这就需要我们在编写代码时,依然要保持警惕,不能完全依赖工具。另外,在检测某些复杂的内存错误场景时,可能会出现误报或漏报的情况,需要我们结合代码逻辑仔细甄别。但即便存在这些小瑕疵,也丝毫不能掩盖 Valgrind 在内存调试和性能分析领域的卓越光芒,它依然是我们编程路上最得力的助手之一。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词