笔者曾在上一篇文章里提及实例程序(轻松看懂的加解密系列(5) —— Windows平台下本地数据加密的新选择(附源码)-CSDN博客)中埋有“雷”,其实就是有内存泄漏。也难怪!实例程序里又是字符集转码、又是字符串混淆处理、又是申请缓冲区存放加解密数据,难免会发生管理堆内存的疏漏。这篇文章将分享一下笔者在处理这类问题时的经验,欢迎您的意见和建议。同时为了缩减篇幅,直奔主题,本文并不对所涉及到的相关工具进行介绍,如果有读者对相关工具还不太了解,可以先自行搜索相关内容学习。
继续以上篇文章中的程序代码为实验对象,步骤如下:
1. 先用 Application Verifier + Windbg 组合快速发现问题;
笔者实验时安装的【Application Verifier】版本为 10.0(x64),【Windbg】版本为 (WinDbg Preview 1.2306.14001.0)。启动【Application Verifier】之后,首先为目标测试程序选择要监控的项目,本例为了演示方便,只选择了监控【Memory】,如【图-1】。
![](https://i-blog.csdnimg.cn/blog_migrate/73a4c3bbd54519b44d3f8f7e457bd771.png)
如你所知,【Application Verifier】是一个用于 Windows 应用程序测试和调试的工具,其原理并不复杂,通过在注册表中针对特定应用程序的配置信息和设置进行修改,从而来检查和捕获错误。
![](https://i-blog.csdnimg.cn/blog_migrate/e4ed310b068b5d450fbf11fcd81f5c62.png)
如【图-2】所示,【GlobalFlag】
是一个用于控制调试和错误报告的标志位。具体地说,【0x100】对应于【FLG_HEAP_ENABLE_TAIL_CHECK】
标志,表示启用堆栈末尾检查。这意味着应用程序的堆栈分配将被监视,以检测是否发生了堆栈溢出或破坏。
【vrfcore.dll】是【Application Verifier】的核心 DLL 文件之一。它包含了用于执行内存和资源验证的核心功能。当【Application Verifier】启用时,它将使用【vrfcore.dll】
来监视应用程序的内存使用,并检查潜在的问题。
诸如以上这些目标测试程序相关联的注册表配置并不是笔者手工添加的,都是【Application Verifier】根据 UI 界面的配置自动生成的。
![](https://i-blog.csdnimg.cn/blog_migrate/7583f9ac616b8b1106d10b866d274867.png)
接下来我们就可以通过 Windbg 来启动目标测试程序,然后执行可能有内存泄漏风险的步骤,如【图-3】所示。此时由于注册表中已经预先定义了一些检测配置,所以程序在运行时会被记录下相关的状态并输出在 Windbg 控制台中。当我们执行完测试退出程序之后,可以发现控制台明确地打印出了 “Detected memory leaks!”,并且还将泄漏的内存块地址和大小打印了出来。比如【图-4】中的以下输出内容:
{1470} normal block at 0x04681248, 24 bytes long.
Data: < 6 ; * ! ( , > 1D 00 36 00 3B 00 2A 00 21 00 28 00 2C 00 1E 00
- 【1470】这个标识符通常称为"堆块号"(Heap Block Number),它是在进程的堆中分配的内存块的一个标识符。
- 【0x04681248】则是分配的内存块的起始地址,它是内存块在物理内存中的位置。大小为
24
字节。 - 【Data】将内存里的内容以ASCII码和16进制两种格式输出,
如果实验人员对被测程序很熟悉的话,其实这时已经可以对发生内存泄漏的地方大致心里有数了。如果暂时还不能确定?没关系,接下来我们可以回到 Visual Studio,使用其他工具来定位问题所在位置。
![](https://i-blog.csdnimg.cn/blog_migrate/a1ba66804b09b03790770b897532cdd4.png)
2. 再用 Visual Studio Diagnostic Tools 定位问题;
在 Visual Studio 里 Debug 运行测试程序之后,被测试的 MFC 程序界面初始化结束,此时可以打开【Diagnostic Tools】窗口,如【图-5】。记得在其中按下【Heap Profiling】按钮后,以监控堆内存,接着就可以像正常使用软件一样操作起来了。然后在你认为可能发生了内存泄漏之后,按下【Take Snapshot】按钮,这就像按下了照相机快门一样,给此时的程序的堆内存拍摄一张快照。【图-6/7/8】分别是程序初始化结束、加密操作结束和解密操作结束,这三个时间窗口拍摄下来的内存快照。
![](https://i-blog.csdnimg.cn/blog_migrate/9f6610a006dd9c7c4e44e48e8c1d94dc.png)
![](https://i-blog.csdnimg.cn/blog_migrate/76c51f2d1758614c232392384863cb10.png)
![](https://i-blog.csdnimg.cn/blog_migrate/6eba3198ddcf383ea2e5e4fc2ab488f1.png)
![](https://i-blog.csdnimg.cn/blog_migrate/a68f81a91ecb19f0e4a4ab5931f0c8f5.png)
![](https://i-blog.csdnimg.cn/blog_migrate/1930cacdadb2a00ac3a6b2d8d5491dc5.png)
我们来观察一下,从堆内存的角度看【Heap Profiling】,第二张快照比第一张快照发生了什么具体变化。如【图-9】右侧【Diagnostic Tools】界面,首先直观上的感受是占用的内存数量在增加,不过这也很正常,我们需要跟踪的是新增的,但是又最终没有被释放的内存。【图-9】左侧展示的表格为具体有哪些地方增加了内存,包括数量和大小(字节)的变化。点开每一行具体的内容,如果其堆栈类似【图-10】这样完全是系统函数的堆栈调用,那基本上就可以免检了,相信操作系统的代码是会正确释放申请的内存。但如果点开后如【图-11】这样,调用栈中包含了本地代码函数,而且进一步点击定位代码后,如【图-12】这样,明显有申请内存,但没见着释放内存的代码,就要引起高度注意了,需要查询确认一下内存最后被释放的逻辑。而当前测试程序内存泄漏的原因,正是字符集转换时没有释放申请的内存导致的。
![](https://i-blog.csdnimg.cn/blog_migrate/f8800266b1d8e6e45cbc093a7bd18ed7.png)
![](https://i-blog.csdnimg.cn/blog_migrate/d0f3dea00bc93907d7937c4f8196d860.png)
![](https://i-blog.csdnimg.cn/blog_migrate/56ac184ce529eb385955fb39f48da8ed.png)
3. 修复问题后,再次使用 Windbg 来验证结果;
![](https://i-blog.csdnimg.cn/blog_migrate/368f2ce1a77e3a13aaf3d2ec87b1e90c.png)
![](https://i-blog.csdnimg.cn/blog_migrate/32f211086ced53d6b76a3b62599584a9.png)
总结:
本例给出的是笔者总结的 Windows Native C++ 程序开发阶段快速定位内存泄漏的一种策略。实际研发过程当中,还有许多其他的工具和策略可供选择,比如利用 Windbg !heap 系列命令调试内存泄漏问题、比如使用 “gflags 和 UMDH tool” 组合调试内存泄漏问题。不过在原理上其实都差不多,都是通过比较两次堆内存的差别,进而帮助使用者定位到问题代码。
避免内存泄漏的传统思路是严格成对使用new/delete或malloc/free来进行内存分配和释放。但是当代码变得复杂时,很容易出现疏漏。为了解决这个问题,我们可以使用智能指针(Smart Pointer)作为终极解决方案。本文是传统思路的修复版,附录里还提供了一个智能指针修复版本。
附录:
传统修复版:https://github.com/345967018/WBBestPractice028EncryptDecrypt.git
智能指针修复版:
![](https://i-blog.csdnimg.cn/blog_migrate/9822d8c4240a7130fc7e16bf1810d8a9.png)