使用 mtrace 分析 “記憶體洩露”

吳章金發表於2019-09-21

本文首次發表於 使用 mtrace 分析 “記憶體洩露”

記憶體洩露導論

在工作中,特別是採用 C 語言編寫程式時,動態記憶體分配是常有的事,而伴隨動態記憶體分配而來的最大的問題就是所謂 “記憶體洩露”。所謂 “記憶體洩露” 的意思就是我們申請了記憶體,但忘記歸還給系統,長此以往,系統的可分配記憶體越來越少,這種問題一旦出現必然很難查詢,原因很簡單,程式是人寫的,寫的人都忘記自己曾經在哪裡分配了而沒有釋放,那系統就更不能隨便幫助我們回收記憶體了。一旦 “記憶體洩露” 發生,特別是放生在一些生命週期較長的程式中(譬如後臺服務這樣的),從系統的角度來說,可用記憶體莫名其妙地越來越少,形象地我們就比喻系統上好像真的出現了一個洞,安裝的記憶體從這個洞裡被 “漏掉” 不見了。

mtrace 使用介紹

一旦發現系統有這個 “苗頭”,當務之急就是要找到程式碼裡哪裡忘記歸還了動態分配的記憶體。 而 “記憶體分配跟蹤(malloc tracing)” 機制則是幫助我們檢查 “記憶體洩露” 的好幫手,本文就來給大家介紹一下這個工具的使用,習慣上這個工具我們簡稱為 mtrace,下文也直接用 mtrace 指稱這個工具。

mtrace 工具的主要思路是在我們的呼叫記憶體分配和釋放的函式中裝載 “鉤子(hook)” 函式,通過 “鉤子(hook)” 函式列印的日誌來幫助我們分析對記憶體的使用是否存在問題。對該工具的使用包括兩部分內容,一個是要修改原始碼,裝載 hook 函式,另一個是通過執行修改後的程式,生成特殊的 log 檔案,然後利用 mtrace 工具分析日誌,判斷是否存在記憶體洩露以及定位可能發生記憶體洩露的程式碼位置。

下面我們通過一個簡單的例子,看一下如何利用 mtrace 機制分析 “記憶體洩露” 問題。mtrace 這個工具本身是 Glibc 的一部分,所以一般情況下大家的機器上都會有,無須特殊安裝,本文演示的環境是 Ubuntu 16.04.6 LTS

修改原始碼,裝載 “鉤子” 函式

我們首先需要改動一下我們的原始碼。新增以下兩個輔助函式:

#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 ***
複製程式碼

相關文章