使用 mtrace 分析 「內存洩露」

2021-02-21 泰曉科技
1 內存洩漏導論

在工作中,特別是採用 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 ***

相關焦點

  • MySQL優化:學會使用show profile和trace分析慢查詢
    MySQL優化:定位慢查詢的兩種方法以及使用explain分析SQL在上一節我們學習了定位慢 SQL 及使用 explain 分析慢 SQL,我們也提到了分析慢 SQL 還有 show profile 和 trace 等方法,本節就重點補充學習這兩種方法。
  • 常見的 JavaScript 內存洩露
    最後希望各位能從文章中有所收穫>🎉 enjoy reading, enjoy life 🐳✏️最新內容請以github上的為準❗️https://github.com/zhansingsong/js-leakage-patterns什麼是內存洩露內存洩漏指由於疏忽或錯誤造成程序未能釋放已經不再使用的內存。
  • 如何使用pprof分析應用
    喚起協程4、runtime·notetslee 棧增長及執行時間檢測7、進入中端一 、查看CPU信息查看內存分配取樣查看某個函數的細節web二、查看阻塞堆棧信息查看某個函數細節web三、查看協程堆棧信息查看某個函數細節web查看四、查看內存分配情況web查看五、查看互斥鎖信息查看某個函數細節web查看六、查看創建新OS線程的堆棧跟蹤查看某個函數細節web8、 PProf 火焰圖9、PProf 編程終端文件分析分析文件和前邊一樣
  • JavaScript 中 4 種常見的內存洩露陷阱
    在這篇文章中我們將要探索客戶端 JavaScript 代碼中常見的一些內存洩漏的情況,並且學習如何使用 Chrome 的開發工具來發現他們。讀一讀吧!介紹內存洩露是每個開發者最終都不得不面對的問題。即便使用自動內存管理的語言,你還是會碰到一些內存洩漏的情況。內存洩露會導致一系列問題,比如:運行緩慢,崩潰,高延遲,甚至一些與其他應用相關的問題。
  • Android 內存洩漏探討
    舉個例子:Sample 類的局部變量 s2 和引用變量 mSample2 都是存在於棧中,但 mSample2 指向的對象是存在於堆上的。mSample3 指向的對象實體存放在堆上,包括這個對象的所有成員變量 s1 和 mSample1,而它自己存在於棧中。
  • 內存洩露的原因找到了,罪魁禍首居然是Java TheadLocal
    ThreadLocal使用不規範,師傅兩行淚組內來了一個實習生,看這小夥子春光滿面、精神抖擻、頭髮微少,我心頭一喜:絕對是個潛力股。於是我找經理申請親自來帶他,為了幫助小夥子快速成長,我給他分了一個需求,這不需求剛上線幾天就出網上問題了後臺監控服務發現內存一直在緩慢上升,初步懷疑是內存洩露。
  • 「tcp丟包分析」實驗解析(二)--kprobe和tracepoint
    tcp丟包分析系列文章代碼來自謝寶友老師,由西郵陳莉君教授研一學生進行解析,本文由戴君毅整理,梁金榮編輯,賀東升校對。
  • 內存洩露的原因找到了,罪魁禍首居然是 Java TheadLocal
    於是我找經理申請親自來帶他,為了幫助小夥子快速成長,我給他分了一個需求,這不需求剛上線幾天就出網上問題了,後臺監控服務發現內存一直在緩慢上升,初步懷疑是內存洩露。把實習生的PR都找出來仔細 review,果然發現問題了。由於公司內部代碼是保密的,這裡簡單寫一個 demo 還原場景(忽略代碼風格問題)。
  • 用valgrind定位內存洩漏
    緣起年前,寫了使用mtrace定位內存洩漏,在留言中,有讀者提到了希望介紹valgrind,那好,今天就介紹使用valgrind定位內存洩漏。大約2-3年前,楊同學讓我幫做模擬面試,他求職的是C++崗位,我問了這樣一個問題:在你的項目中,你是如何定位內存洩漏的呢?
  • Android性能優化:帶你全面實現內存優化
    定義優化處理 應用程式的內存使用、空間佔用2. 作用避免因不正確使用內存 & 缺乏管理,從而出現 內存洩露(ML)、內存溢出(OOM)、內存空間佔用過大 等問題,最終導致應用程式崩潰(Crash)3.
  • Python 性能分析入門指南
    分析程序的性能可以歸結為回答四個基本問題:正運行的多快速度瓶頸在哪裡內存使用率是多少內存洩露在哪裡下面,我們將用一些神奇的工具深入到這些問題的答案中去。用 time 粗粒度的計算時間讓我們開始通過使用一個快速和粗暴的方法計算我們的代碼:傳統的 unix time 工具。
  • K8S 問題排查:cgroup 內存洩露問題
    :SLUB: Unable to allocate memory on node -13、節點 OOM 開始按優先級殺進程,有可能會導致有些正常 pod 被殺掉4、機器free 查看可用內存還有很多,卻無法分配,懷疑是內存洩露。
  • Android性能優化:手把手帶你全面實現內存優化
    定義優化處理 應用程式的內存使用、空間佔用2. 作用避免因不正確使用內存 & 缺乏管理,從而出現 內存洩露(ML)、內存溢出(OOM)、內存空間佔用過大 等問題,最終導致應用程式崩潰(Crash)3. 儲備知識:Android 內存管理機制3.1 簡介
  • C/C++程序內存問題分析
    本次,FourExperts小組整理了一些內存問題,希望通過對這些問題的分析,大家能夠了解到程序內存問題的類型以及一些基礎知識。從分析結果來看,這類問題主要可以分為幾類:指向內存空間的指針丟失申請的內存沒有釋放或釋放不完全內存無限申請而不釋放使用的數據結構不釋放內存指針丟失問題,是最常見的一類內存洩露的原因。
  • 使用 redis-rdb-tools 解析 reids dump.rdb 文件及分析內存使用量
    現在越來越多人開始使用 Redis 了,主要是因為它十分高效、性能強勁、擴展性好。先介紹幾種分析 Redis 的工具!
  • 【藏經閣】C/C++程序性能案例分析-內存問題分析
    ▌3.1 內存洩露案例分析程序內存洩漏問題是十分常見的一類程序問題,有很多原因都會導致程序表現出內存洩漏的現象。從造成洩漏的根本原因上劃分,我們可以簡單地將問題分為兩類:▌3.1.1 程序設計不合理從分析結果來看,這類問題主要可以分為幾類:指向內存空間的指針丟失申請的內存沒有釋放或釋放不完全、內存無限申請而不釋放使用的數據結構不釋放內存
  • 手把手教你認識OPTIMIZER_TRACE
    * one_line:決定了跟蹤信息的存儲方式,為on表示使用單行存儲,否則以JSON樹的標準展示形式存儲。單行存儲中跟蹤結果中沒有空格,造成可讀性極差,但對於JSON解析器來說是可以解析的,將該參數打開唯一的優勢就是節省空間,一般不建議開啟。
  • GDB與Valgrind ,調試C++代碼內存的工具
    2、利用Valgrind判斷內存洩露亡羊補牢不如未雨綢繆,與其等到出現程序崩潰時使用 GDB 來調試解決,不如事前確認代碼之中可能引發的問題。所以筆者接下來要介紹一款來自大不列顛的C++代碼分析神器:Valgrind。
  • 針對Cortex-M調試診斷 HardFault 的錯誤追蹤庫(CmBacktrace)
    該庫採用採用 MIT 開源協議,開源地址:https://github.com/armink/CmBacktraceCmBacktrace (Cortex Microcontroller Backtrace)是一款針對 ARM Cortex-M 系列 MCU 的錯誤代碼自動追蹤、定位,錯誤原因自動分析的開源庫
  • MySQL內存不釋放分析
    問題分析場景1 使用sysbench壓測資料庫場景2 load 一個很大事務的insert語句問題突破測試jemalloc場景1使用sysbench