深入內存調試:Valgrind 工具的終極指南
在軟件開發的世界裏,代碼質量就是生命線,而內存管理又是這條生命線中最脆弱的一環。內存泄漏,哪怕只是微小的一處,日積月累,都可能對整個系統造成災難性的打擊,無論是大型企業級應用、實時性要求極高的嵌入式系統,還是對性能錙銖必較的遊戲開發。此時,掌握一款強大的內存檢測工具至關重要,Valgrind 便是這樣的利器。它以其精準、全面的檢測能力,成爲衆多開發者捍衛代碼質量的 “祕密武器”。今天,我們就深入探索 Valgrind,看看它如何幫助我們規避內存泄漏,打造堅如磐石的代碼。
一、Valgrind 是什麼?
在編程的世界裏,代碼就像是一座宏偉的建築,而內存管理則是這座建築的基石。一個小小的內存錯誤,可能就會引發程序崩潰、數據丟失等災難性後果。這時候,Valgrind 就像是一位專業的建築質檢員,能夠幫助我們找出代碼中的內存問題,確保程序的穩定性和可靠性。
Valgrind 是一款用於內存調試、內存泄漏檢測和性能分析的軟件開發工具,堪稱程序員的得力助手。它最初由 Julian Seward 設計,2006 年因其在 Linux x86 平臺上的免費內存調試工具上的卓越貢獻,榮獲第二屆 Google-O'Reilly 開放源碼獎,並且遵循 GNU 通用公共許可證,是自由軟件中的明星產品。
對於 C/C++ 程序員來說,Valgrind 更是不可或缺。在 C/C++ 編程中,內存的分配與釋放需要程序員手動管理,稍有不慎就會出現各種問題,比如使用未初始化的內存、內存泄漏、越界訪問等。這些問題往往難以察覺,可能在程序運行一段時間後才突然爆發,給調試帶來極大的困難。而 Valgrind 就如同一個敏銳的偵探,能夠精準地發現這些隱藏的內存 “陷阱”,讓我們及時修復問題,避免程序在關鍵時刻 “掉鏈子”。
Valgrind 的體系結構以下圖所示:
二、Valgrind 的強大功能
Valgrind 之所以如此強大,是因爲它包含了一系列各有所長的工具,就像一個多功能的瑞士軍刀,能夠從不同角度剖析我們的程序。接下來,讓我們深入瞭解一下這些工具的獨特魅力。
⑴Memcheck:內存問題的 “放大鏡”
Memcheck 是 Valgrind 中當之無愧的明星工具,也是使用最爲廣泛的一個。它就像一個高倍放大鏡,能夠精準地檢測出程序中各種各樣的內存問題。無論是使用未初始化的內存、讀寫已釋放的內存塊,還是數組下標越界、內存泄漏等,統統都逃不過它的 “法眼”。在開發過程中,這些內存問題往往隱藏得很深,可能在特定的條件下才會暴露出來,給調試帶來極大的困擾。而 Memcheck 能夠在程序運行時實時監測內存的使用情況,一旦發現問題,立即給出詳細的錯誤報告,包括錯誤發生的位置、涉及的內存地址等信息,讓我們能夠迅速定位並修復問題。
因此,它能檢測如下問題:
-
未初始化內存的使用;
-
讀 / 寫釋放後的內存塊;
-
讀 / 寫超出 malloc 分配的內存塊;
-
讀 / 寫不適當的棧中內存塊;
-
內存泄漏,指向一塊內存的指針永遠丟失;
-
不正確的 malloc/free 或 new/delete 匹配;
-
memcpy() 相關函數中的 dst 和 src 指針重疊。
⑵Cachegrind:優化緩存的 “指南針”
Cachegrind 則專注於程序的緩存使用情況,爲我們優化程序性能提供了有力的支持。它就像是一個精準的指南針,能夠幫助我們找到代碼中與緩存相關的問題,指引我們優化的方向。在現代計算機體系結構中,CPU 的速度遠遠快於內存的速度,緩存的作用就顯得尤爲重要。如果程序不能有效地利用緩存,頻繁地從內存中讀取數據,就會導致 CPU 等待,從而大大降低程序的性能。Cachegrind 通過模擬 CPU 的緩存行爲,詳細地統計緩存的命中和未命中情況,爲我們提供諸如指令計數、緩存未命中次數、內存引用次數等關鍵信息。這些信息就像是寶藏地圖上的標記,讓我們能夠清楚地看到程序中哪些部分的緩存利用率不高,進而針對性地進行優化,比如調整數據結構、優化算法,以提高緩存命中率,提升程序的運行速度。
⑶Callgrind:函數調用的 “透視鏡”
Callgrind 主要用於分析程序中函數的調用過程,如同一個透視鏡,讓函數調用的細節一覽無餘。它能夠收集函數調用的相關數據,建立起函數調用關係圖,清晰地展示出各個函數之間的調用層次和頻率。這對於理解程序的執行流程、發現潛在的性能瓶頸非常有幫助。
在大型項目中,函數之間的調用關係錯綜複雜,很難直觀地看出哪些函數的調用開銷較大,哪些函數被頻繁調用但實際上可以進行優化。Callgrind 不僅可以告訴我們這些信息,還能提供每個函數執行的指令數、緩存使用情況等詳細數據。通過分析這些數據,我們可以找出那些佔用大量 CPU 資源的 “熱點” 函數,對它們進行優化,比如減少不必要的函數調用、優化函數內部的算法,從而提升整個程序的性能。
⑷Helgrind:多線程程序的 “守護者”
在多線程編程的世界裏,線程之間的同步與競爭問題就像是隱藏在暗處的 “幽靈”,隨時可能導致程序出現難以捉摸的錯誤。Helgrind 就是專門用來驅趕這些 “幽靈” 的 “守護者”。它致力於檢查多線程程序中出現的競爭問題,通過先進的算法,仔細監測內存中被多個線程訪問的區域,一旦發現沒有正確加鎖或同步的情況,就會及時發出警報。
這些競爭問題往往會導致程序出現死鎖、數據不一致等嚴重錯誤,而且由於它們的出現具有不確定性,很難通過常規的調試手段發現。Helgrind 的出現,爲多線程程序的調試帶來了極大的便利,讓我們能夠提前發現並解決這些潛在的問題,確保多線程程序的正確性和穩定性。
⑸Massif:內存使用的 “分析師”
Massif 是一位專業的 “分析師”,專注於程序的堆棧內存使用情況。它能夠精確地測量程序在運行過程中堆棧內存的使用量,詳細地告訴我們堆塊、堆管理塊和棧的大小,以及內存的分配和釋放情況。對於那些需要嚴格控制內存使用的程序,比如嵌入式系統開發、服務器端程序等,Massif 的作用尤爲突出。
通過它提供的信息,我們可以深入瞭解程序的內存使用行爲,發現內存泄漏、內存過度分配等問題,並進行鍼對性的優化。例如,我們可以根據 Massif 的報告,調整數據結構的大小、優化內存分配策略,以減少內存的佔用,提高程序的運行效率,避免因內存不足而導致的程序崩潰或性能下降。
三、Valgrind 安裝與配置
3.1 不同系統下的安裝方法
安裝 Valgrind 其實並不複雜,不過不同的操作系統下,安裝方式還是略有差異的。下面,我就來給大家詳細介紹一下。
在 Linux 系統下,安裝 Valgrind 就像是一場輕鬆的旅行。以常見的 Ubuntu 系統爲例,我們只需打開終端,輸入以下幾條命令:
sudo apt-get update
sudo apt-get install valgrind
簡單幾步,就能輕鬆搞定安裝,是不是超級方便?這就好比在應用商店裏一鍵下載安裝軟件一樣便捷,讓你快速擁有這款強大的工具。
對於 Windows 用戶來說,由於 Valgrind 本身是基於 Linux 開發的,所以不能直接在 Windows 上安裝原生版本。不過別擔心,我們可以藉助 Windows 下的 Linux 子系統(WSL)來使用它。首先,按照微軟官方的教程安裝 WSL,安裝完成後,在 WSL 的終端中,使用和 Linux 系統下類似的命令安裝 Valgrind。就像是在 Windows 系統裏開闢了一塊 “Linux 小天地”,讓 Valgrind 在其中順暢運行,爲我們的 Windows 編程保駕護航。
Mac 用戶也有自己的安裝方式。我們可以使用 Homebrew 這個強大的包管理器來安裝 Valgrind,只需在終端中輸入:
brew install valgrind
這就像是用一把萬能鑰匙打開了軟件安裝的大門,Homebrew 會自動幫我們處理好所有的依賴關係,輕鬆完成安裝,讓我們在 Mac 上也能盡情享受 Valgrind 帶來的便利。
3.2 配置要點
安裝好 Valgrind 後,還需要進行一些簡單的配置,才能讓它更好地發揮作用。在編譯我們的程序時,記得要打開調試模式,這就像是給程序戴上了一個 “智能手環”,可以記錄更多的運行信息,方便 Valgrind 進行分析。以 gcc 編譯器爲例,我們需要加上 “-g” 選項,像這樣:
gcc -g -o myprog myprog.c
另外,爲了避免編譯優化影響 Valgrind 的檢測結果,最好關閉編譯優化選項。因爲有些優化可能會改變程序的執行順序,讓 Valgrind 難以準確找到問題所在。在 gcc 中,我們可以使用 “-O0” 選項來關閉優化,就像這樣:
gcc -g -O0 -o myprog myprog.c
完成這些配置後,我們就可以讓 Valgrind 閃亮登場,開啓代碼的 “體檢” 之旅啦。
3.3 檢測內存泄漏
終端進入可執行文件所在的文件夾,輸入
valgrind --tool=memcheck
--leak-check=full
--show-leak-kinds=all
--undef-value-errors=no
--log-file=log ./可執行文件名
即可在終端所在文件夾下生成 log 文件, 在 log 文件最後會有個 summary,其中對內存泄露進行了分類,總共有五類:
-
“definitely lost” 意味着你的程序一定存在內存泄露;
-
”indirectly lost” 意味着你的程序一定存在內存泄露,並且泄露情況和指針結構相關
-
“possibly lost” 意味着你的程序一定存在內存泄露,除非你是故意進行着不符合常規的操作,例如將指針指向某個已分配內存塊的中間位置。
-
“still reachable” 意味着你的程序可能是沒問題的,但確實沒有釋放掉一些本可以釋放的內存。這種情況是很常見的,並且通常基於合理的理由。
-
”suppressed” 意味着有些泄露信息被壓制了。在默認的 suppression 文件中可以看到一些 suppression 相關設置。
其中,如果二叉樹的根節點被判定爲”definitely lost”,則其所有子節點將被判定爲”indirectly lost”,而如果你正確修復了類型爲 “definitely lost” 的根節點泄露,那麼類型爲 “indirectly lost” 的子節點泄露也會隨着消失。
對於以上的情況,posslbly lost 其實並沒有造成內存上的影響,如果想要過濾掉該類報告信息,可以加入 --show-possibly-lost=no ,而對於”still reachable” ,同樣可以通過 --show-reachable=yes 來控制是否輸出相應的信息。如果某些需要的庫沒有找到,用指令進行添加:
export LD_LIBRARY_PATH=/usr/local/mysql/lib:$LD_LIBRARY_PATH
查看發生泄露的具體位置
在 log 中由 summary 往上翻即可看到對應的錯誤,錯誤是不斷細化的,比如:
這樣的是一個錯誤,先告訴你出現了多少的內存泄露,然後從最裏層不斷往外部函數顯示:先說是 calloc 造成的錯誤,然後不斷往外部函數顯示。可以從下往上進行查看,比如先說 main() 函數發生了泄露,往上看到是 main() 中的 init() 函數,再往上 init() 中的 init_detectionmodel,如此不斷細定位泄露位置。
四、Valgrind 工作原理
Memcheck 能夠檢測出內存問題,關鍵在於其建立了兩個全局表。Valid-Value 表對於進程的整個地址空間中的每一個字節 (byte),都有與之對應的 8 個 bits;對於 CPU 的每個寄存器,也有一個與之對應的 bit 向量。這些 bits 負責記錄該字節或者寄存器值是否具有有效的、已初始化的值。
Valid-Address 表:對於進程整個地址空間中的每一個字節 (byte),還有與之對應的 1 個 bit,負責記錄該地址是否能夠被讀寫。
檢測原理:當要讀寫內存中某個字節時,首先檢查這個字節對應的 A bit。如果該 A bit 顯示該位置是無效位置,memcheck 則報告讀寫錯誤。
內核(core)類似於一個虛擬的 CPU 環境,這樣當內存中的某個字節被加載到真實的 CPU 中時,該字節對應的 V bit 也被加載到虛擬的 CPU 環境中。一旦寄存器中的值,被用來產生內存地址,或者該值能夠影響程序輸出,則 memcheck 會檢查對應的 V bits,如果該值尚未初始化,則會報告使用未初始化內存錯誤。
五、Valgrind 命令介紹
用法 valgrind[options] prog-and-args [options] 常用選項,適用於所有 Valgrind 工具:
-
-tool= 最常用的選項。運行 valgrind 中名爲 toolname 的工具。默認 memcheck。
-
h –help 顯示幫助信息。
-
-version 顯示 valgrind 內核的版本,每個工具都有各自的版本。
-
q –quiet 安靜地運行,只打印錯誤信息。
-
v –verbose 更詳細的信息, 增加錯誤數統計。
-
-trace-children=no|yes 跟蹤子線程? [no]
-
-track-fds=no|yes 跟蹤打開的文件描述?[no]
-
-time-stamp=no|yes 增加時間戳到 LOG 信息? [no]
-
-log-fd= 輸出 LOG 到描述符文件 [2=stderr]
-
-log-file= 將輸出的信息寫入到 filename.PID 的文件裏,PID 是運行程序的進行 ID
-
-log-file-exactly= 輸出 LOG 信息到 file
-
-log-file-qualifier= 取得環境變量的值來做爲輸出信息的文件名。[none]
-
-log-socket=ipaddr:port 輸出 LOG 到 socket ,ipaddr:port
LOG 信息輸出:
-
-xml=yes 將信息以 xml 格式輸出,只有 memcheck 可用
-
-num-callers= show callers in stack traces [12]
-
-error-limit=no|yes 如果太多錯誤,則停止顯示新錯誤? [yes]
-
-error-exitcode= 如果發現錯誤則返回錯誤代碼 [0=disable]
-
-db-attach=no|yes 當出現錯誤,valgrind 會自動啓動調試器 gdb。[no]
-
-db-command= 啓動調試器的命令行選項 [gdb -nw %f %p]
適用於 Memcheck 工具的相關選項:
-
-leak-check=no|summary|full 要求對 leak 給出詳細信息? [summary]
-
-leak-resolution=low|med|high how much bt merging in leak check [low]
-
-show-reachable=no|yes show reachable blocks in leak check? [no]
六、使用 Valgrind 進行內存調試
6.1 基本命令參數解析
瞭解了 Valgrind 的安裝和配置,下面我們就來看看如何在實戰中使用它。使用 Valgrind 的基本命令格式如下:
valgrind [options] program [arguments]
其中,[options] 是一系列的參數,用來控制 Valgrind 的行爲,program 是我們要檢測的程序,[arguments] 則是程序運行所需的參數。
下面,給大家介紹幾個常用的參數。首先是 “--tool=memcheck”,這個參數指定使用 Memcheck 工具,它是 Valgrind 中最常用的工具,用於檢測各種內存問題,如果你不確定程序具體存在哪種內存問題,使用這個參數準沒錯。
“--leak-check” 參數用於檢測內存泄漏,它有幾個可選的值,“no” 表示不檢查內存泄漏,“summary” 僅顯示內存泄漏的摘要信息,而 “full” 則會顯示所有內存泄漏的詳細信息,包括泄漏的內存位置、大小等,方便我們深入排查問題,一般在調試階段,建議使用 “full” 模式,以獲取最全面的信息。
還有 “--track-origins=yes”,這個參數非常實用,它可以幫助我們追蹤未初始化內存的使用情況,讓我們清楚地知道未初始化的內存是在哪裏被創建的,以及在哪些地方被使用,對於找出那些因使用未初始化內存而導致的詭異問題特別有幫助。
6.2 應用實踐
下面通過介紹幾個範例來說明如何使用 Memcheck ,示例僅供參考,更多用途可在實際應用中不斷探索。
⑴數組越界 / 內存未釋放
#include<stdlib.h>
void k(void)
{
int *x = malloc(8 * sizeof(int));
x[9] = 0; //數組下標越界
} //內存未釋放
int main(void)
{
k();
return 0;
}
①編譯程序 test.c
gcc -Wall test.c -g -o test#Wall提示所有告警,-g gdb,-o輸出
②使用 Valgrind 檢查程序 BUG
valgrind --tool=memcheck --leak-check=full ./test
#--leak-check=full 所有泄露檢查
③運行結果如下:
==2989== Memcheck, a memory error detector
==2989== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward
et al.
==2989== Using Valgrind-3.8.1 and LibVEX; rerun with -h for
copyright info
==2989== Command: ./test
==2989==
==2989== Invalid write of size 4
==2989== at 0x4004E2: k (test.c:5)
==2989== by 0x4004F2: main (test.c:10)
==2989== Address 0x4c27064 is 4 bytes after a block of size 32 alloc'd
==2989== at 0x4A06A2E: malloc (vg_replace_malloc.c:270)
==2989== by 0x4004D5: k (test.c:4)
==2989== by 0x4004F2: main (test.c:10)
==2989==
==2989==
==2989== HEAP SUMMARY:
==2989== in use at exit: 32 bytes in 1 blocks
==2989== total heap usage: 1 allocs, 0 frees, 32 bytes allocated
==2989==
==2989== 32 bytes in 1 blocks are definitely lost in loss record 1
of 1
==2989== at 0x4A06A2E: malloc (vg_replace_malloc.c:270)
==2989== by 0x4004D5: k (test.c:4)
==2989== by 0x4004F2: main (test.c:10)
==2989==
==2989== LEAK SUMMARY:
==2989== definitely lost: 32 bytes in 1 blocks
==2989== indirectly lost: 0 bytes in 0 blocks
==2989== possibly lost: 0 bytes in 0 blocks
==2989== still reachable: 0 bytes in 0 blocks
==2989==suppressed: 0 bytes in 0 blocks
==2989==
==2989== For counts of detected and suppressed errors, rerun with: -v
==2989== ERROR SUMMARY: 2 errors from 2 contexts
(suppressed: 6 from 6)
⑵內存釋放後讀寫
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *p = malloc(1); //分配
*p = 'a';
char c = *p;
printf("\n [%c]\n",c);
free(p); //釋放
c = *p; //取值
return 0;
}
①編譯程序 t2.c
gcc -Wall t2.c -g -o t2
②使用 Valgrind 檢查程序 BUG
valgrind --tool=memcheck --leak-check=full ./t2
③運行結果如下:
==3058== Memcheck, a memory error detector
==3058== Copyright (C) 2002-2012, and GNU GPL'd, by Julian
Seward et al.
==3058== Using Valgrind-3.8.1 and LibVEX; rerun with -h
for copyright info
==3058== Command: ./t2
==3058==
[a]
==3058== Invalid read of size 1
==3058== at 0x4005A3: main (t2.c:14)
==3058== Address 0x4c27040 is 0 bytes inside a block of size
1 free'd
==3058== at 0x4A06430: free (vg_replace_malloc.c:446)
==3058== by 0x40059E: main (t2.c:13)
==3058==
==3058==
==3058== HEAP SUMMARY:
==3058== in use at exit: 0 bytes in 0 blocks
==3058== total heap usage: 1 allocs, 1 frees, 1 bytes allocated
==3058==
==3058== All heap blocks were freed -- no leaks are possible
==3058==
==3058== For counts of detected and suppressed errors, rerun with:
-v
==3058== ERROR SUMMARY: 1 errors from 1 contexts
(suppressed: 6 from 6)
從上輸出內容可以看到,Valgrind檢測到無效的讀取操作然後輸出“Invalid read of size 1”。
⑶無效讀寫
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *p = malloc(1); //分配1字節
*p = 'a';
char c = *(p+1); //地址加1
printf("\n [%c]\n",c);
free(p);
return 0;
}
①編譯程序 t3.c
gcc -Wall t3.c -g -o t3
②使用 Valgrind 檢查程序 BUG
valgrind --tool=memcheck --leak-check=full ./t3
③運行結果如下:
==3128== Memcheck, a memory error detector
==3128== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.
==3128== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==3128== Command: ./t3
==3128==
==3128== Invalid read of size 1 #無效讀取
==3128==at 0x400579: main (t3.c:9)
==3128==Address 0x4c27041 is 0 bytes after a block of size 1 alloc'd
==3128==at 0x4A06A2E: malloc (vg_replace_malloc.c:270)
==3128==by 0x400565: main (t3.c:6)
==3128==
[]
==3128==
==3128== HEAP SUMMARY:
==3128==in use at exit: 0 bytes in 0 blocks
==3128==total heap usage: 1 allocs, 1 frees, 1 bytes allocated
==3128==
==3128== All heap blocks were freed -- no leaks are possible
==3128==
==3128== For counts of detected and suppressed errors, rerun with: -v
==3128== ERROR SUMMARY: 1 errors from 1 contexts
(suppressed: 6 from 6)
⑷內存泄露
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p = malloc(1);
*p = 'x';
char c = *p;
printf("%c\n",c); //申請後未釋放
return 0;
}
①編譯程序 t4.c
gcc -Wall t4.c -g -o t4
②使用 Valgrind 檢查程序 BUG
valgrind --tool=memcheck --leak-check=full ./t4
③運行結果如下:
==3221== Memcheck, a memory error detector
==3221== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.
==3221== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==3221== Command: ./t4
==3221==
==3221== Invalid write of size 4
==3221==at 0x40051E: main (t4.c:7)
==3221==Address 0x4c27040 is 0 bytes inside a block of size 1 alloc'd
==3221==at 0x4A06A2E: malloc (vg_replace_malloc.c:270)
==3221==by 0x400515: main (t4.c:6)
==3221==
==3221== Invalid read of size 4
==3221==at 0x400528: main (t4.c:8)
==3221==Address 0x4c27040 is 0 bytes inside a block of size 1 alloc'd
==3221==at 0x4A06A2E: malloc (vg_replace_malloc.c:270)
==3221==by 0x400515: main (t4.c:6)
==3221==
x
==3221==
==3221== HEAP SUMMARY:
==3221==in use at exit: 1 bytes in 1 blocks
==3221==total heap usage: 1 allocs, 0 frees, 1 bytes allocated
==3221==
==3221== 1 bytes in 1 blocks are definitely lost in loss record 1 of 1
==3221==at 0x4A06A2E: malloc (vg_replace_malloc.c:270)
==3221==by 0x400515: main (t4.c:6)
==3221==
==3221== LEAK SUMMARY:
==3221==definitely lost: 1 bytes in 1 blocks
==3221==indirectly lost: 0 bytes in 0 blocks
==3221== possibly lost: 0 bytes in 0 blocks
==3221==still reachable: 0 bytes in 0 blocks
==3221== suppressed: 0 bytes in 0 blocks
==3221==
==3221== For counts of detected and suppressed errors, rerun with: -v
==3221== ERROR SUMMARY: 3 errors from 3 contexts
(suppressed: 6 from 6)
從檢查結果看,可以發現內存泄露。
⑸內存多次釋放
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *p;
p=(char *)malloc(100);
if(p)
printf("Memory Allocated at: %s/n",p);
else
printf("Not Enough Memory!/n");
free(p); //重複釋放
free(p);
free(p);
return 0;
}
①編譯程序 t5.c
gcc -Wall t5.c -g -o t5
②使用 Valgrind 檢查程序 BUG
valgrind --tool=memcheck --leak-check=full ./t5
③運行結果如下:
==3294== Memcheck, a memory error detector
==3294== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward
et al.
==3294== Using Valgrind-3.8.1 and LibVEX; rerun with -h for
copyright info
==3294== Command: ./t5
==3294==
==3294== Conditional jump or move depends on uninitialised value(s)
==3294== at 0x3CD4C47E2C: vfprintf (in /lib64/libc-2.12.so)
==3294== by 0x3CD4C4F189: printf (in /lib64/libc-2.12.so)
==3294== by 0x400589: main (t5.c:9)
==3294==
==3294== Invalid free() / delete / delete[] / realloc()
==3294== at 0x4A06430: free (vg_replace_malloc.c:446)
==3294== by 0x4005B5: main (t5.c:13)
==3294== Address 0x4c27040 is 0 bytes inside a block of size
100 free'd
==3294== at 0x4A06430: free (vg_replace_malloc.c:446)
==3294== by 0x4005A9: main (t5.c:12)
==3294==
==3294== Invalid free() / delete / delete[] / realloc()
==3294== at 0x4A06430: free (vg_replace_malloc.c:446)
==3294== by 0x4005C1: main (t5.c:14)
==3294== Address 0x4c27040 is 0 bytes inside a block of size
100 free'd
==3294== at 0x4A06430: free (vg_replace_malloc.c:446)
==3294== by 0x4005A9: main (t5.c:12)
==3294==
Memory Allocated at: /n==3294==
==3294== HEAP SUMMARY:
==3294== in use at exit: 0 bytes in 0 blocks
==3294== total heap usage: 1 allocs, 3 frees, 100 bytes allocated
從上面的輸出可以看到 (標註), 該功能檢測到我們對同一個指針調用了 3 次釋放內存操作。
⑹內存動態管理
常見的內存分配方式分三種:靜態存儲,棧上分配,堆上分配。全局變量屬於靜態存儲,它們是在編譯時就被分配了存儲空間,函數內的局部變量屬於棧上分配,而最靈活的內存使用方式當屬堆上分配,也叫做內存動態分配了。常用的內存動態分配函數包括:malloc, alloc, realloc, new 等,動態釋放函數包括 free, delete。
一旦成功申請了動態內存,我們就需要自己對其進行內存管理,而這又是最容易犯錯誤的。下面的一段程序,就包括了內存動態管理中常見的錯誤。
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
int i;
char* p = (char*)malloc(10);
char* pt=p;
for(i = 0;i < 10;i++)
{
p[i] = 'z';
}
free(p);
pt[1] = 'x';
free(pt);
return 0;
}
①編譯程序 t6.c
gcc -Wall t6.c -g -o t6
②使用 Valgrind 檢查程序 BUG
valgrind --tool=memcheck --leak-check=full ./t6
③運行結果如下:
==3380== Memcheck, a memory error detector
==3380== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.
==3380== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==3380== Command: ./t6
==3380==
==3380== Invalid write of size 1
==3380==at 0x40055C: main (t6.c:14)
==3380==Address 0x4c27041 is 1 bytes inside a block of size 10 free'd
==3380==at 0x4A06430: free (vg_replace_malloc.c:446)
==3380==by 0x400553: main (t6.c:13)
==3380==
==3380== Invalid free() / delete / delete[] / realloc()
==3380==at 0x4A06430: free (vg_replace_malloc.c:446)
==3380==by 0x40056A: main (t6.c:15)
==3380==Address 0x4c27040 is 0 bytes inside a block of size 10 free'd
==3380==at 0x4A06430: free (vg_replace_malloc.c:446)
==3380==by 0x400553: main (t6.c:13)
==3380==
==3380==
==3380== HEAP SUMMARY:
==3380==in use at exit: 0 bytes in 0 blocks
==3380==total heap usage: 1 allocs, 2 frees, 10 bytes allocated
申請內存在使用完成後就要釋放。如果沒有釋放,或少釋放了就是內存泄露;多釋放也會產生問題。上述程序中,指針 p 和 pt 指向的是同一塊內存,卻被先後釋放兩次。系統會在堆上維護一個動態內存鏈表,如果被釋放,就意味着該塊內存可以繼續被分配給其他部分,如果內存被釋放後再訪問,就可能覆蓋其他部分的信息,這是一種嚴重的錯誤,上述程序第 14 行中就在釋放後仍然寫這塊內存。
輸出結果顯示,第 13 行分配和釋放函數不一致;第 14 行發生非法寫操作,也就是往釋放後的內存地址寫值;第 15 行釋放內存函數無效。
七、多線程程序調試
在多線程編程的世界裏,線程之間的同步與競爭問題就像是隱藏在暗處的 “幽靈”,隨時可能導致程序出現難以捉摸的錯誤。Valgrind 中的 Helgrind 和 DRD 工具,就是專門用來驅趕這些 “幽靈” 的 “守護者”。
Helgrind 致力於檢查多線程程序中出現的競爭問題,它通過先進的算法,仔細監測內存中被多個線程訪問的區域,一旦發現沒有正確加鎖或同步的情況,就會及時發出警報。比如說,我們有一個多線程程序,多個線程同時對一個共享變量進行讀寫操作,卻沒有使用任何鎖來保護這個共享變量,這就很可能導致數據不一致的問題。運行 Helgrind,它就能精準地檢測到這種潛在的風險,輸出類似這樣的報告:
==12345== Possible data race during write of size 4 at 0x... by thread #1
==12345== at 0x... increment_counter
==12345== by 0x... start_thread...
這份報告清晰地指出了在哪個線程、哪個函數中發生了可能的數據競爭,讓我們能夠迅速定位問題。
DRD 同樣是檢測多線程程序問題的得力助手,它專注於查找數據競爭、死鎖等併發錯誤。它會對程序的執行過程進行全面的 “掃描”,一旦發現可疑的併發問題,就會給出詳細的提示。例如,在一個複雜的多線程程序中,多個線程之間存在複雜的鎖依賴關係,如果不小心出現了死鎖的情況,DRD 就能及時察覺,幫助我們找出導致死鎖的鎖獲取順序,讓我們能夠調整代碼,避免死鎖的發生。
下面,我們通過一個具體的例子來看看它們的實戰效果。假設我們有以下一段多線程 C++ 代碼:
#include <thread>
#include <iostream>
#include <vector>
#include <mutex>
std::vector<int> shared_data;
std::mutex mtx;
void thread_function() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
shared_data.push_back(rand());
lock.unlock();
}
}
int main() {
std::thread t1(thread_function);
std::thread t2(thread_function);
t1.join();
t2.join();
return 0;
}
在這段代碼中,兩個線程都在向共享的 shared_data 向量中添加隨機數,雖然使用了互斥鎖 mtx 來保護共享數據的訪問,但在實際複雜的多線程環境下,可能還存在一些隱藏的問題。
我們使用 Helgrind 來檢測這個程序,在終端中輸入:
valgrind --tool=helgrind./test
Helgrind 運行後,可能會給出一些關於鎖使用的建議,比如是否存在鎖競爭、鎖的粒度是否合適等信息,幫助我們進一步優化代碼,確保多線程程序的穩定性。
如果我們使用 DRD 來檢測,輸入:
valgrind --tool=drd./test
DRD 可能會從不同的角度發現一些潛在的併發問題,比如是否存在某個線程長時間持有鎖,導致其他線程阻塞,影響程序的併發性能等。通過這兩個工具的雙重保障,我們能夠更加全面地排查多線程程序中的問題,讓程序在多線程環境下穩定高效地運行。
7.1 性能剖析功能
除了內存調試,Valgrind 在性能剖析方面也有着出色的表現,能幫我們深挖程序性能瓶頸,讓程序 “跑” 得更快。
Callgrind 是性能剖析的得力工具,它就像程序的 “動態心電圖”,能詳細記錄程序運行時函數的調用情況與 CPU 指令執行信息。運行程序時加上 “--tool=callgrind” 參數,它會生成一個包含豐富數據的文件,像函數調用次數、每個函數執行的指令數等。藉助 callgrind_annotate 命令或 KCachegrind 圖形界面工具查看分析結果,那些 “喫” CPU 資源多的函數便無所遁形。比如開發一個圖形渲染程序,用 Callgrind 分析後發現某個複雜的光照計算函數佔用大量 CPU 時間,對其算法優化或採用更高效的數學庫後,程序渲染速度大幅提升。
Cachegrind 則專注於緩存使用分析,是優化緩存命中率的 “好幫手”。它模擬 CPU 緩存行爲,精確統計緩存命中、未命中次數,還涵蓋指令計數、內存引用次數等關鍵指標。執行程序加上 “--tool=cachegrind” 參數,會得到詳細記錄緩存信息的文件,用 cg_annotate 工具查看,能清晰知曉程序哪些部分緩存利用率低。如處理大規模圖像數據時,發現頻繁從內存加載數據導致緩存未命中率高,通過調整數據結構爲連續存儲,或優化算法減少數據跨緩存行訪問,就能提高緩存命中率,讓程序運行如 “閃電” 般迅速。
7.2Valgrind 的侷限性
儘管 Valgrind 如此強大,但它也並非十全十美,存在一些侷限性。就像再好的醫生也有棘手的病症一樣,Valgrind 在某些複雜的情況下,也可能會 “力不從心”。
比如說,對於一些靜態分配或在堆棧上分配的數組的超出範圍的讀取或寫入,Valgrind 可能無法檢測到。這就需要我們在編寫代碼時,依然要保持警惕,不能完全依賴工具。另外,在檢測某些複雜的內存錯誤場景時,可能會出現誤報或漏報的情況,需要我們結合代碼邏輯仔細甄別。但即便存在這些小瑕疵,也絲毫不能掩蓋 Valgrind 在內存調試和性能分析領域的卓越光芒,它依然是我們編程路上最得力的助手之一。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Lw3L55bIxH90I0woao6P6Q