C++ 内存泄漏检测
早期的一篇文章 [“嵌入式系统中的内存泄漏检测”,LJ,2002 年 9 月,可在 www.linuxjournal.com/article/6059 获取] 讨论了在使用 C 作为编程语言时如何检测内存泄漏。本文讨论了在 C++ 程序中检测内存泄漏的问题。此处讨论的工具检测应用程序错误,而不是内核内存泄漏。所有这些工具都已与 MontaVista Linux Professional Edition 2.1 和 3.0 产品一起使用,其中一个工具 dmalloc 随 MontaVista Linux 一起发布。
在为嵌入式系统开发应用程序时,设计人员和程序员必须非常小心地使用系统内存资源。与工作站不同,嵌入式系统具有有限的内存源。通常,没有交换区域可供空闲程序使用。当系统耗尽所有资源时,除了崩溃并重新启动或杀死一些程序以腾出所需资源外,别无他法。因此,编写不泄漏内存的程序非常重要。许多工具可以帮助程序员找到这些资源泄漏。此处讨论的所有工具都带有自己的测试程序。
我见过应用程序开发人员成功使用的一种测试方法是使用工作站开发原型代码并在其上尽可能多地进行调试。强烈建议以这种方式使用内存泄漏工具。通过在工作站上进行调试,应用程序程序员可以确保向目标处理器的过渡会更容易。使用工作站的主要原因是它们价格便宜,而且每个相关人员都有一台。另一方面,目标通常很少且需求量很大。
大多数内存泄漏检测程序都以完整源代码的形式提供。它们通常构建在基于 x86 的平台上。在非 x86 目标上运行它们需要进行一些移植。这种移植工作可能像重新编译、链接和运行一样简单,或者可能需要将一些汇编代码从一个平台更改为另一个平台。一些工具附带了在交叉编译环境中使用它们的提示和建议。
dmalloc 的作者(我在 2002 年 9 月的文章中详细介绍过该工具)表示,他对 C++ 的了解有限,因此 C++ 内存泄漏检测也受到限制。为了将 dmalloc 与 C++ 和线程一起使用,必须将应用程序静态链接。
ccmalloc 工具是一个内存分析器,具有简单的使用模型,支持动态链接库,但不支持 dlopen。它可以检测内存泄漏、同一数据的多次释放、欠写和溢写以及写入已释放的数据。它显示分配和释放统计信息。它适用于优化和剥离的代码,并支持 C++。它还为整个调用链提供文件和行号信息,而不仅仅是 malloc/free 的直接调用者,并且它支持 C++。无需重新编译即可使用 ccmalloc;只需将其与 -lccmalloc -ldl 或 ccmalloc.o -ldl 链接即可。ccmalloc 提供调用链的有效表示、调用链的可自定义打印、调用链的选择性打印、压缩日志文件和一个名为 .ccmalloc 的启动文件。主要文档可以在名为 ccmalloc.cfg 的文件中找到。程序附带的测试文件提供了更多文档。nm 和 gdb 是获取有关符号的信息所必需的,gzip 是压缩日志文件所必需的。
正如作者所说,NJAMD “不仅仅是另一个 malloc 调试器”。与大多数内存分配调试器一样,标准分配函数被新的函数替换,这些新函数在使用内存时执行各种检查。具体来说,它查找动态缓冲区溢出/欠溢出,并检测释放后内存的重用。为 NJAMD 构建的库可以 LD_PRELOADed,也可以链接到程序。它在第一次内存分配时创建一个大的内存缓冲区(20MB),然后根据程序需要将其分割。
NJAMD 可以单独使用,也可以与前端或 gdb 一起使用。它有一个实用程序,允许事后堆分析。另一个功能允许正在调试的应用程序跳过重新编译;只需预加载库即可。NJAMD 还能够跟踪包装 malloc 和 free、GUI 小部件分配器以及 C++ new 和 delete 的库函数中的泄漏。通常,内存泄漏不会立即被发现,而是潜伏着,等待在最明显的时刻爆发。追踪这个问题可能需要很长时间。NJAMD 有许多环境变量,允许设置不同级别的检测。与大多数调试工具一样,性能可能是 NJAMD 的一个问题,因此该工具应仅在开发期间使用。在启用该工具的情况下部署可能会导致系统速度变慢。
YAMD(又一个内存调试器)是另一个用于捕获已分配内存块边界的软件包。它通过使用处理器的分页机制来实现这一点。可以检测到读取和写入越界条件。错误的检测发生在导致错误发生的指令上,而不是在稍后其他访问发生时。陷阱会记录文件名和行号以及回溯信息。回溯非常有用,因为大多数内存分配都是通过有限数量的例程完成的。
该库模拟 malloc 和 free 调用。这样做可以捕获许多间接 malloc 调用,例如 strdup 所做的调用。它还可以捕获 new 和 delete 操作。但是,如果 new 和 delete 运算符被重载,则无法捕获它们。
与同类型的其他程序一样,YAMD 需要大量的虚拟内存或交换空间才能发挥其魔力。但是,在嵌入式系统上,通常无法获得这些资源。此处也鼓励早先关于在工作站上使用此工具进行原型调试的建议。完成此调试后,可以将应用程序移动到目标,并确信已找到大部分(如果不是全部)内存泄漏。
YAMD 提供了一个脚本 run-yamd,用于使程序易于执行。它提供了几个选项来尝试从某些条件中恢复。当正在检查的程序执行核心转储时,可以创建日志文件。调试器可用于调试 YAMD 控制的程序。但是,当 YAMD 预加载而不是与程序静态链接时,可能会出现问题。
Valgrind 是一个相对较新的开源内存调试器,适用于 x86-GNU Linux 系统。它比早期工具具有更多功能,但它仅在 x86 主机上运行。当程序在 Valgrind 的控制下运行时,将检查所有对内存的读取和写入,以及对 malloc、free、new 和 delete 的调用。Valgrind 可以检测未初始化的内存、内存泄漏、传递未初始化或不可寻址的内存、POSIX 线程的一些误用以及 malloc/free 和 new/delete 操作的不匹配使用。
Valgrind 也可以与 gdb 一起使用,以捕获错误并允许程序员在错误发生时使用 gdb。这样做时,程序员可以查找问题的根源并更快地修复它。在某些情况下,可以进行修补并继续调试。Valgrind 旨在用于大型和小型应用程序,包括 KDE 3、Mozilla、OpenOffice 等。
Valgrind 的一个功能是它能够提供有关缓存分析的详细信息。它可以创建 CPU 的 L1-D、L1-I 和统一 L2 缓存的详细模拟,并计算程序跟踪的每一行的缓存命中计数。Valgrind 有一个写得很好的 HOWTO,其中包含大量示例。它的网站包含大量信息,并且易于浏览。有许多不同的选项组合可用,用户可以自行确定他们喜欢的组合。
Valgrind 的错误显示包含正在检查的程序的进程 ID,后跟错误描述。地址与行号和源文件名一起显示。还会显示完整的追溯信息。Valgrind 读取一个启动文件,该文件可以包含抑制某些错误检查消息的指令。这使您可以更多地关注手头的代码,而不是无法更改的现有库。
Valgrind 通过在模拟处理器环境中运行应用程序来进行检查。它强制动态链接器/加载器首先加载模拟器,然后将程序及其库加载到模拟器中。所有数据都在程序运行时收集。当程序终止时,所有日志数据都会显示或写入日志文件。
mpatrol 库可以与您的程序链接以跟踪和追溯内存分配。它是在多个不同的操作系统平台上编写和运行的。该库的一个明显优势是它已移植到许多不同的目标处理器,包括 MIPS、PowerPC、x86 以及一些 MontaVista 客户的 StrongARM 目标。
mpatrol 是高度可配置的;它可以设置为从固定大小的静态数组中分配内存,而不是使用堆。它可以构建为静态库、共享库或线程安全库。它也可以是一个大型目标文件,因此可以链接到应用程序而不是包含在库中。此功能为最终用户提供了极大的灵活性。
它创建的代码包含 44 个不同的内存分配和字符串函数的替换。提供了钩子,因此可以从 gdb 中调用这些例程。这允许调试使用 mpatrol 的程序。
库设置和堆使用情况可以在程序运行时定期显示。运行时收集的所有统计信息都会在程序终止时显示。该程序具有内置的默认值,这些默认值可以被环境变量覆盖。通过在运行时更改这些环境变量,无需重建库。可以动态调整各种测试。所有日志记录都写入当前工作目录中的文件;这些文件可以被覆盖以转到 stdout 和 stderr 或其他文件。
在程序运行时,可以收集和记录调用堆栈追溯信息。如果程序和关联库是使用有关符号和行号的调试信息构建的,则可以在日志文件中显示此信息。
如果在某个时候程序员想要在较小的内存占用空间上模拟压力测试,则可以指示 mpatrol 限制内存占用空间。这允许测试在实验室环境中可能不易获得的条件。使用此功能可以更轻松地在模拟客户环境或设置严苛的测试工具中进行压力测试。此外,可以使测试程序随机失败一组内存分配,以测试错误恢复例程。此功能对于 C++ 中的异常处理非常有用。可以拍摄堆的快照,以测量内存使用量的高水位线和低水位线。
Parasoft 的 Insure++ 产品不是 GPL 或免费软件,但它是一个用于内存泄漏检测和代码覆盖率的好工具,与 mpatrol 非常相似。Insure++ 在代码覆盖率方面比 mpatrol 做得更多,并提供收集和显示数据的工具。该软件的试用副本可以下载并在非 Linux 工作站上试用指定的时间段。
该产品在 Linux 下易于安装,但节点锁定到安装它的计算机上。Insure++ 附带一套全面的文档和多个选项。代码覆盖率工具是独立的,但随初始软件包一起提供。
Insure++ 提供了大量有关它发现的问题的信息。要使用 Insure++,必须使用 Insure++ 前端编译它,该前端将其传递给普通编译器。此前端检测代码以使用 Insure++ 库例程。在编译器阶段,会检测到非法类型转换以及不正确的参数传递。会报告明显的内存损坏错误。在运行时,错误会报告给 stderr,但可以通过图形工具显示。构建应用程序时,可以使用命令行或 makefile,从而方便构建项目和大型应用程序。
程序的执行很简单。Insure++ 不需要任何特殊命令即可执行;程序就像普通程序一样运行。所有调试和错误捕获代码都包含在与程序链接的 Insure++ 库中。
一个名为 Inuse 的附加工具实时显示程序如何使用内存。它可以准确地描述内存的使用方式、碎片化的程度以及看起来很小但可能会随着时间推移而累积的细微泄漏。我有一个客户的经验,他们发现一个特定的 C++ 类泄漏了少量内存,在工作站上,这被认为是非常小的。对于预计运行数月甚至数年的嵌入式系统,泄漏可能会变得非常大。使用此工具,可以轻松地追踪、找到和修复泄漏。其他可用工具没有捕获到此泄漏。
代码覆盖率由另一个工具 TCA 分析。当程序在启用 Insure++ 的情况下运行时,可以收集数据,当由 TCA 分析时,这些数据可以准确地描述执行了哪些代码。TCA 具有 GUI,可增强代码覆盖率的显示。
