Valgrind *不是* 泄漏檢查工具

概要:

在我的社區中,Valgrind 是我已知的被誤解最深的工具。Valgrind 不僅僅是一個內存泄露檢查器。它只是包含了一個檢查內存泄露的工具而已。但我想說的是這個工具恰恰是 Valgrind 中用處最小的一個組件。

無需改變 Valgrind 的調用方式,你就能得到比大多數人想象的要多得多的極具價值的信息。 Valgrind 會在你的程序奔潰之前找出潛在的錯誤;它不僅告訴你錯誤在哪裏,還會告訴你原因(用英語哦). Valgrind 首先是一個未知行爲 檢測工具,其次他是一個函數和內存分析工具, 然後是一個數據競爭條件偵測工具, 它最後纔是一個內存泄露檢查工具。

泥牛
泥牛
翻譯於 3年前
3人頂
 翻譯得不錯哦!

首先也是最重要的:

要運行 Valgrind, 你只需切換到你程序所在的目錄然後運行如下命令:

valgrind ./myProgram myProgramsFirstArg myProgramsSecondArg

無需特殊的參數。

你將會同時看到你的程序的輸出,以及由 Valgrind 生成的調試輸出信息(那些 ‘==‘ 開頭的行)。如果你的程序在編譯生成時帶了 -g 選項(生成調試符號信息),Valgrind 將提供更多有幫助的信息(比方說執行代碼的行號)。

基於本文的目的, 請 忽略所有 Valgrind 輸出內容裏 "HEAD SUMMARY" 行之後的內容。 這正是本文不關心的部分:內容泄露摘要。

泥牛
泥牛
翻譯於 3年前
2人頂
 翻譯得不錯哦!

它能檢測到些什麼呢?

1) 誤用未初始化的值. 這也是它的基本功:

bool condition;
if (condition) {
  //Do the thing
}

有趣的是,大部分時間裏你的程序只是繼續運行,然後當運行到這裏時,毫無徵兆的出現運行失敗。 它可能(大多數時候)看似在按你預想的那樣的運行。理論上,如果你的程序有錯誤,那每次運行它它都應該出錯。這些錯誤是硬性的,很快就能顯現出來。只有先確定哪裏有錯誤,然後我們才能修復它。問題是我們從一開始就沒有賦予那個布爾變量任何值,它也不會被程序自動的初始化. 此時,它的值可能是任何恰好留在它的內存位置上的隨機的值。

上面實例中 Valgrind 會輸出這樣的行:

==2364== Conditional jump or move depends on uninitialized value(s)
==2364==    at 0x400916: main (test.cpp:106)

注意:上述輸出給出了代碼會引發未知行爲的原因,不光只是位置。更棒的是,Valgrind 在這些未知行爲引發程序崩潰之前就捕捉到了他們。

泥牛
泥牛
翻譯於 3年前
2人頂
 翻譯得不錯哦!

像上面那樣顯而易見的錯誤估計很難出現,但下面這個錯誤估計就沒那麼好發現了:

bool condition;
if (foo) {
  condition = true;
}
if (bar) {
  condition = false;
}
if (baz) {
  condition = true;
}
if (condition) {
  //Do the thing
}

這裏我們只有某些時候成功地初始化了condition,但不是全部。Valgrind仍然可以檢查出這些未定行爲。

使用某些防禦性編程的方法可以從根源避免這種錯誤。我比較傾向於給每一個變量一個初始值。或是使用auto關鍵字來強迫你去初始化某個變量(在沒有一個值的情況下,你不能推斷出那個變量的類型)。你可以看看Herb Sutter的博客 ,裏面提到了更多關於auto關鍵字的事情。

crab2313
crab2313
翻譯於 3年前
2人頂
 翻譯得不錯哦!

2) 操作你不該去碰的內存。讀寫從來沒被分配出來的內存,被釋放掉的內存;訪問超過一塊分配好的內存的邊界的內存;棧上不能讀寫的內存。

一個例子:

  vector<int> v { 12345 };
  v[5] = 0//Oops

你看到了麼?

如果我在我的計算機上運行這段程序,很可能沒有什麼問題。可能運行超過20次都不會掛掉一次,但是它絕對是錯的。即使我湊巧在使用GDB(一種其他的調試工具)調試它的時候它掛掉了,我最多能得到一個棧的調用記錄,但使它並不是造成這個問題的所在,而是這個問題的表現形式。

這裏是Valgrind對上邊問題的輸出:

==2710== Invalid write of size 4
==2710==    at 0x400961: foo() (test.cpp:85)
==2710==    by 0x4009A2: main (test.cpp:89)
==2710==  Address 0x5a1d054 is 0 bytes after a block of size 20 alloc'd
==2710==    at 0x4C2B0E0operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2710==    by 0x400EDF__gnu_cxx::new_allocator<int>::allocate(unsigned longvoid const*) (new_allocator.h:104)
==2710==    by 0x400DCEstd::_Vector_base<intstd::allocator<int> >::_M_allocate(unsigned long) (in /home/mark/test/a.out)
==2710==    by 0x400C5Fvoid std::vector<intstd::allocator<int> >::_M_range_initialize<int const*>(int const*, int const*, std::forward_iterator_tag) (stl_vector.h:1201)
==2710==    by 0x400AF4std::vector<intstd::allocator<int> >::vector(std::initializer_list<int>, std::allocator<intconst&) (stl_vector.h:368)
==2710==    by 0x400943: foo() (test.cpp:84)
==2710==    by 0x4009A2: main (test.cpp:89)

如果你對STL的棧調用不太熟悉,上面的東西並不好懂。讓我們仔細看看。

crab2313
crab2313
翻譯於 3年前
2人頂
 翻譯得不錯哦!

第一行告訴你爲什麼你的代碼會出現未定行爲。這裏有一個“Invalid write of size 4”。size 4意味着我寫入了一個4字節大的東西。在我的機器上,這可能是一個int類型。invalid意味着我碰了不該碰的的內存。這是一個差一錯誤:我寫入了超過我那個vector結尾的內存。

現在我們看看2,3行。這是Valgrind認爲你最感興趣的棧調用信息。確實,在這個例子中,出現問題的代碼在foo中,而main是調用foo的函數。

第四行更爲詳細地描述了“你越界地使用了內存”這個問題。、

剩下的部分是包括STL在內的更詳細的棧調用信息。事實上,問題從不出現在STL中。(好吧,幾乎從不。)

crab2313
crab2313
翻譯於 3年前
3人頂
 翻譯得不錯哦!

3) 誤用 std::memcpy 以及基於該函數構建的其他函數會導致你的源數組和目標數組地址重疊 (請先 閱讀我的這篇文章裏面解釋了爲什麼 std::memcpy 會被棄用,並牢記當你使用其他看似不錯的更高層級的抽象層時,你依然無法避免間接的調用到 std::memcpy)

這裏就不再給出此項和下一項的示例代碼了;我想在現代代碼裏這種情況已經不常見了,如果您不幸遭遇此類問題,簡單的運行 Valgrind 命令,無需任何參數,它就能把這兩類問題報告給您。

4) 無效的內存釋放 (在現代代碼中已經幾乎沒有了,總之您應該優先使用智能指針)

泥牛
泥牛
翻譯於 3年前
3人頂
 翻譯得不錯哦!

5) 數據競爭:

如果我運行如下命令:

valgrind --tool=helgrind ./myProgram

其中 myProgram 包含如下代碼:

  auto x = 0;
  thread([&] {
    ++x;
  }).detach();
  ++x;

我將得到如下的 Valgrind 反饋:

==2872== Possible data race during read of size 4 at 0xFFEFFFE8C by thread #1
==2872== Locks held: none
==2872==    at 0x401081: main (test.cpp:96)
==2872== 
==2872== This conflicts with a previous write of size 4 by thread #2
==2872== Locks held: none
==2872==    at 0x40103A: main::{lambda()#1}::operator()() const (test.cpp:94)
==2872==    by 0x401F2D: void std::_Bind_simple<main::{lambda()#1} ()>::_M_invoke
<>(std::_Index_tuple<>) (functional:1732)
==2872==    by 0x401E84: std::_Bind_simple<main::{lambda()#1} ()>::operator()() 
(functional:1720)
==2872==    by 0x401E1D: std::thread::_Impl<std::_Bind_simple<main::{lambda()#1} 
()> >::_M_run() (thread:115)
==2872==    by 0x4EEEBEF: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19)
==2872==    by 0x4C30E26: ??? (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linu
x.so)
==2872==    by 0x535F181: start_thread (pthread_create.c:312)
==2872==    by 0x566FEFC: clone (clone.S:111)

它告訴我,我的數據沒有得到適當的保護。沒有通過互斥鎖的同步,我就共享了數據。

我必須得說,儘管它偵測到了代碼中錯誤,但是它的輸出中依然包含了一堆不精確的診斷(這裏它打印出一堆 std::shared_ptr 被 std::thread 內部調用之類的過於冗餘的信息)。看起來 Valgrind 還需要在信息篩選方面再接再厲。當然你也可以寫個簡單的 D 腳本或者 Python 腳本來幫 helgrind 過濾出有用的信息。

泥牛
泥牛
翻譯於 3年前
2人頂
 翻譯得不錯哦!

6) 歐耶... 內存泄露了, 你不會還沒啓用智能指針吧。

運行:

valgrind --leak-check=full ./myProgram

(如果你忘了是哪個參數的話,只要像往常那樣運行一次 Valgrind,它會在輸出中的內存摘要部分提醒你的)

針對如下行:

auto x = new int(5);

Valgrind 會有如下輸出:

==2881== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==2881==    at 0x4C2B0E0: operator new(unsigned long) (in /usr/lib/valgrind/vgp
reload_memcheck-amd64-linux.so)
==2881==    by 0x400966: main (test.cpp:92)

Valgrind 用作函數和內存分析:

Valgrind 不僅能告訴您錯誤出在哪裏,他還能幫您優化代碼。 人們常常自以爲知道是什麼導致了程序在運行時大量消耗內存...,一番折騰之後才發現錯了。 怎樣才能節約您寶貴的時間呢? 多測量。

通過如下命令運行您的程序:

valgrind --tool=callgrind ./myProgram

它會在被測試程序的目錄下生成一個類似名爲 ”callgrind.out.2887“ 的文件。下載程序 KCachegrind,它提供一個可視化的界面,顯示您的程序的執行路徑以及哪個函數在吞噬您寶貴的內存。一目瞭然,你很快就知道自己該把火力集中在哪裏了。

這裏有些簡單的輸出示例, 它列出了每個函數的時間消耗(wall time),內存(百分比)消耗和調用次數。 Google 一下,你能搜到很多它生成的其他有趣的圖表。

我們也可以利用 --tool=massif 參數來發現大量消耗內存的代碼。它原本是用於檢測內存泄露,但是內存泄露就會造成大量的內存滯留在程序。

泥牛
泥牛
翻譯於 3年前
2人頂
 翻譯得不錯哦!

結語:

Valgrind 遠不止是一款內存泄露檢測工具。是時候改變您的觀念了: Valgrind 要做未知行爲的清道夫。

Valgrind 完全可以作爲您的首選工具。他不僅向您報告錯誤的地點和原因,關鍵是他會搶在程序奔潰之前提醒您(這兩點都是 GDB 無法做到的)。 當然 GDB 依然優秀,它能在斷言失敗時給出完整詳盡的堆棧跟蹤信息,這對調試併發代碼和其他一些情況都是很必要的。

-pedantic -Wall -Wextra 這些編譯選項也是相當有用的。越來越聰明的現代編譯器也能幫你定位一些未知行爲。Valgrind 應該被當做對編譯器的有力的補足,而非功能重疊的競爭者。

如果您對此有一進步的興趣,您還可以看看下面的工具,他們或多或少完成了和 Valgrind 類似的工作,並在對程序的運行時的影響較小:
Address Sanitizer for clang and g++
Undefined Behavior Sanitizer for clang and g++
Memory Sanitizer for clang
Thread Sanitizer for clang


轉自:https://www.oschina.net/translate/valgrind-is-not-a-leak-checker

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章