說明
使用 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
。