在工作中,特別是採用 C 語言編寫程序時,動態內存分配是常有的事,而伴隨動態內存分配而來的最大的問題就是所謂 「內存洩漏」。所謂 「內存洩漏」 的意思就是我們申請了內存,但忘記歸還給系統,長此以往,系統的可分配內存越來越少,這種問題一旦出現必然很難查找,原因很簡單,程序是人寫的,寫的人都忘記自己曾經在哪裡分配了而沒有釋放,那系統就更不能隨便幫助我們回收內存了。一旦 「內存洩漏」 發生,特別是放生在一些生命周期較長的程序中(譬如後臺服務這樣的),從系統的角度來說,可用內存莫名其妙地越來越少,形象地我們就比喻系統上好像真的出現了一個洞,安裝的內存從這個洞裡被 「漏掉」 不見了。
2 mtrace 使用介紹一旦發現系統有這個 「苗頭」,當務之急就是要找到代碼裡哪裡忘記歸還了動態分配的內存。而 「內存分配跟蹤(malloc tracing)」 機制則是幫助我們檢查 「內存洩漏」 的好幫手,本文就來給大家介紹一下這個工具的使用,習慣上這個工具我們簡稱為 mtrace,下文也直接用 mtrace 指稱這個工具。
mtrace 工具的主要思路是在我們的調用內存分配和釋放的函數中裝載 「鉤子(hook)」 函數,通過 「鉤子(hook)」 函數列印的日誌來幫助我們分析對內存的使用是否存在問題。對該工具的使用包括兩部分內容,一個是要修改源碼,裝載 hook 函數,另一個是通過運行修改後的程序,生成特殊的 log 文件,然後利用 mtrace 工具分析日誌,判斷是否存在內存洩漏以及定位可能發生內存洩漏的代碼位置。
下面我們通過一個簡單的例子,看一下如何利用 mtrace 機制分析 「內存洩漏」 問題。mtrace 這個工具本身是 Glibc 的一部分,所以一般情況下大家的機器上都會有,無須特殊安裝,本文演示的環境是 Ubuntu 16.04.6 LTS。
2.1 修改源碼,裝載 「鉤子」 函數我們首先需要改動一下我們的源碼。添加以下兩個輔助函數:
#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 參數,這個很重要,因為後面我們需要調試信息幫助我們定位出問題的代碼行數。
2.2 生成日誌文件並分析定位問題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);
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 ***