Dr. Memory 是一個(gè)開源免費(fèi)的內(nèi)存檢測(cè)工具,它能夠及時(shí)發(fā)現(xiàn)內(nèi)存相關(guān)的編程錯(cuò)誤,比如未初始化訪問、內(nèi)存非法訪問以及內(nèi)存泄露等。它不僅能夠在 Linux 下面工作,也能在微軟的 Windows 操作系統(tǒng)上工作。不過,本文撰寫時(shí),DrMemory 僅能支持 32 位程序,這是它的一個(gè)巨大缺陷,但相信隨著開發(fā)的進(jìn)行,DrMemory 會(huì)推出支持 64 位程序的版本。
Dr Memory特點(diǎn):
Dr Memory 與 Valgrind 類似,可以直接檢查已經(jīng)編譯好的可執(zhí)行文件。用戶不用改寫被檢查程序的源代碼,也無須重新鏈接第三方庫文件,使用起來非常方便。
易用性和性能是 DrMemory 的主要優(yōu)點(diǎn),此外 DrMemory 可以用于調(diào)試 Windows 程序,因此它被廣泛認(rèn)為是 Windows 上的 Valgrind 替代工具。在 Linux 平臺(tái)中,DrMemory 也往往可以作為 Valgrind 之外的另一個(gè)選擇。
DrMemory 對(duì)內(nèi)存泄露的監(jiān)測(cè)采用了比較獨(dú)特的算法,大量減少了”false positive”,即虛假錯(cuò)誤。如果您使用 Valgrind 等工具后仍無法找到程序中的內(nèi)存錯(cuò)誤,不妨試試 DrMemory 吧。
Dr. Memory 建立在 DynamoRIO 這個(gè)動(dòng)態(tài)二進(jìn)制插樁平臺(tái)上。動(dòng)態(tài)監(jiān)測(cè)程序的運(yùn)行,并對(duì)內(nèi)存訪問相關(guān)的執(zhí)行代碼進(jìn)行動(dòng)態(tài)修改,記錄其行為,并采用先進(jìn)的算法進(jìn)行錯(cuò)誤檢查。
C++程序員最大的敵人就是內(nèi)存處理錯(cuò)誤,比如內(nèi)存泄露、內(nèi)存溢出等。這些錯(cuò)誤不易發(fā)現(xiàn),調(diào)試?yán)щy。本文介紹一個(gè)新的內(nèi)存調(diào)試工具 DrMemory,為您的工具箱中添加一個(gè)新的內(nèi)存檢查利器吧。
Dr Memory內(nèi)存檢測(cè)實(shí)例:
Dr. Memory 建立在 DynamoRIO 這個(gè)動(dòng)態(tài)二進(jìn)制插樁平臺(tái)上。動(dòng)態(tài)監(jiān)測(cè)程序的運(yùn)行,并對(duì)內(nèi)存訪問相關(guān)的執(zhí)行代碼進(jìn)行動(dòng)態(tài)修改,記錄其行為,并采用先進(jìn)的算法進(jìn)行錯(cuò)誤檢查。
根據(jù) DrMemory 開發(fā)人員發(fā)表在 CGO 2011上的論文 Practical Memory Checking with Dr. Memory,DrMemory 對(duì)程序的正常執(zhí)行影響較小,這在同類工具中是比較領(lǐng)先的。其 performance 和 Valgrind 的比較如圖 1 所示(圖片源自 DrMemory 主頁):
圖 1. 和 Valgrind 的性能比較
Valgrind 對(duì)程序的正常運(yùn)行影響較大,一般來說如果進(jìn)行全面內(nèi)存檢測(cè),會(huì)使程序的運(yùn)行速度有 50 到 300 倍的減慢。而 DrMemory 在這個(gè)方面則有一定的優(yōu)勢(shì)。
易用性和性能是 DrMemory 的主要優(yōu)點(diǎn),此外 DrMemory 可以用于調(diào)試 Windows 程序,因此它被廣泛認(rèn)為是 Windows 上的 Valgrind 替代工具。在 Linux 平臺(tái)中,DrMemory 也往往可以作為 Valgrind 之外的另一個(gè)選擇。
DrMemory 對(duì)內(nèi)存泄露的監(jiān)測(cè)采用了比較獨(dú)特的算法,大量減少了”false positive”,即虛假錯(cuò)誤。如果您使用 Valgrind 等工具后仍無法找到程序中的內(nèi)存錯(cuò)誤,不妨試試 DrMemory 吧。
Windows 上 DrMemory 提供了可執(zhí)行安裝包,只需點(diǎn)擊下一步,即可安裝完畢。
DrMemory,第一印象 DrMemory 的使用很簡(jiǎn)單,可以說它是傻瓜式。正常運(yùn)行一個(gè)程序時(shí),我們?cè)?shell 中敲入命令然后回車。為了用 DrMemory 檢查,只需要在
Hello DrMemory,第一印象
DrMemory 的使用很簡(jiǎn)單,可以說它是傻瓜式。正常運(yùn)行一個(gè)程序時(shí),我們?cè)?shell 中敲入命令然后回車。為了用 DrMemory 檢查,只需要在正常命令之前加入 drmemory.pl,比如程序檢查程序 t,那么就這樣:
drmemory.pl ./t
在計(jì)算機(jī)領(lǐng)域,Helloworld 總是第一個(gè)程序。讓我們寫一個(gè) HelloDrMemory,來和 DrMemory 簡(jiǎn)單接觸一下吧。
清單 1,Hello DrMem 例子程序
1: int main() 2: { 3: char *ptr; 4: int i; 5: for(i=0;i<100;i++) 6: { 7: ptr=(char*)malloc(i); 8: if(i%2) free(ptr); 9: } 10: return 0; 11: }
很明顯,有 50 個(gè)內(nèi)存泄露,都在同一行代碼中(Line 8)。讓我們用 Dr Memory 來檢查它。
屏幕上會(huì)有如上所示的錯(cuò)誤匯總,注意看 ERRORS FOUND 下面的第 5 行:”50 total leaks”。不錯(cuò)吧。根據(jù)提示,更多的細(xì)節(jié)被寫入一個(gè) result 文本文件。打開并查看該文件,就可以知道程序在哪里出現(xiàn)了內(nèi)存錯(cuò)誤了。真是太方便了。不過 result 文件是否容易閱讀呢?下面我們來詳細(xì)解釋如何閱讀 DrMemory 產(chǎn)生的 result 文件。
DrMemory 報(bào)告解讀細(xì)節(jié)
內(nèi)存非法訪問
DrMemory 認(rèn)為任何對(duì)未分配內(nèi)存區(qū)域的讀寫都是非法的。在 Linux 中,應(yīng)用程序可以用以下幾個(gè)方式分配內(nèi)存:
調(diào)用 mmap (或者 mremap)
調(diào)用 malloc 在堆上分配內(nèi)存
使用 alloca 在棧上分配內(nèi)存
非法訪問就是對(duì)以上三種方法分配的內(nèi)存區(qū)域之外進(jìn)行的訪問。常見的問題包括 buffer overflow、數(shù)組越界、讀寫已經(jīng) free 的內(nèi)存、堆棧溢出等等。讓我們測(cè)試下面這個(gè)問題程序。
Buffer overflow
例子程序的第 5 到 6 行存在 buffer overflow。在內(nèi)存中,buffer 的分布如下圖所示:
圖 2. Buffer 分布
訪問 x+8 將產(chǎn)生一個(gè)非法內(nèi)存訪問。對(duì)此,Dr Memory 將給出如下的錯(cuò)誤信息:
首先用大寫的單詞 UNADDRESSABLE ACCESS 表明這是一個(gè)非法訪問錯(cuò)誤。接著,“reading 0x0804a020-0x0804a021 1 byte(s)”表示這是一個(gè)非法讀,讀取的范圍為 0x0804a020 到 0x0804a021,一共讀了 1 個(gè) byte。接下來的三行是調(diào)用堆棧信息,可以方便地看到錯(cuò)誤發(fā)生在哪個(gè)源文件的哪一行(程序 t 需要在用 gcc 編譯的時(shí)候給定-g 選項(xiàng))。此外 DrMemory 還給出了一些輔助的錯(cuò)誤信息。比如:
錯(cuò)誤發(fā)生的時(shí)間:Note: elapsed time = 0:00:00.133 in thread 13971。這表明錯(cuò)誤是程序開始的第 0.133 秒后發(fā)生的,有些情況下,人們可以根據(jù)這個(gè)時(shí)間進(jìn)行輔助判斷。
錯(cuò)誤細(xì)節(jié):Note: refers to 1 byte(s) beyond last valid byte in prior malloc。這里給出了錯(cuò)誤的詳細(xì)信息,如前所述,造成非法訪問的可能很多,在本例中是 buffer overflow,因此這里的詳細(xì)信息可以幫助我們了解非法訪問的具體原因。
Note: prev lower malloc: 0x0804a018-0x0804a020。這里給出了 overflow 之前的合法內(nèi)存地址,有些情況下對(duì)于查錯(cuò) 有一定的幫助。
Note: instruction: movzx (%eax) -> %eax。這里給出的是造成錯(cuò)誤的具體指令。
可以看到 DrMemory 只報(bào)告了一個(gè)未初始化讀錯(cuò)誤,在第 12 行。很多其他工具對(duì)于 memcpy(&b,&a, sizeof(T))也會(huì)報(bào)錯(cuò)。
GCC 將自動(dòng)對(duì)齊數(shù)據(jù)結(jié)構(gòu)(未使用 pack 修飾符的情況下)。因此 struct T 在內(nèi)存中的實(shí)際分布如下:
圖 3. 內(nèi)存拷貝細(xì)節(jié)
在 memcpy 時(shí),有 3 個(gè)未初始化 byte 也被訪問了,但這類錯(cuò)誤如果也報(bào)告的話,對(duì)正常程序 DrMemory 會(huì)產(chǎn)生很多錯(cuò)誤信息。這些其實(shí)不是錯(cuò)誤,所以被稱為 False Positive。類似醫(yī)學(xué)名詞“假陽性”。內(nèi)存調(diào)試工具的一個(gè)主要目標(biāo)就是減少 False Positive,否則產(chǎn)生的報(bào)告有用性將極大降低。
其它很多工具,遇到上述拷貝會(huì)報(bào)告 false positive,浪費(fèi)讀報(bào)告的人們的時(shí)間。因此這是 Dr Memory 的一個(gè)重要優(yōu)點(diǎn)。
內(nèi)存泄露
內(nèi)存泄露是常見的內(nèi)存錯(cuò)誤,我們可能都曾經(jīng)遇到過。不過 Dr.Memory 對(duì)內(nèi)存泄露的定義比較獨(dú)特,在程序退出之前,Dr.Memory 把所有依然被分配的內(nèi)存分為三類:
Still-reachable allocation
很多程序分配了內(nèi)存之后,在其整個(gè)生命周期內(nèi)都不釋放。雖然這是一種泄露,但實(shí)際上多數(shù)情況下這是無害的,甚至是特意這樣設(shè)計(jì)的。因此 Dr.Memory 并不認(rèn)為這是一種內(nèi)存泄露,而稱之為”Still-reachable allocation”。
Leak
有一些內(nèi)存無法再被釋放,因?yàn)橹赶蛟搩?nèi)存的指針丟失了。比如下面這個(gè)代碼:
清單 5.內(nèi)存 Leak 例子代碼
DrMemory 稱這類錯(cuò)誤為內(nèi)存泄露。因?yàn)檫@些內(nèi)存已經(jīng)沒有辦法被釋放了。
Possible Leak
如前所述指向內(nèi)存的指針被修改會(huì)被認(rèn)為是一個(gè) Leak,但并非所有的指針修改都是一個(gè) Leak。DrMemory 利用一些經(jīng)驗(yàn)規(guī)則(Heuristic)將以下幾種指針修改列為 Possible Leak。
第一種情況:C++程序利用 new[]分配了一個(gè)數(shù)組,該數(shù)組的每個(gè)元素都是 擁有自己的析構(gòu)函數(shù)的復(fù)雜數(shù)據(jù)結(jié)構(gòu)。這種情況下,New 操作符為每個(gè)元素加上一個(gè) header 用來保存數(shù)組的個(gè)數(shù),以便 delete[]操作符知道需要調(diào)用多少個(gè)析構(gòu)函數(shù)。但 new[]返回 caller 的是 header 之后的地址,這樣就變成了一個(gè) mid-allocation 指針。這可能被 Dr memory 認(rèn)為是一個(gè)內(nèi)存泄露。但可以使用-no_midchunk_new_ok 選項(xiàng)讓 DrMemory 將這類錯(cuò)誤報(bào)告為”possible leak”而非”leak”。
參考下圖,理解這種情況。
圖 4.mid-chunk new
從堆分配器的角度來看,buffer 的起點(diǎn)在 A 處,但 new 返回 B,給 Object 變量賦值。從某種角度上看,指針 A 丟失了,是一個(gè) leak,但實(shí)際上,當(dāng)調(diào)用 delete []操作符時(shí),C++運(yùn)行時(shí)庫會(huì)自動(dòng)將 Object 指針減 4,從而指向 A 點(diǎn),再進(jìn)行釋放。某些編譯器不使用這種做法,則沒有這個(gè)問題。
第二種情況,某些 C++編譯器在處理多繼承時(shí),會(huì)出現(xiàn) mid-chunk 指針。很抱歉,具體細(xì)節(jié)本人也不甚了解。Dr Memory 的原文如下:it includes instances of a pointer to a class with multiple inheritance that is cast to one of the parents: it can end up pointing to the subobject representation in the middle of the allocation. 您可以用-no_midchunk_inheritance_ok 選項(xiàng)將這類“錯(cuò)誤”報(bào)告為”possible leak” 。
還有一種可能:std::string 類把一個(gè) char[]數(shù)組放置在分配空間中,并返回一個(gè)指針直接指向它,造成了一個(gè) mid-allocation 指針。您可以用-no_midchunk_string_ok 選項(xiàng)讓這類錯(cuò)誤顯示為”possible leak”。
一些有用的選項(xiàng):
現(xiàn)實(shí)世界中真正的程序有很多不同于本文中所羅列的那些例子程序,現(xiàn)實(shí)程序更復(fù)雜,查找錯(cuò)誤并不像例子所示的那么容易。DrMemory 設(shè)計(jì)了一些輔助選項(xiàng),靈活使用它們才能在真正的工作中得到有用的信息。
監(jiān)控子程序
缺省情況下 DrMemory 將監(jiān)控當(dāng)前進(jìn)程產(chǎn)生的子進(jìn)程的內(nèi)存錯(cuò)誤。如果您想禁止檢查子進(jìn)程,可以使用-no_follow_children 選項(xiàng)。
合并檢查結(jié)果
用-aggregate 選項(xiàng)可以合并 DrMemory 的檢查結(jié)果,比如下面的命令把 logs 目錄下面多個(gè) DrMemory 報(bào)告合并為一個(gè)總的報(bào)告。
這個(gè)功能在某些情況下比較有用。比如對(duì)同一個(gè)程序用多個(gè)不同的測(cè)試用例測(cè)出不同的內(nèi)存錯(cuò)誤,可以把多個(gè)報(bào)告合并起來,以便程序員一次閱讀。
檢查不退出程序
一些程序永遠(yuǎn)或者長(zhǎng)時(shí)間都不退出,對(duì)于某些內(nèi)存錯(cuò)誤,比如未初始化讀寫,或者非法讀寫,DrMemory 一旦發(fā)現(xiàn)就立即寫入 result 文件。但 DrMemory 只有在進(jìn)程退出時(shí)才檢查內(nèi)存泄露。因此對(duì)于長(zhǎng)期運(yùn)行的程序,如果我們想在其運(yùn)行期間得到內(nèi)存泄露的報(bào)告,就需要使用 DrMemory 的 nudge 命令。比如您的進(jìn)程 pid 為 1000,正在被 DrMemory 檢測(cè)。那么你可以在 Shell 中運(yùn)行下面這條命令,強(qiáng)制 DrMemory 進(jìn)行內(nèi)存泄露檢查,并把結(jié)果更新到 result 文件中。
現(xiàn)在打開 result 文件,如果程序有內(nèi)存泄露,您將在該文件中找到錯(cuò)誤信息。
Suppressing Errors
內(nèi)存錯(cuò)誤檢查工具的一個(gè)重要能力就是能夠 suppress errors,即隱藏指定”錯(cuò)誤”的能力。因?yàn)槿藗兪褂脙?nèi)存錯(cuò)誤檢測(cè)工具最希望的是它能給出“真正的”錯(cuò)誤,而不是給出大量的不是錯(cuò)誤的錯(cuò)誤。工具本身可以根據(jù)一些經(jīng)驗(yàn)算法隱藏一些“眾所周知”的假錯(cuò)誤。但更多的情況下,需要使用者告訴工具如何區(qū)分出假錯(cuò)誤。
每次運(yùn)行 DrMemory 時(shí),它會(huì)產(chǎn)生一個(gè) suppress 文件,和 result 文件放在一起。該文件的格式如下:
圖 5. suppress 文件格式
suppress 文件有多個(gè)”O(jiān)ne Error”小節(jié)組成,每個(gè)”O(jiān)ne Error”表示一個(gè)可以被 suppress 的錯(cuò)誤。用調(diào)用堆棧來表示,有兩種格式來表示堆棧:
DrMemory 支持通配符,比如 t!*表示不報(bào)告所有模塊 t 中的錯(cuò)誤。在 Linux 下面,模塊 t,就是由 t.c 生成的 t.o 所包含的代碼,換句話說就是不檢查 t.c 中的錯(cuò)誤。
一些有用的選項(xiàng):
現(xiàn)實(shí)世界中真正的程序有很多不同于本文中所羅列的那些例子程序,現(xiàn)實(shí)程序更復(fù)雜,查找錯(cuò)誤并不像例子所示的那么容易。DrMemory 設(shè)計(jì)了一些輔助選項(xiàng),靈活使用它們才能在真正的工作中得到有用的信息。
監(jiān)控子程序
缺省情況下 DrMemory 將監(jiān)控當(dāng)前進(jìn)程產(chǎn)生的子進(jìn)程的內(nèi)存錯(cuò)誤。如果您想禁止檢查子進(jìn)程,可以使用-no_follow_children 選項(xiàng)。
合并檢查結(jié)果
用-aggregate 選項(xiàng)可以合并 DrMemory 的檢查結(jié)果,比如下面的命令把 logs 目錄下面多個(gè) DrMemory 報(bào)告合并為一個(gè)總的報(bào)告。
這個(gè)功能在某些情況下比較有用。比如對(duì)同一個(gè)程序用多個(gè)不同的測(cè)試用例測(cè)出不同的內(nèi)存錯(cuò)誤,可以把多個(gè)報(bào)告合并起來,以便程序員一次閱讀。
檢查不退出程序
一些程序永遠(yuǎn)或者長(zhǎng)時(shí)間都不退出,對(duì)于某些內(nèi)存錯(cuò)誤,比如未初始化讀寫,或者非法讀寫,DrMemory 一旦發(fā)現(xiàn)就立即寫入 result 文件。但 DrMemory 只有在進(jìn)程退出時(shí)才檢查內(nèi)存泄露。因此對(duì)于長(zhǎng)期運(yùn)行的程序,如果我們想在其運(yùn)行期間得到內(nèi)存泄露的報(bào)告,就需要使用 DrMemory 的 nudge 命令。比如您的進(jìn)程 pid 為 1000,正在被 DrMemory 檢測(cè)。那么你可以在 Shell 中運(yùn)行下面這條命令,強(qiáng)制 DrMemory 進(jìn)行內(nèi)存泄露檢查,并把結(jié)果更新到 result 文件中。
現(xiàn)在打開 result 文件,如果程序有內(nèi)存泄露,您將在該文件中找到錯(cuò)誤信息。
Suppressing Errors
內(nèi)存錯(cuò)誤檢查工具的一個(gè)重要能力就是能夠 suppress errors,即隱藏指定”錯(cuò)誤”的能力。因?yàn)槿藗兪褂脙?nèi)存錯(cuò)誤檢測(cè)工具最希望的是它能給出“真正的”錯(cuò)誤,而不是給出大量的不是錯(cuò)誤的錯(cuò)誤。工具本身可以根據(jù)一些經(jīng)驗(yàn)算法隱藏一些“眾所周知”的假錯(cuò)誤。但更多的情況下,需要使用者告訴工具如何區(qū)分出假錯(cuò)誤。
每次運(yùn)行 DrMemory 時(shí),它會(huì)產(chǎn)生一個(gè) suppress 文件,和 result 文件放在一起。該文件的格式如下:
圖 5. suppress 文件格式
suppress 文件有多個(gè)”O(jiān)ne Error”小節(jié)組成,每個(gè)”O(jiān)ne Error”表示一個(gè)可以被 suppress 的錯(cuò)誤。用調(diào)用堆棧來表示,有兩種格式來表示堆棧:
DrMemory 支持通配符,比如 t!*表示不報(bào)告所有模塊 t 中的錯(cuò)誤。在 Linux 下面,模塊 t,就是由 t.c 生成的 t.o 所包含的代碼,換句話說就是不檢查 t.c 中的錯(cuò)誤。