使用gdb+python查看C/C++進程內存分配情況

2021-03-02 點融黑幫

內存使用和管理在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|網際網路公司萬聖節這樣過

想了解更多請關注我們

相關焦點

  • GDB入門教程之如何使用GDB啟動調試
    可以使用 Linux 命令 ps-ef|grep-w demo 、 ps-aux|grep-w demo 或 pidof demo 獲取到 demo 進程當前的進程號。獲取到待調試的目標進程號後 (假設為 pid ),可以使用 gdb 命令進入 GDB 終端,並使用 attach pid 的方式啟動對當前正在運行的 demo 進程的 GDB 調試。
  • Linux C/C++ 開發人員要熟練掌握 GDB 調試代碼塊
    一、啟動GDB調試使用 GDB 調試程序一般有三種方式: gdb filename gdb attach pid gdb filename corename1、直接調試目標程序2、附加進程3、調試 core
  • C/C++程序調試和內存檢測
    2、使用gdb進行程序調試常用功能命令:g++ -g -o test test.cpp  //編譯時加上-g參數1、啟動gdb: gdb test2、help3、具備帶有歷史記錄的命令行編輯功能,方向鍵選擇之前執行過的命令
  • python+C、C++混合編程的應用
    有的語言專注於簡單高效,比如python,內建的list,dict結構比c/c++易用太多,但同樣為了安全、易用,語言也犧牲了部分性能。在有些領域,比如通信,性能很關鍵,但並不意味這個領域的coder只能苦苦掙扎於c/c++的陷阱中,比如可以使用多種語言混合編程。
  • MySQL內存不釋放分析
    通過gdb調試結論線上MySQL資料庫發現一些實例,內存使用不斷增高,並且當連接數斷開後內存不會釋放,最終導致的結果是被作業系統OOM問題分析模擬兩個場景來分析此問題:場景1 使用sysbench壓測資料庫使用sysbench壓測MySQL,等待連接斷開後,使用top查看mysqld進程,內存使用情況將mysql innodb_buffer_pool
  • c++之內存分配、命名空間、強制類型轉換學習總結
    一、C++動態內存分配:在學習c語言的時候,我們一般都是使用庫函數malloc()來進行內存的申請分配,然後使用庫函數free()來進行釋放申請到的內存;現在在c++裡面採用了另外一種內存申請的方法:c++中通過
  • GDB調試實戰三完整的數據流程
    這裡需要注意的是,如果不調試了,要馬上停止這個進程,否則會產生大量的垃圾日誌。二、啟動調試按照上面兩篇文件的方法啟動啟動進程:「nohup ./gdbTest >1.log 2>&1 &」。
  • 獨家|Linux進程內存用量分析之堆內存篇
    本文將介紹幾種內存洩漏檢測工具,並通過實際例子介紹一種分析堆內存佔用量的工具和方法,幫助定位內存膨脹問題。背景進程的內存管理是每一個開發者必須要考慮的問題,對於C++程序進程來說,出現問題很多情況下都與內存掛鈎。進程崩潰問題通常可以使用gdb等調試工具輕鬆排查並解決。
  • GDB與Valgrind ,調試C++代碼內存的工具
    1、利用 GDB 調試 CoreDumpCoreDump時一個二進位的文件,進程發生錯誤崩潰時,內核會產生一個瞬時的快照,記錄該進程的內存、運行堆棧狀態等信息保存在core文件之中。做個簡單的類比,core 文件相當於飛機運行時的"黑匣子",能夠幫助我們更好的調試 C++程序的問題。
  • Python中的內存分配和管理
    了解內存管理可以幫助您編寫高效的Python代碼。儘管您可能無法控制內存分配,但是您可以優化程序來更好地分配內存。在深入研究之前,請記住:在python中,一切都是對象。與C,C ++或Java不同,值存儲在內存中,並且變量指向該內存位置。
  • C/C++程序內存問題分析
    無限申請內存的情況,一般發生程序出現死循環或類似死循環的情況中。使用 mmap 直接映射的 chunk 在釋放時直接解除映射,而不再屬於進程的內存空間,任何對該內存的訪問都會產生段錯誤。而在heap中分配的空間則可能會留在進程內存空間內,也就是說調用free()時,並沒有將內存真正地歸還給OS,free只是將這塊內存放到了free list裡,brk分配的內存需要等到高地址內存釋放以後才能釋放。
  • GDB調試還不會?看這篇就夠了!
    file查看strip狀況下面的情況也是不可調試的:file helloWorldhelloWorld: (省略前面內容) stripped如果最後是stripped,則說明該文件的符號表信息和調試信息已被去除,不能使用gdb調試。但是not stripped的情況並不能說明能夠被調試。
  • Linux平臺中調試C/C++內存洩漏方法 (騰訊和MTK面試的時候問到的)
    內存洩漏一般指的是堆內存的洩漏。堆內存是指程序從堆中分配的、大小任意的(內存塊的大小可以在程序 運行期決定)、使用完後必須顯示的釋放的內存。應用程式一般使用malloc、realloc、new 等函數從堆中分配到一塊內存,使用完後,程序必須負責相應的調用 free 或 delete 釋放該內存塊。否則,這塊內存就不能被再次使用,我們就說這塊內存洩漏了。1.
  • Java堆外內存排查小結
    版本的更改如下:使用jps 查看啟動參數,發現分配了大約3GB的堆內存[root]$ jps -v75 Bootstrap -Xmx3000m -Xms3000m  -verbose:gc -Xloggc:/home/logs/gc.log -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSCompactAtFullCollection
  • C 2 C++進階篇(1)
    首先談談筆者的水平,只學過c和數據結構,接觸過指針,對於取地址&從來沒有接觸過(因為據說是老師說不符合嚴謹的c....), python
  • 常用 GDB 命令中文速覽
    — 刪除自動顯示項help — 列印命令列表(帶參數時查找命令的幫助),縮寫為 hattach — 掛接到已在運行的進程來調試run — 啟動被調試的程序,縮寫為 rbacktrace — 查看程序調用棧的信息,縮寫為 btptype — 列印類型 TYPE 的定義break使用
  • C語言calloc的效率為何那麼高?作業系統究竟是如何分配內存的?
    ,得到如下結果:# gcc t.c# time .修改後的C語言代碼編譯並執行這段C語言代碼,同樣使用 time 命令查看程序運行消耗時間,得到如下結果:# gcc t.c# time .有了這樣的機制,一個進程的崩潰不會導致其他進程跟著崩潰,系統的穩定性會得到保障。因此,在作業系統內核的管理下,當某段C語言代碼需要使用一段內存時,它不能直接使用物理內存,而只能通過 mmap() 以及 sbrk() 等系統調用向內核申請,由內核修改頁表為每個進程提供 RAM。
  • 騰訊C++後臺開發面試筆試知識點參考筆記
    查看內存(gdb)p &a //列印變量地址(gdb)x 0xbffff543 //查看內存單元內變量0xbffff543: 0x12345678(gdb) x /4xb 0xbffff543 //單字節查看4個內存單元變量的值0xbffff543: 0x78 0x56 0x34 0x12多線程調試(gdb) info threads:查看GDB當前調試的程序的各個線程的相關信息
  • 用圖文帶你徹底弄懂GDB調試原理
    這篇文章的重點是理解gdb底層的調試機制,所以應用層的這些指令的使用方法就不再列出了,網絡上的資源很多。「程序」描述的是一個靜態的概念,就是一堆數據躺著硬碟上,而「進程」描述的是動態的過程,是這個程序被讀取、加載到內存上之後,在作業系統中有一個任務控制塊(一個數據結構),專門用來管理這個進程的。鋪墊了半天,終於輪到主角登場了,那就是系統調用函數ptrace(其中的參數後面會解釋),正是在它的幫助下,gdb才擁有了強大的調試能力。
  • linux下進程和線程狀態查看
    每進程可用線程數 = VIRT上限/stack size   32位x86系統默認的VIRT上限是3G(內存分配的3G+1G方式),64位x86系統默認的VIRT上限是64G用 ulimit -s 可以查看默認的線程棧大小,一般情況下,這個值是 8M[8192]查看最大線程數:cat /proc/sys/kernel/threads-max