內存使用和管理在C/C++程序中是一個無法繞開的問題, 在gdb支持python script以後, 我們就可以使用gdb這個新的特性來幫助我們查看在glibc ptmalloc算法中管理的內存的情況。為了方便, 下面我們主要針對x64環境。
在可以查看內存分配情況以前, 我們當然需要知道ptmalloc算法大致是一個什麼樣子的。 相關的文章現在在網上已經非常普遍了, 你只需要以ptmalloc analysis為關鍵字google一下就可以看到很多的相關文章,例如Glibc 內存管理或者 Understanding glibc malloc。
在這裡我們大致介紹一下ptmalloc是如何管理內存的。ptmalloc的基本思想是將從內核分配出的大塊連續內存(例如64M)拿出來,然後按照一定的大小切分成若干個小的內存塊。
這些內存塊用鍊表連結起來, 當請求一塊內存的時候, 就從鍊表中查找出一個最小可以滿足需求的塊交給應用程式, 如果沒有合適的內存塊,那麼可能需要從更大的內存塊中切分出來合適大小的內存塊,連結到已有的鏈上,然後交給應用程式。
當應用程式free掉不再需要的內存塊的時候, ptmalloc就會把這個內存塊標記為free, 並且在適當的時候查看到該內存塊的周圍的內存塊也處於free狀態的時候,那麼ptmalloc會嘗試將這些內存塊合併成一個更大的內存塊。
當然了ptmalloc裡面還做了很多優化。例如, 小內存塊的使用是很頻繁的, 所以在ptmalloc裡面針對小內存塊設立了一個fastbin,專門使用單鍊表來記錄這些小內存塊, 以方便在下次應用程式需要分配小內存塊的時候可以更加迅速的得到響應。
為了可以了解ptmalloc的內存管理情況, 有3個概念我們需要了解一下。
第一個是malloc_chunk, 如下是其定義
struct malloc_chunk { INTERNAL_SIZE_T mchunk_prev_size; INTERNAL_SIZE_T mchunk_size; struct malloc_chunk* fd; struct malloc_chunk* bk;
struct malloc_chunk* fd_nextsize; struct malloc_chunk* bk_nextsize;};
該malloc_chunk位於每一個小塊內存的最前端, 當該內存塊處於被分配狀態的時候,從fd開始的內存區域都屬於用戶數據區域。也就是說此時fd, bk, fd_nextsize, bk_nextsize都是無效的。
同時 mchunk_size的低3位有特殊用途, 最低位(稱之為P)用於指示前一個內存塊是否處於空閒狀態, 次低位(稱之為M)表明當前的內存塊是否來源於mmap,倒數第三個低位(稱之為A)表示當前的內存塊屬於主分配區還是非主分配區。
mchunk_size&(~0x7)就是當前內存塊的真實大小。 而當該內存塊處於空閒狀態,那麼fd和bk就將用來形成鍊表, 根據chunk的大小不同,可能會形成單向鍊表, 雙向鍊表, 或者跳表。
第二個需要了解的是malloc_state
struct malloc_state{ ...
mfastbinptr fastbinsY[NFASTBINS];
mchunkptr top; mchunkptr bins[NBINS * 2 - 2];
struct malloc_state *next;
struct malloc_state *next_free;
INTERNAL_SIZE_T system_mem; INTERNAL_SIZE_T max_system_mem;};typedef struct malloc_state *mstate;
上面的數據結構裡面去掉了一些這裡不需要講解的field。一個malloc_state代表一個用於內存分配的heap。 多個heap可以通過next連結成一個鍊表。 malloc_state的fastBinsY的每一項都指向一個相同大小內存塊的單鍊表。而bins中則是使用兩項分別作為雙鍊表的head和tail,來形成一個雙向鍊表。
第三個需要連結的是heap_info.這個概念是因為多線程而引進的。
typedef struct _heap_info{ mstate ar_ptr; struct _heap_info *prev; size_t size; size_t mprotect_size; ...
char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK];} heap_info;
一個應用程式一定有一個主分配區, 主分配區在ptmalloc中對應一個靜態變量static struct malloc_state main_arena。 但是對於多線程的程序, 一般情況下新的線程會有新的mstate與之對應, 這個時候的mstate是heap_info的一部分, 多個heap_info通過heap_info::prev形成一個單鍊表。
為了能夠使用gdb+python查看內存分配的情況, 我們首先需要配置一下我們的環境。 以centos6為例。
1.首先需要安裝libc的調試符號。
·修改/etc/yum.repos.d/CentOS-Debuginfo.repo文件中的enabled為1
·使用命令 yum install yum-utils安裝debuginfo-install
·使用命令 debuginfo-install glibc安裝glibc的調試符號
2.安裝gdb(需要能夠支持python script的)
·yum install gdb
為了可以在gdb中查看內存的情況, 我們需要對剛才講到的幾個數據結構進行解析。 在gdb的python script中我們可以使用gdb.lookup_type來查找某個具體的數據結構symbol, 例如
#point to malloc_chunktype_mchunkptr = gdb.lookup_type("mchunkptr")#long is used for most address calculationtype_long = gdb.lookup_type("long")#point to heap_infotype_heapinfo = gdb.lookup_type("struct _heap_info").pointer()#point to malloc_statetype_mstate = gdb.lookup_type("struct malloc_state").pointer()還可以使用gdb.parse_and_eval來獲取某個變量的值, 例如:main_arena = gdb.parse_and_eval("main_arena")
有了gdb+python這個工具再加上前面了解的幾個基本概念, 我們就可以製作一個東西幫我們來了解這個ptmalloc的內存管理情況了。
首先, 我們需要知道當前的process有多少個mstate, 通過main_arena我們就可以獲得該信息。 我們使用main_arena = gdb.parse_and_eval("main_arena")拿到main_arena的值以後, 可以通過malloc_state::next來找到下一個mstate, 一直到當next指向main_arena自己的時候,所有的mstate就被找到了。
現在, 我們有了所有的mstate。 我們就可以通過mstate找到其上所有的小內存塊, 以及處於空閒狀態的小內存塊。 為了找到所有的小內存塊, 我們需要為每一個mstate代表的大內存塊確定一下邊界,也就是這個大內存塊的起始地址。由於mstate分為主分配區和非主分配區, 所以在解析mstate所代表的大內存塊的起始地址的時候也需要分別對待。
對於主分配區, 由於其主要使用sbrk來分配內存, 所所以找到sbrk_base和main_arena的top就可以確定其對應的內存塊其實地址。 而對於非主分配區, 每一個mstate實際上包含在一個heap_info裡面, 所以會稍微複雜一點,因為這個時候mstate指向的地址是heap_info的一部分, 通過mstate_address & (~HEAP_MASK)可以獲得heap_info指向的地址。然後我們可以通過heap_info中的size之類的field,找到其對應的內存的起始地址。
當找到大內存塊的起始地址以後,接下來我們就需要在其中找到所有的內存塊和處於空閒狀態的內存塊。 回憶剛才的內容, malloc_chunk通過mchunk_size(實際上是mchunk_size&(~0x07))就可以找到所有的內存塊了。 也就是從大內存塊的起點開始, 加上當前chunk的大小得到的位置即為新的小內存塊的起始地址,如此重複一直遍歷到當前大內存塊的結束。 這些所有的內存塊(佔用或者空閒狀態的小內存塊)都被查找出來了。
接下來我們需要查找出那些處於空閒狀態的內存塊。 這個時候malloc_state的fastbinsY和bins所分別代表的fast chunk和normal chunk鍊表就可以幫我們的忙了。我們首先遍歷fastbins。fastbins的每一項都是一個單鍊表,malloc_chunk::fd指向下一個相同大小的chunk。當fd指向空的時候表示鍊表結束。分析normal bins也非常方便, normal bins每兩項用來作為一個雙向鍊表的head和tail指針, 所以我們可以從tail開始一直遍歷到head指針結束。在normal bins中針對較大內存塊會採用跳表提高查找速度,不過這個對於我們解析空閒狀態的chunk沒有幫助,所以就可以忽略掉。
很明顯找到了所有的內存塊以及處於空閒的狀態的內存塊, 做一個集合差我們就可以知道處於分配狀態的內存塊有哪些了。而且我們有內存塊的大小,也可以按照內存的大小做一個分類。如果該內存塊是分配給一個struct或者class那麼我們還可以通過查找symbol來查看這個內存塊上的結構化數據。這裡(https://gist.github.com/ZhangHongQuan-Dianrong/c906e2f81844e336a883597dc56c69f4)
提供了一個可以用於解析ptmalloc內存分配的python腳本, 裡面實現了簡單的按照chunk大小查找已經分配的內存塊等基本功能。
有了這些小內存塊的分布情況, 我們在遇到有些極端情況,例如, 部署在客戶現場的某個程序發生了內存異常增長的情況而又不能直接調試的情況。 那麼我們就可以獲取該process的core。然後使用上面的方法對該core文件進行分析,找到大量被分配的內存的共性,然後進行分析。很可能這樣可以很快幫你找到問題的所在。 你還可以使用這個方法構思出更多可以幫助你的小工具。
註:
Glibc 內存管理
(https://paper.seebug.org/papers/Archive/refs/heap/glibc%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86ptmalloc%E6%BA%90%E4%BB%A3%E7%A0%81%E5%88%86%E6%9E%90.pdf)
Understanding glibc malloc
(https://sploitfun.wordpress.com/tag/ptmalloc/)
點擊回顧往期精彩內容
淺談訪談技巧:從「一問一答」到「聽講故事」
無印良品的設計
【譯文】跟著谷歌學學怎麼寫UX文案
淺析 Angular4 組件
Go+Vue.js快速搭建Web應用
Java Vs Scala
認識JS原型和繼承
談談對react-redux的理解
如何用比特幣高頻套利
Dianrongween|網際網路公司萬聖節這樣過
想了解更多請關注我們