【Visual Leak Detector】QT 中 VLD 输出解析(二)

说明

使用 VLD 内存泄漏检测工具辅助开发时整理的学习笔记。 同系列文章目录可见 《内存泄漏检测工具》目录


1. 使用方式

在 QT 中使用 VLD 的方法可以查看另外几篇博客:

本次测试使用的环境为:QT 5.9.2MSVC 2015 32bitDebug 模式,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) 表示产生此内存泄漏块函数所在线程的 TID22408,据此来指示发生内存泄漏的线程。在调试多线程程序时,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 bytes0xEE bytes),这也是一个 Windows 操作系统函数,用于检索与进程相关的应用程序容器名称,由 Windows 系统的各个部分和其他需要知道应用程序容器名称的程序调用,关于 Windows 容器的介绍,可以查看 Microsoft Windows 和容器

第 18~19 行,分别用十六进制及 ASCII 字符显示了泄漏内存块中信息,内存初始化时赋的初始值为 0x55345678,计算机默认使用小端字节序,因此在内存中各字节的十六进制初始值分别为 78 56 34 55,转化为十进制数,0x780x560x340x55 分别为 120、86、52、85,查找 ASCII 码表,可得这四个字节对应的 ASCII 字符分别为 xV4U,与 VLD 的输出完全一致。VLD 在显示内存内容时,每行最多显示 16 个字节,但这次只泄漏了 4 个字节,因此在显示上第 19 行中间有 12 个字节的空白位,行尾有 12 个占位点(.... ........)。

第 22 行,表示本次运行检测到 1 处内存泄漏,泄漏的总大小为 40 bytes,这里面不光包含用于 int 存储的 4 bytes,还包含用于管理追踪这块内存的另外 36 bytes,因此,虽然代码只请求了 4 bytes 的内存,但程序实际上为此分配了 40 bytes 的内存。可以查看以下资料。(当使用 32 bit 的 Debug 编译器时,这块管理内存的大小为 36bytes,当使用 64 bit 的 Debug 编译器时,大小变为 52bytes。差别产生的原因是 _CrtMemBlockHeader 结构体中含有指针类型成员,而指针类型在不同位数的环境下内存量宽度是不一样的。)

第 23 行,表示整个检测过程中全部泄漏块总大小的最大值(即检测过程中 \(max{\{上一行括号内的值\}}\)),为 40 bytes,原因看前一条,有兴趣的可以阅读 VLD 源码。

第 24 行,表示本次运行中堆上分配内存的总大小,为 40 bytes,即代码申请 int4 bytes,和管理块占用的 36 bytes

第 25 行,表示 VLD 正常退出。

在程序的第 6 行加个断点,按 F5 进入调试状态,结果如下,调用堆栈中各函数的名称、所属文件、所在行号、调用顺序都和 VLD 一致。

Oh Shit!-图片走丢了-打个广告-欢迎来博客园关注“木三百川”

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 扩展字符集,无法直接在界面上显示,因此仍以 "." 英文句点来代替,源码根据 isgraph() 返回值来判断是否可正常显示。此外,未初始化内存单个字节的值都为 CD,对应的十进制数为 205,这是 Microsoft's C++ debugging runtime library 自动初始化的结果。通常,在 Debug 模式下,MSVC 会把未初始化的栈内存全部填充成 0xCC ,当成字符串看就是”烫烫烫烫……“;同时会把未初始化的堆内存全部填充成 0xCD ,当成字符串看就是“屯屯屯屯……”。实际使用时可以根据这一点来判断是否对泄漏内存赋了初始值。关于 0xCD 这一特殊十六进制数,更详细的可以查看以下资料,国内可能无法直接访问:

第 24~26 行中,76 bytes 包含有申请 int[10]40 bytes,和管理块占用的 36 bytes

posted @ 2023-03-26 20:30  木三百川  阅读(346)  评论(0编辑  收藏  举报