GDB內存調試初探二

  • 背景

本文記錄了筆者調試一個簡單的由C語言編寫的嵌入式應用,在main函數執行之後,第一次調用內存分配函數時,glibc的執行過程。對於普通的二進制應用,Linux內加載應用並執行,首先運行的是動態鏈接器;在應用的main函數被調用之前,動態鏈接器也會分配內存:不過動態鏈接器沒用使用到libc.so中的內存分配功能模塊,據筆者當前的調試經歷,動態鏈接器是過mmap來分配私有的匿名內存的。

筆者調試的應用,在main函數中間接地調用了fgets(…)這個函數,在fgets()函數調用的另一些函數中,最終會嘗試分配1024字節的內存空間大小。有興趣的讀者可以自行編寫簡單的應用來調試。

 

  • 與ptmalloc相關的重要結構體

在glibc源碼中,malloc/malloc.c文件中包含了大量的註釋說明信息,爲我們理解、調試ptmalloc功能有很大的幫助;筆者在此就不再鸚鵡學舍了,爲了方便大家查看,筆者對兩個重要結構體圖如下:

其中mchunkptr被定義爲malloc_chunk的結構體指針;對於ARMv7平臺,NFASTBINS爲10,NBINS爲128,BINMAPSIZE爲4。結構體malloc_state的指針通常被定義爲mstate;詳細的定義請查看glibc源碼。

 

  • 初始化函數ptmalloc_init

在筆者的博客文章《GDB內存調試初探一》中,提到了__libc_malloc函數會通過一個鉤子來初始化內存狀態結構體靜態變量main_arena;但經筆者調試發現ptmalloc_init()函數並未修改此變量,而僅僅將其地址寫入指針變量thread_arena中。首先,在執行ptmalloc_init函數前後加入斷點:

之後,在兩個斷點觸發時查看main_arena變量:

經比較可知,執行函數ptmalloc_init前後靜態結構體變量main_arena沒有被修改。注意到main_arena結構體中的next指針指向該結構體本身。不過,函數ptmalloc_init對進程的環境變量進行了一些處理,即檢測有沒有與內存分配相關的環境變量;如果有,就會對內存分配的功能進行一些調整。當前調試的應用使用的是默認的環境變量,沒有與內存分配相關的環境變量。

此時main_arena中的max_system變量和max_system_mem均爲零,說明內存分配狀態結構體不能分配內存;如果要爲應用分配內存,那麼須先向Linux內核發起分配內存的系統調用。

 

  • _int_malloc對分配內存大小的修改

在函數ptmalloc_init執行完成之後,會跳轉回到__libc_malloc()函數;後者會調用到_int_malloc函數進一步基於main_arena結構體變量來分配內存。本次調試的應用請求的內存分配大小爲1024個字節,不過最終分配的內存比1024字節稍大一些,這是通過宏定義checked_request2size來實現的:

如上圖,函數_int_malloc的第二個參數bytes爲1024,經過checked_request2size宏處理,函數局部變量nb被賦值成爲1032,即最終會嘗試分配1032個字節;不過對於應用而言,僅使用1024個字節是安全的:ptmalloc內存分配模塊會使用1032字節的前幾個字節保存一些內存分配相關的信息。

 

  • 根據分配內存大小的一些特殊處理

接下來函數_int_malloc會根據請求分配的內存大小進行一些特殊的處理。不過此次調試的應用請求分配的內存大小爲1032個字節(即nb = 1032),均不滿足條件,下圖中的兩個條件分支均沒有執行:

現在這兩個分支均未執行,並不是很重要:此時內存分配狀態結構體中沒用空閒的內存供以分配:最終函數_int_malloc不得不調用sysmalloc以向內核請求更多的內存。

 

  • 主線程malloc_state的初始化

上面提到,ptmalloc_init並未修改main_arena結構體變量;實際上,main_arena的初始化函數爲malloc_init_state()。不過,該函數也不會爲main_arena向Linux內核申請可用的內存,它僅僅是將其初始化;初始化完成後,main_arena結構體會發生一些變化:

上面的調試流程爲:_int_malloc(…) -> malloc_consolidate(…) -> malloc_init_state(…);上圖的調試結果爲返回到_int_malloc(…)的結果。函數malloc_init_state將global_max_fast賦值爲64;該變量的作用留待以後考察。注意,main_arena結構體指針top被初始化爲top指針所在的地址;結構體中的bins指針數組也指向臨近的內存空間。這是很有趣的設計,其具體的功能特點尚待我們去發掘。

 

  • 通過系統調用brk向內存申請內存

以上的分析結果說明,儘管main_arena被函數malloc_init_state()初始化,但其沒有可用的內存空間分配給應用。它必須向內核申請內存;那麼就直接給分配/釋放內存的系統調用brk加上捕捉斷點(catch):

上圖給出了完整的調用棧回溯信息,不過下圖給出了該系統調用的前後信息:

此次系統調用的參數爲0,即系統調用爲brk(NULL),並沒有向Linux內核申請內存。內核返回的值應當被視爲當前堆(Heap)的起始地址。之後,brk系統調用再次被觸發:

上圖的調試結果表明,ptmalloc內存分配模塊爲main_arena向Linux內核申請了0x21000個字節的內存空間,即132K(或135168字節),這遠遠大於應用第一次請求分配的1024個字節的空間大小。那麼可推測,剩餘的空間會被記錄在main_arena結構體中,留待下次應用分配內存時使用。

 

  • 首次向內核申請內存後對malloc_state的修改

應用只要求libc分配1K大小的內存,但libc卻一次性向內核請求了132K大小的內存。main_arena中需要記錄這些信息:

如上圖,top指向了堆空間的起始地址;在堆空間的起始地址偏移4字節處,寫入了堆空間的大小與PREV_INUSE相與,即0x21001(上圖中的135169)。

 

  • 主線程第一次分配內存的返回值

首先讓我們根據代碼先計算一下爲了嚮應用分配1024字節的空間,需要對main_arena進行哪些修改,以及嚮應用返回的內存地址:

上圖的計算結顯示,分配的內存地址應該爲0x2a013008。然後加入一個新的斷點,讓上圖的代碼執行完成並返回至_int_malloc函數中,查看內存分配狀態結構體main_arena:

由此可以確定依據C代碼手動計算的結果與實際運行的結果一致。最後在函數__libc_malloc的返回地址加上斷點,查看其返回值,與我們的預測結果也是一致的:

至此,我們對簡單應用第一次分配內存的基本的流程,就有了一個基本的瞭解了。

  • 總結

對於簡單的C語言編寫的應用,在main函數執行之後纔會對glibc中的內存分配模塊ptmalloc進行初始化。該模塊通過brk系統調用向Linux內存申請堆空間,並將其存儲於main_arena結構體中。該結構體的用於記錄主線程的內存的分配與釋放信息。具體地來說,本次調試案例分配的堆內存地址空間起始地址爲0x2a013000,而應用獲得的內存地址爲0x2a013008,共偏移了8個字節,即兩個4字節。該兩個4字節分別對應結構體malloc_chunk中的mchunk_prev_size和mchun_size。筆者已將本次調試的簡單應用及源碼上傳至下載區,有需要的可以下載自己調試分析。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章