说明
使用 VLD 内存泄漏检测工具辅助开发时整理的学习笔记。 同系列文章目录可见 《内存泄漏检测工具》目录
1. 使用方式
在 QT 中使用 VLD 的方法可以查看另外几篇博客:
本次测试使用的环境为:QT 5.9.2,MSVC 2015 32bit,Debug 模式,VLD 版本为 2.5.1,VLD 配置文件不做任何更改使用默认配置,测试工程所在路径为:E:\Cworkspace\Qt 5.9\QtDemo\testVLD
。
2. 有一处内存泄漏时的输出报告(int 型)
写一个有一处内存泄漏的程序,如下:
#include <QCoreApplication>
#include "vld.h"
void testFun()
{
int *ptr = new int(0x55345678);
printf("ptr = %08x, *ptr = %08x", ptr, *ptr);
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
testFun();
return a.exec();
}
程序运行时,在标准输出窗会输出以下结果:
ptr = 0127b7a0, *ptr = 55345678
程序运行结束后,检测到了内存泄漏,VLD 会输出以下报告(本例中出现一处内存泄漏),第 1~3 行显示 VLD 运行状态,第 4~21 行显示泄漏内存的详细信息,第 22~24 行总结此次泄漏情况,第 25 行显示 VLD 退出状态。
Visual Leak Detector read settings from: D:\Program Files (x86)\Visual Leak Detector\vld.ini
Visual Leak Detector Version 2.5.1 installed.
WARNING: Visual Leak Detector detected memory leaks!
---------- Block 1 at 0x0127B7A0: 4 bytes ----------
Leak Hash: 0xEB4D3A14, Count: 1, Total 4 bytes
Call Stack (TID 22408):
ucrtbased.dll!malloc()
f:\dd\vctools\crt\vcstartup\src\heap\new_scalar.cpp (19): testVLD.exe!operator new() + 0x9 bytes
e:\cworkspace\qt 5.9\qtdemo\testvld\main.cpp (6): testVLD.exe!testFun() + 0x7 bytes
e:\cworkspace\qt 5.9\qtdemo\testvld\main.cpp (16): testVLD.exe!main()
f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl (74): testVLD.exe!invoke_main() + 0x1B bytes
f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl (264): testVLD.exe!__scrt_common_main_seh() + 0x5 bytes
f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl (309): testVLD.exe!__scrt_common_main()
f:\dd\vctools\crt\vcstartup\src\startup\exe_main.cpp (17): testVLD.exe!mainCRTStartup()
KERNEL32.DLL!BaseThreadInitThunk() + 0x19 bytes
ntdll.dll!RtlGetAppContainerNamedObjectPath() + 0x11E bytes
ntdll.dll!RtlGetAppContainerNamedObjectPath() + 0xEE bytes
Data:
78 56 34 55 xV4U.... ........
Visual Leak Detector detected 1 memory leak (40 bytes).
Largest number used: 40 bytes.
Total allocations: 40 bytes.
Visual Leak Detector is now exiting.
第 1 行表示 VLD 读取的配置文件路径,可以根据路径找到该文件,然后更改里面的相关配置,获得想要的效果。
第 2 行表示 VLD 2.5.1 在程序中初始化成功。
第 3 行表示本次运行检测到了内存泄漏。
第 4 行中,Block 1
表示本块内存是在堆上分配的第 1 个内存块,0x0127B7A0
表示该内存块的首地址,与标准输出窗输出的 ptr = 0127b7a0
一致,4 bytes
表示该内存块的大小,这一行输出了泄漏内存块的地址信息和大小信息。
第 5 行中,Leak Hash: 0xEB4D3A14
是由泄漏块大小及调用堆栈信息计算出的唯一标识符,如果在报告中看到相同的 Leak Hash
,这表示这些泄漏块具有相同大小和相同的调用堆栈。Count: 1
是发生泄漏的计数,使用默认配置时全部等于 1,可以将配置文件中的参数 AggregateDuplicates
设置为 yes
来合并显示具有相同 Leak Hash
值的的泄漏块信息。Total 4 bytes
是此内存块的泄漏大小,与第 4 行一致。这一行输出了泄漏内存块的唯一标识符、泄漏频次、大小信息。
第 6 行中,Call Stack
表示接下来的几行是产生泄漏的调用堆栈,(TID 22408)
表示产生此内存泄漏块函数所在线程的 TID
为 22408
,据此来指示发生内存泄漏的线程。在调试多线程程序时,TID
信息很有帮助,它能帮助确定泄漏所在线程。
第 7 行中,ucrtbased.dll
是一个系统库,提供了各种标准 C 和 C++ 函数的实现,包括 malloc()
,这个函数用于在运行时动态分配内存。它处于调用栈顶,表示此内存块是使用 ucrtbased.dll
库中的 malloc()
分配的,然后传递给第 8 行 testVLD.exe
程序中的 operator new()
。
第 8 行中,f:\dd\vctools\crt\vcstartup\src\heap\new_scalar.cpp
是从 MSVC
中获取的调试信息,这个路径是内置在 Visual C++ Runtime Library
中的,并不代表 new_scalar.cpp
的真实路径,它的真实路径一般在 Visual Studio
的安装目录下。在我电脑上:
Visual Studio 2015
使用的new_scalar.cpp
文件真实路径为C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\crt\src\vcruntime\new_scalar.cpp
;Visual Studio 2019
使用的new_scalar.cpp
文件真实路径为D:\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.23.28105\crt\src\vcruntime\new_scalar.cpp
。
在不同系统上的相同版本的 Visual C++ Runtime Library
中,这个内置路径通常是一样的。(19)
表示 operator new()
函数中分配内存的代码位于 new_scalar.cpp
文件的第 19 行,最后面的 + 0x9 bytes
表示从 operator new()
函数开始到导致泄漏产生的指令的内存偏移量,这些信息在调试时很有用,可以帮助快速定位到确切代码行。
第 9 行中,e:\cworkspace\qt 5.9\qtdemo\testvld\main.cpp (6): testVLD.exe!testFun() + 0x7 bytes
表示 main.cpp
位于 e:\cworkspace\qt 5.9\qtdemo\testvld
路径下,这与项目实际路径是一致的,差别只是 VLD 将其全部转成了小写字母形式,testFun()
函数中分配内存的代码位于 main.cpp
的第 6 行,这与实际情况完全一致。最后面的 + 0x7 bytes
表示从 testFun()
函数开始到导致泄漏产生的指令的内存偏移量,这个信息据说是多用于汇编调试中,与实际是否能对上还没仔细研究过。
第 10 行中,e:\cworkspace\qt 5.9\qtdemo\testvld\main.cpp (16): testVLD.exe!main()
表示 main()
函数中分配内存的代码位于 main.cpp
的第 16 行,没有提供指令的内存偏移信息,这与实际情况(第 14 行)有些差异,不过第 14 与第 16 行之间并没有别的代码,造成这种差异的原因有待深究,但对于定位泄漏点所在位置已经够用了。
第 11~14 行,跟踪显示了启动程序所调用的函数链,其中 mainCRTStartup()
函数是入口点。
第 15~17 行,跟踪显示了程序启动时所调用的 Windows
操作系统函数,BaseThreadInitThunk()
一般都会出现在调用栈底,它是 Windows 进程中所有用户模式线程的入口点,系统调用它在进程中启动一个新线程,并由它调用程序的主函数。ntdll.dll
中的 RtlGetAppContainerNamedObjectPath()
函数被调用了两次,但指令的内存偏移量不同(分别是 0x11E bytes
和 0xEE bytes
),这也是一个 Windows
操作系统函数,用于检索与进程相关的应用程序容器名称,由 Windows
系统的各个部分和其他需要知道应用程序容器名称的程序调用,关于 Windows
容器的介绍,可以查看 Microsoft Windows 和容器。
第 18~19 行,分别用十六进制及 ASCII 字符显示了泄漏内存块中信息,内存初始化时赋的初始值为 0x55345678
,计算机默认使用小端字节序,因此在内存中各字节的十六进制初始值分别为 78 56 34 55
,转化为十进制数,0x78
、0x56
、0x34
、0x55
分别为 120、86、52、85,查找 ASCII 码表,可得这四个字节对应的 ASCII 字符分别为 x
、V
、4
、U
,与 VLD 的输出完全一致。VLD 在显示内存内容时,每行最多显示 16 个字节,但这次只泄漏了 4 个字节,因此在显示上第 19 行中间有 12 个字节的空白位,行尾有 12 个占位点(.... ........
)。
第 22 行,表示本次运行检测到 1 处内存泄漏,泄漏的总大小为 40 bytes
,这里面不光包含用于 int
存储的 4 bytes
,还包含用于管理追踪这块内存的另外 36 bytes
,因此,虽然代码只请求了 4 bytes
的内存,但程序实际上为此分配了 40 bytes
的内存。可以查看以下资料,这两个网站内容一样,第一个是国外源博客,第二个是国内某爬虫网站盗取的博客。(实际测试发现,使用 32 bit 的编译器时,这个管理头的大小为 36bytes
,当使用 64 bit 的编译器时,大小变为 52bytes
。)
- Visual Leak Detector indicates 40 bytes filtered for an int *。
- Visual Leak Detector indicates 40 bytes filtered for an int *(国内)。
第 23 行,表示本次运行中单次分配的最大内存大小,为 40 bytes
,原因看前一条。在实际使用过程中,这个 Largest number used
有时候跟实际情况对不上,又好像表示本次运行中分配的连续堆内存最大宽度,有兴趣的可以深入研究。
第 24 行,表示本次运行中堆上分配内存的总大小,为 40 bytes
,即代码申请 int
的 4 bytes
,和管理头占用的 36 bytes
。
第 25 行,表示 VLD 正常退出。
在程序的第 6 行加个断点,按 F5
进入调试状态,结果如下,调用堆栈中各函数的名称、所属文件、所在行号、调用顺序都和 VLD 一致。
3. 有一处内存泄漏时的输出报告(int 数组型)
写一个有一处内存泄漏的程序,如下:
#include <QCoreApplication>
#include "vld.h"
void testFun()
{
int *ptr = new int[10];
ptr[0] = 0x64568932;
printf("ptr = %08x", ptr);
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
testFun();
return a.exec();
}
程序运行时,在标准输出窗会输出以下结果:
ptr = 00ab4340
程序运行结束后,检测到了内存泄漏,VLD 会输出以下报告(本例中出现一处内存泄漏),第 1~3 行显示 VLD 运行状态,第 4~23 行显示泄漏内存的详细信息,第 24~26 行总结此次泄漏情况,第 27 行显示 VLD 退出状态。
Visual Leak Detector read settings from: D:\Program Files (x86)\Visual Leak Detector\vld.ini
Visual Leak Detector Version 2.5.1 installed.
WARNING: Visual Leak Detector detected memory leaks!
---------- Block 1 at 0x00AB4340: 40 bytes ----------
Leak Hash: 0x39CB72AB, Count: 1, Total 40 bytes
Call Stack (TID 29256):
ucrtbased.dll!malloc()
f:\dd\vctools\crt\vcstartup\src\heap\new_array.cpp (15): testVLD.exe!operator new[]() + 0x9 bytes
e:\cworkspace\qt 5.9\qtdemo\testvld\main.cpp (6): testVLD.exe!testFun() + 0x7 bytes
e:\cworkspace\qt 5.9\qtdemo\testvld\main.cpp (17): testVLD.exe!main()
f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl (74): testVLD.exe!invoke_main() + 0x1B bytes
f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl (264): testVLD.exe!__scrt_common_main_seh() + 0x5 bytes
f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl (309): testVLD.exe!__scrt_common_main()
f:\dd\vctools\crt\vcstartup\src\startup\exe_main.cpp (17): testVLD.exe!mainCRTStartup()
KERNEL32.DLL!BaseThreadInitThunk() + 0x19 bytes
ntdll.dll!RtlGetAppContainerNamedObjectPath() + 0x11E bytes
ntdll.dll!RtlGetAppContainerNamedObjectPath() + 0xEE bytes
Data:
32 89 56 64 CD CD CD CD CD CD CD CD CD CD CD CD 2.Vd.... ........
CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD ........ ........
CD CD CD CD CD CD CD CD ........ ........
Visual Leak Detector detected 1 memory leak (76 bytes).
Largest number used: 76 bytes.
Total allocations: 76 bytes.
Visual Leak Detector is now exiting.
输出与上一节基本类似,这里提几个不同点:
第 4~5 行中,40 bytes
与代码请求的内存量大小相同,即 sizeof(int) * 10 = 40
。
第 8 行,表示 operator new[]()
函数中分配内存的代码位于 new_array.cpp
文件的第 15 行,这与前面的 operator new()
函数及 new_scalar.cpp
文件不同,实际使用时可以根据这一点来判断泄漏形式,是数组还是标量。
第 19~21 行,十六进制数 32 89 56 64
的十进制表示为 50 137 86 100
,其中 50 86 100
对应的 ASCII 字符分别为 2
、 V
、 d
,是可以输出显示的字符,但 137
超过了 127
,不属于 ASCII 标准字符集,属于 ASCII 扩展字符集,无法直接在界面上显示,因此仍以 "."
英文句点来代替。此外,未初始化内存单个字节的值都为 CD
,对应的十进制数为 205
,这是 Microsoft's C++ debugging runtime library
自动初始化的结果。通常,在 Debug
模式下,MSVC
会把未初始化的栈内存全部填充成 0xCC
,当成字符串看就是”烫烫烫烫……“;同时会把未初始化的堆内存全部填充成 0xCD
,当成字符串看就是“屯屯屯屯……”。实际使用时可以根据这一点来判断是否对泄漏内存赋了初始值。关于 0xCD
这一特殊十六进制数,更详细的可以查看以下资料,国内可能无法直接访问:
第 24~26 行中,76 bytes
包含有申请 int[10]
的 40 bytes
,和管理头占用的 36 bytes
。