使用 mtrace 分析 「內存泄露」

本文首次發表於 使用 mtrace 分析 「內存泄露」程序員

內存泄露導論

在工做中,特別是採用 C 語言編寫程序時,動態內存分配是常有的事,而伴隨動態內存分配而來的最大的問題就是所謂 「內存泄露」。所謂 「內存泄露」 的意思就是咱們申請了內存,但忘記歸還給系統,久而久之,系統的可分配內存愈來愈少,這種問題一旦出現必然很難查找,緣由很簡單,程序是人寫的,寫的人都忘記本身曾經在哪裏分配了而沒有釋放,那系統就更不能隨便幫助咱們回收內存了。一旦 「內存泄露」 發生,特別是放生在一些生命週期較長的程序中(譬如後臺服務這樣的),從系統的角度來講,可用內存莫名其妙地愈來愈少,形象地咱們就比喻系統上好像真的出現了一個洞,安裝的內存從這個洞裏被 「漏掉」 不見了。bash

mtrace 使用介紹

一旦發現系統有這個 「苗頭」,當務之急就是要找到代碼裏哪裏忘記歸還了動態分配的內存。 而 「內存分配跟蹤(malloc tracing)」 機制則是幫助咱們檢查 「內存泄露」 的好幫手,本文就來給你們介紹一下這個工具的使用,習慣上這個工具咱們簡稱爲 mtrace,下文也直接用 mtrace 指稱這個工具。函數

mtrace 工具的主要思路是在咱們的調用內存分配和釋放的函數中裝載 「鉤子(hook)」 函數,經過 「鉤子(hook)」 函數打印的日誌來幫助咱們分析對內存的使用是否存在問題。對該工具的使用包括兩部份內容,一個是要修改源碼,裝載 hook 函數,另外一個是經過運行修改後的程序,生成特殊的 log 文件,而後利用 mtrace 工具分析日誌,判斷是否存在內存泄露以及定位可能發生內存泄露的代碼位置。工具

下面咱們經過一個簡單的例子,看一下如何利用 mtrace 機制分析 「內存泄露」 問題。mtrace 這個工具自己是 Glibc 的一部分,因此通常狀況下你們的機器上都會有,無須特殊安裝,本文演示的環境是 Ubuntu 16.04.6 LTSui

修改源碼,裝載 「鉤子」 函數

咱們首先須要改動一下咱們的源碼。添加如下兩個輔助函數:spa

#include <mcheck.h>

void mtrace(void);

void muntrace(void);
複製代碼

函數的具體介紹參考 man 3 mtrace。其中 mtrace() 用於開啓內存分配跟蹤,muntrace() 用於取消內存分配跟蹤。具體的作法是 mtrace() 函數中會爲那些和動態內存分配有關的函數(譬如 malloc()、realloc()、memalign() 以及 free())安裝 「鉤子(hook)」 函數,這些 hook 函數會爲咱們記錄全部有關內存分配和釋放的跟蹤信息,而 muntrace() 則會卸載相應的 hook 函數。基於這些 hook 函數生成的調試跟蹤信息,咱們就能夠分析是否存在 「內存泄露」 這類問題了。命令行

這裏演示用的源碼文件 test_memleak.c 以下所示。設計

$ cat -n test_memleak.c

     1  #include <stdlib.h>
     2  #include <stdio.h>
     3  #include <mcheck.h>
     4
     5  int main(int argc, char **argv)
     6  {
     7          mtrace();
     8
     9          char *p = malloc(16);
    10
    11          free(p);
    12
    13          p = malloc(32);
    14
    15          muntrace();
    16
    17          return 0;
    18  }
複製代碼

其中咱們但願調試的代碼段是第 9 行到第 13 行,第 9 行調用 malloc() 申請了 16 個字節的內存,第 11 行調用 free() 函數釋放了第 9 行分配的內存,第 13 行又調用 malloc() 申請了 32 個字節的內存。很顯然,這段代碼的第 13 行存在問題,因爲第 13 行分配的內存沒有被釋放掉,會引發 「內存泄露」。以上是咱們人工閱讀代碼後的分析結果,如今咱們來看看如何利用 mtrace 機制幫助咱們獲得相同的結論。調試

首先咱們須要用 mtrace()/muntrace() 這一對函數將咱們關係的代碼段括起來,因此咱們在第 7 行添加了 mtrace() 函數,第 15 行添加了 muntrace() 函數。另外不要忘記包含 mcheck.h,這個能夠參見上面代碼的第 3 行。日誌

而後就能夠直接編譯連接,生成可執行程序:

$ gcc -g test_memleak.c -o a.out
複製代碼

注意這裏不要忘記加上 -g 參數,這個很重要,由於後面咱們須要調試信息幫助咱們定位出問題的代碼行數。

生成日誌文件並分析定位問題

mtrace 機制須要咱們實際運行一下程序,而後才能生成跟蹤的日誌,但在實際運行程序以前還有一件要作的事情是須要告訴 mtrace (即前文提到的 hook 函數)生成日誌文件的路徑。具體的方法是經過定義並導出一個環境變量 MALLOC_TRACE,以下所示。

$ export MALLOC_TRACE=./test.log
複製代碼

上述的結果就是告訴 mtrace 在生成日誌信息時,在當前路徑下建立一個名爲 test.log 的文件,並將日誌輸出到這個文件中去。

而後就能夠直接運行程序了。

$ ./a.out
複製代碼

運行結束後,咱們能夠發現當前路徑下果真生成了一個 test.log 文件。

$ ls
a.out  test_memleak.c  test.log
複製代碼

好奇的我忍不住打開這個日誌文件看了一下:

$ cat test.log
= Start
@ ./a.out:[0x400624] + 0x852450 0x10
@ ./a.out:[0x400634] - 0x852450
@ ./a.out:[0x40063e] + 0x852470 0x20
= End
複製代碼

其實這個文件的內容仍是蠻好懂的。三行 「有效」 記錄(除去第一行 = Start 和最後一行 = End),分別對應這前面咱們給你們介紹的源文件的三次 malloc -> free -> malloc 操做。

先看一下每一行的具體格式,以第一行 @ ./a.out:[0x400624] + 0x852450 0x10 爲例。./a.out 顯然指的是咱們運行的可執行程序的名字。[0x400624] 這裏的數值是對應代碼中第一次調用 malloc() 的指令,但注意這是機器碼的地址,剛好咱們在編譯可執行程序的時候利用 -g 帶上了調試信息,因此咱們徹底能夠利用 addr2line 這個工具,基於該值(0x400624)反推出源文件的行數。具體作法以下:

$ addr2line -f -e a.out 0x400624
main
/home/u/samples/test_memleak.c:9
複製代碼

的確就是第 9 行,一點都沒有錯。

繼續分析日誌行的信息,接着後面的是一個符號 +,代表這一行對應的是分配內存,反之 - 表示是釋放。再日後是一個數值 0x852450,這又是一個地址值,只不過是 malloc() 函數分配的內存的首地址。繼續,最後是 0x10,換算成十進制就是 16,正是咱們代碼中第 9 行分配的內存的字節大小。

瞭解了具體格式後咱們從三行有效日誌中能夠得出什麼結論呢,由於第一行是分配,其分配的內存首地址是 0x852450,而第二行釋放的內存的首地址也是 0x852450,天然說明是一對,相互抵消,不存在內存泄露。第三行分配的內存首地址是 0x852470,後面沒有匹配的釋放日誌,則說明這裏出現了 「內存泄露」。

這麼分析對於這裏的簡單的例子也許是足夠了,可是在實際工做中的場景代碼絕對不會就這麼幾行的,那怎麼辦,人爲的分析豈不是一件很麻煩的事情,或許在瞭解了日誌文件的格式後咱們聰明的程序員本身也會開發一個日誌分析工具來作這件事。這麼天然而然的事情固然 mtrace 的設計人員早就爲咱們想到了。系統提供了一個叫作 mtrace 的命令行工具能夠幫助咱們完成對日誌的分析。

趕忙來試一下。輸入以下命令:

$ mtrace ./a.out $MALLOC_TRACE
Memory not freed:
-----------------
           Address     Size     Caller
0x0000000000852470     0x20  at /home/u/samples/test_memleak.c:13
複製代碼

輸出的結果已經告訴咱們了一切。mtrace 這個工具須要至少兩個參數,一個是咱們生成的可執行程序文件的路徑,還有一個是日誌文件的路徑。man 1 mtrace 告訴咱們 mtrace 這個工具其實是一個 Perl 腳本,至於爲何這個命令須要這兩個參數,以及這個 Perl 腳本里幹了些啥,通過咱們這一路走來的分析,我想聰明的讀者您應該本身能夠想明白,我這裏就很少解釋了。

根據 man 3 mtrace 的說明 mtrace 還能幫助咱們查找 「重複釋放」 問題(man 手冊上的原話叫 「free nonallocated memory」)。我試了一下,發如今個人環境中實際編譯運行如下程序會直接報錯,也就沒法生成 mtrace 的日誌。或許對於此類問題,採用 mtrace 工具並非最好的作法。這裏我只給出了我所看到的 「重複釋放」 的代碼例子和執行的命令結果,供你們簡單參考:

$ cat -n test_dupfree.c
     1  #include <stdio.h>
     2  #include <malloc.h>
     3  #include <mcheck.h>
     4
     5  int main(int argc, char *argv[])
     6  {
     7          char *s = NULL;
     8
     9          mtrace();
    10
    11          s = malloc(32);
    12
    13          free(s);
    14
    15          free(s); // <-- free nonallocated memory
    16
    17          muntrace();
    18
    19          return 0;
    20  }
$ gcc -g test_dupfree.c -o a.out
$ export MALLOC_TRACE=./test.log
$ ./a.out
*** Error in `./a.out': double free or corruption (fasttop): 0x00000000018a7450 *** 複製代碼
相關文章
相關標籤/搜索