Stories

Detail Return Return

【C/C++實用工具】內存相關問題排查工具---cppcheck與valgrind - Stories Detail

C++中令人疑惑的內存問題

C++的內存問題時常令人十分困惑。總結起來C++的內存問題可以分為以下幾類

  1. 內存泄露
    當程序員使用new(或malloc)關鍵字分配內存而忘記使用 delete (或free)函數或 delete[] 運算符釋放內存時,C++ 中就會發生內存泄漏。在 C++ 中使用錯誤的刪除運算符會發生內存泄漏最多的情況之一。delete 運算符應用於釋放單個分配的內存空間,而 delete [] 運算符應用於釋放數據值數組。內存泄漏對於很多不能停機的程序是致命的。它會導致內存使用量就不斷增加,直至程序宕機。
  2. 內存越界
    內存越界的背後其實是訪問異常的內存位置。內存越界導致的問題往往讓C++程序員十分困惑的問題,因為由內存越界導致程序出現crash的位置往往不是真正導致程序crash的位置,這給排查內存越界帶來很大的困難。
  3. 訪問未初始化對象
    與內存越界類似,訪問未初始化的對象往往也會導致程序在其他位置crash。而產生crash的位置往往並不是導致crash原因。

對於一個嚴謹的程序員是絕不能讓內存訪問問題帶到正式發佈版本的。因此需要一些工具來幫助我們排查內存相關問題。本文的示例代碼為內存泄漏。

靜態檢查工具---Cppcheck

CppCheck是一個C/C++代碼缺陷靜態檢查工具。不同於C/C++編譯器及其它分析工具,CppCheck只檢查編譯器檢查不出來的bug,不檢查語法錯誤。所謂靜態代碼檢查就是使用一個工具檢查我們寫的代碼是否安全和健壯,是否有隱藏的問題。

Cppcheck的安裝

在Ubuntu下只需要運行sudo apt install cppcheck即可安裝。安裝成功後,通過cppcheck --version命令,可以檢查是否安裝成功,以及查看版本。作者安裝的版本是1.82。

Cppcheck的使用

cppcheck的使用也十分簡單對於項檢查的文件只需要輸入cppcheck ${filename}即可。
例如在內存泄漏示例中,對main.cpp進行靜態代碼檢查,會發現如下問題:

~/Code/leak_example$ cppcheck main.cpp 
cppcheck main.cpp 
Checking main.cpp ...
[main.cpp:13]: (error) Array 'b[10]' accessed at index 99, which is out of bounds.
[main.cpp:35]: (error) Memory leak: a

會發現在main.cpp第13行,出現了數組越界的問題,同時會發現代碼中沒有回收在開始時分配的內存。對於源代碼分散在多個目錄下的項目,可以直接把根目錄作為cppcheck命令的參數,這樣cppcheck就可以遞歸的檢查該目錄下所有的文件。

但是僅僅是靜態代碼檢測是不足夠的,細心地讀者應該發現了其他的內存使用問題。事實上確實有很多問題不運行是難以發現的,因此需要valgrind進行更加完備的內存訪問檢測。

動態檢查工具---valgrind

Valgrind 是一個用於構建動態分析工具的儀器框架。 Valgrind 工具可以自動檢測許多內存管理和線程錯誤,並詳細分析您的程序。您還可以使用 Valgrind 構建新工具。

Valgrind 發行版目前包括七個生產質量工具:一個內存錯誤檢測器、兩個線程錯誤檢測器、一個緩存和分支預測分析器、一個調用圖生成緩存和分支預測分析器,以及兩個不同的堆分析器。可見valgrind是一個非常強大的CPP分析工具,在本文中主要介紹如何用valgrind進行內存問題的排查。

valgrind安裝

在Ubuntu系統中只需運行sudo apt install valgrind即可開啓內存檢測之旅。同樣可以使用valgrind --version來檢查是否安裝成功並檢查版本。作者安裝的版本是3.13.0。

valgrind使用和報告説明

在使用valgrind進行內存泄漏檢測時一定要用debug模式編譯項目,否則valgrind無法獲取問題出現的文件行數。編譯完之後運行valgrind ./${executablefile}就可以對生成的可執行文件進行檢測了。valgrind將會執行該文件,並記錄下內存使用問題的位置。在我們檢測示例得到的可執行文件,可以得到以下報告(為了節省篇幅,只截取報告問題的部分)。

==12288== Invalid write of size 4
==12288==    at 0x1091B1: main (main.cpp:9)
==12288==  Address 0x5b7fc84 is 0 bytes after a block of size 4 alloc'd
==12288==    at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12288==    by 0x109176: main (main.cpp:7)


==12288== Invalid read of size 8
==12288==    at 0x109C0A: __gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >::__normal_iterator(int* const&) (stl_iterator.h:783)
==12288==    by 0x109819: std::vector<int, std::allocator<int> >::begin() (stl_vector.h:564)
==12288==    by 0x10C522: Printer<int>::print() (printer.cpp:7)
==12288==    by 0x109203: main (main.cpp:17)
==12288==  Address 0x0 is not stack'd, malloc'd or (recently) free'd

==12288== Process terminating with default action of signal 11 (SIGSEGV)
==12288==  Access not within mapped region at address 0x0
==12288==    at 0x109C0A: __gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >::__normal_iterator(int* const&) (stl_iterator.h:783)
==12288==    by 0x109819: std::vector<int, std::allocator<int> >::begin() (stl_vector.h:564)
==12288==    by 0x10C522: Printer<int>::print() (printer.cpp:7)
==12288==    by 0x109203: main (main.cpp:17)

報告中主要發現了兩個問題,,一個是位於main.cpp:9的內存訪問越界,另一個是位於main.cpp:17的對空指針的成員函數調用。其中第一個錯誤是因為在分配內存時,錯誤的使用sizeof(),只分配了4字節的容量,卻將其誤用為大小為100個int大小的內存區域。第二個錯誤是因為使用了空指針的成員變量(member,printer.cpp:7)。修正這兩個錯誤,繼續檢測該程序。

==13652== HEAP SUMMARY:
==13652==     in use at exit: 400 bytes in 1 blocks
==13652==   total heap usage: 16 allocs, 15 frees, 74,500 bytes allocated

依然檢測到異常信息,但是並沒有定位到有效位置,只定位到了程序的最後一行。這時需要增加選項--leak-check=full --show-leak-kinds=all,展示更多信息。最終得到更多的信息:

==12568== 400 bytes in 1 blocks are still reachable in loss record 1 of 1
==12568==    at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12568==    by 0x109176: main (main.cpp:7)

原來是程序開始時(main.cpp:7)分配的內存,沒有被回收,與cppcheck檢測結果一致。

valgrind的侷限性

修正錯誤之後依然會發現下面的錯誤,這是因為訪問數組b時出現了越界。然而valgrind沒有定位到具體位置,只是報了一個異常退出的錯誤。這裏就不得不提到valgrind自身的侷限性:不能檢測在棧上分配內存的使用正確性。因此對於這種情況,可以結合cppcheck來共同檢查,確保內存使用的正確性。

*** stack smashing detected ***: <unknown> terminated
==13774== 
==13774== Process terminating with default action of signal 6 (SIGABRT)
==13774==    at 0x541DFB7: raise (raise.c:51)
==13774==    by 0x541F920: abort (abort.c:79)
==13774==    by 0x5468966: __libc_message (libc_fatal.c:181)
==13774==    by 0x5513B60: __fortify_fail_abort (fortify_fail.c:33)
==13774==    by 0x5513B21: __stack_chk_fail (stack_chk_fail.c:29)
==13774==    by 0x10943F: main (main.cpp:28)
user avatar ZhongQianwen Avatar u_16231477 Avatar starrocks Avatar youqingyouyidedalianmao Avatar thinkerdjx Avatar jkkang Avatar duwenlong Avatar keen_626105e1ef632 Avatar greyham Avatar lradian Avatar wanmuc Avatar feixianghelanren Avatar
Favorites 18 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.