技術乾貨丨經過wrap malloc定位C/C++的內存泄漏問題

摘要:用C/C++開發的程序執行效率很高,但卻常常受到內存泄漏的困擾。本文提供一種經過wrap malloc查找memory leak的思路。

用C/C++開發的程序執行效率很高,但卻常常受到內存泄漏的困擾。本文提供一種經過wrap malloc查找memory leak的思路,依靠這個方法,筆者緊急解決了內存泄漏問題,避免項目流血上大促,該方法在往後工做中大放光彩,發現了項目中大量沉痾已久的內存泄漏問題。linux

什麼是內存泄漏?

動態申請的內存丟失引用,形成沒有辦法回收它(我知道槓jing要說進程退出前系統會統一回收),這即是內存泄漏。c++

Java等編程語言會自動管理內存回收,而C/C++須要顯式的釋放,有不少手段能夠避免內存泄漏,好比RAII,好比智能指針(大多基於引用計數計數),好比內存池。程序員

理論上,只要咱們足夠當心,在每次申請的時候,都牢記釋放,那這個世界就清淨了,但現實每每沒有那麼美好,好比拋異常了,釋放內存的語句執行不到,又或者某菜鳥程序員不當心埋了一個雷,因此,咱們必須直面真實的世界,那就是咱們會遭遇內存泄漏。編程

怎麼查內存泄漏?

咱們能夠review代碼,但從海量代碼裏找到隱藏的問題,這如同大海撈針,每每兩手空空。數組

因此,咱們須要藉助工具,好比valgrind,但這些找內存泄漏的工具,每每對你使用動態內存的方式有某種期待,或者說約束,好比常駐內存的對象會被誤報出來,而後真正有用的信息會掩蓋在誤報的汪洋大海里。不少時候,甚至valgrind根本解決不了平常項目中的問題。緩存

因此不少著名的開源項目,爲了能用valgrind跑,都費大力氣,大幅修改源代碼,從而使得項目符合valgrind的要求,知足這些要求,用valgrind跑完沒有任何報警的項目叫valgrind乾淨。編程語言

既然這些玩意兒都中看不中用,因此,求人不如求己,仍是得自力更生。函數

什麼是動態內存分配器?

動態內存分配器是介於kernel跟應用程序之間的一個函數庫,glibc提供的動態內存分配器叫ptmalloc,它也是應用最普遍的動態內存分配器實現。工具

從kernel角度看,動態內存分配器屬於應用程序層;而從應用程序的角度看,動態內存分配器屬於系統層。性能

應用程序能夠經過mmap系統直接向kernel申請動態內存,也能夠經過動態內存分配器的malloc接口分配內存,而動態內存分配器會經過sbrk、mmap向kernel分配內存,因此應用程序經過free釋放的內存,並不必定會真正返還給系統,它也有可能被動態內存分配器緩存起來。

google有本身的動態內存分配器tcmalloc,另外jemalloc也是著名的動態內存分配器,他們有不一樣的性能表現,也有不一樣的緩存和分配策略。你能夠用它們替換linux系統glibc自帶的ptmalloc。

new/delete跟malloc/free的關係

new是c++的用法,好比Foo *f = new Foo,其實它分爲3步。

(1)經過operator new()分配sizeof(Foo)的內存,最終經過malloc分配。

(2)在新分配的內存上構建Foo對象。

(3)返回新構建的對象地址。

new=分配內存+構造+返回,而delete則是等於析構+free。

因此搞定malloc、free就是從根本上搞定動態內存分配。

chunk

每次經過malloc返回的一塊內存叫一個chunk,動態內存分配器是這樣定義的,後面咱們都這樣稱呼。

wrap malloc

gcc支持wrap,即經過傳遞-Wl,--wrap,malloc的方式,能夠改變調用malloc的行爲,把對malloc的調用連接到自定義的__wrap_malloc(size_t)函數,而咱們能夠在__wrap_malloc(size_t)函數的實現中經過__real_malloc(size_t)真正分配內存,然後咱們能夠作搞點小動做。

一樣,咱們能夠wrap free。malloc跟free是配對的,固然也有其餘相關API,好比calloc、realloc、valloc,但這根本上仍是malloc+free,好比realloc就是malloc + free。

怎麼去定位內存泄漏呢?

咱們會malloc各類不一樣size的chunk,也就是每種不一樣size的chunk會有不一樣數量,若是咱們可以跟蹤每種size的chunk數量,那就能夠知道哪一種size的chunk在泄漏。很簡單,若是該size的chunk數量一直在增加,那它極可能泄漏。

光知道某種size的chunk泄漏了還不夠,咱們得知道是哪一個調用路徑上致使該size的chunk被分配,從而去檢查是否是正確釋放了。

怎麼跟蹤到每種size的chunk數量?

咱們能夠維護一個全局 unsigned int malloc_map[1024 * 1024]數組,該數組的下標就是chunk的size,malloc_map[size]的值就對應到該size的chunk分配量。

這等於維護了一個chunk size到chunk count的映射表,它足夠快,並且它能夠覆蓋到0 ~ 1M大小的chunk的範圍,它已經足夠大了,試想一次分配一兆的塊已經很恐怖了,能夠覆蓋到大部分場景。

那大於1M的塊怎麼辦呢?咱們能夠經過log記錄下來。

在__wrap_malloc裏,++malloc_map[size]

在__wrap_free裏,--malloc_map[size]

很簡單,咱們經過malloc_map記錄了各size的chunk的分配量。

如何知道釋放的chunk的size?

不對,free(void *p)只有一個參數,我如何知道釋放的chunk的size呢?怎麼辦?

咱們經過在__wrap_malloc(size_t)的時候,分配8+size的chunk,也就是多分配8字節,開始的8字節存儲該chunk的size,而後返回的是(char*)chunk + 8,也就是偏移8個字節返回給調用malloc的應用程序。

這樣在free的時候,傳入參數void* p,咱們把p往前移動8個字節,解引用就能獲得該chunk的大小,而該大小值就是前一步,在__wrap_malloc的時候設置的size。

好了,咱們真正作到記錄各size的chunk數量了,它就存在於malloc_map[1M]的數組中,假設64個字節的chunk一直在被分配,數量一直在增加,咱們以爲該size的chunk頗有可能泄漏,那怎麼定位到是哪裏調用過來的呢?

如何記錄調用鏈?

咱們能夠維護一個toplist數組,該數組假設有10個元素,它保存的是chunk數最大的10種size,這個很容易作到,經過對malloc_map取top 10就行。

而後咱們在__wrap_malloc(size_t)裏,測試該size是否是toplist之一,若是是的話,那咱們經過glibc的backtrace把調用堆棧dump到log文件裏去。

注意:這裏不能再分配內存,因此你只能使用backtrace,而不能使用backtrace_symbols,這樣你只能獲得調用堆棧的符號地址,而不是符號名。

如何把符號地址轉換成符號名,也就是對應到代碼行呢?

addr2line

addr2line工具能夠作到,你能夠追查到調用鏈,進而定位到內存泄漏的問題。

至此,你已經get到了整個核心思想。

固然,實際項目中,咱們作的更多,咱們不只僅記錄了toplist size,還記錄了各size chunk的增量toplist,會記錄大塊的malloc/free,會wrap更多的API。

總結一下:經過wrap malloc/free + backtrace + addr2line,你就能夠定位到內存泄漏了,恭喜你們。


點擊關注,第一時間瞭解華爲雲新鮮技術~

相關文章
相關標籤/搜索