Linux堆內存管理深刻分析前端
做者@走位,阿里聚安全git
在上一篇文章中(連接見文章底部),詳細介紹了堆內存管理中涉及到的基本概念以及相互關係,同時也着重介紹了堆中chunk分配和釋放策略中使用到的隱式鏈表技術。經過前面的介紹,咱們知道使用隱式鏈表來管理內存chunk總會涉及到內存的遍歷,效率極低。對此glibc malloc引入了顯示鏈表技術來提升堆內存分配和釋放的效率。github
所謂的顯示鏈表就是咱們在數據結構中經常使用的鏈表,而鏈表本質上就是將一些屬性相同的「結點」串聯起來,方便管理。在glibc malloc中這些鏈表統稱爲bin,鏈表中的「結點」就是各個chunk,結點的共同屬性就是:1)均爲free chunk;2)同一個鏈表中各個chunk的大小相等(有一個特例,詳情見後文)。算法
如前文所述,bin是一種記錄free chunk的鏈表數據結構。系統針對不一樣大小的free chunk,將bin分爲了4類:1) Fast bin; 2) Unsorted bin; 3) Small bin; 4) Large bin。c#
在glibc中用於記錄bin的數據結構有兩種,分別以下所示:數組
fastbinsY: 這是一個數組,用於記錄全部的fast bins;安全
bins: 這也是一個數組,用於記錄除fast bins以外的全部bins。事實上,一共有126個bins,分別是:數據結構
bin 1 爲unsorted bin;函數
bin 2 到63爲small bin;ui
bin 64到126爲large bin。
其中具體數據結構定義以下:
struct malloc_state { …… /* Fastbins */ mfastbinptr fastbinsY[NFASTBINS]; …… /* Normal bins packed as described above */ mchunkptr bins[NBINS * 2 - 2]; // #define NBINS 128 …… }; 這裏mfastbinptr的定義:typedef struct malloc_chunk *mfastbinptr; mchunkptr的定義:typedef struct malloc_chunk* mchunkptr; |
畫圖更直觀:
圖1-1 bins分類
那麼處於bins中個各個free chunk是如何連接在一塊兒的呢?回顧malloc_chunk的數據結構:
struct malloc_chunk { /* #define INTERNAL_SIZE_T size_t */ INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* 這兩個指針只在free chunk中存在*/ struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; }; |
其中的fd和bk指針就是指向當前chunk所屬的鏈表中forward或者backward chunk。
既然有fast bin,那就確定有fast chunk——chunk size爲16到80字節的chunk就叫作fast chunk。爲了便於後文描述,這裏對chunk大小作以下約定:
1) 只要說到chunk size,那麼就表示該malloc_chunk的實際總體大小;
2) 而說到chunk unused size,就表示該malloc_chunk中刨除諸如prev_size, size, fd和bk這類輔助成員以後的實際可用的大小。所以,對free chunk而言,其實際可用大小老是比實際總體大小少16字節。
在內存分配和釋放過程當中,fast bin是全部bin中操做速度最快的。下面詳細介紹fast bin的一些特性:
1) fast bin的個數——10個
2)每一個fast bin都是一個單鏈表(只使用fd指針)。爲何使用單鏈表呢?由於在fast bin中不管是添加仍是移除fast chunk,都是對「鏈表尾」進行操做,而不會對某個中間的fast chunk進行操做。更具體點就是LIFO(後入先出)算法:添加操做(free內存)就是將新的fast chunk加入鏈表尾,刪除操做(malloc內存)就是將鏈表尾部的fast chunk刪除。須要注意的是,爲了實現LIFO算法,fastbinsY數組中每一個fastbin元素均指向了該鏈表的rear end(尾結點),而尾結點經過其fd指針指向前一個結點,依次類推,如圖2-1所示。
3) chunk size:10個fast bin中所包含的fast chunk size是按照步進8字節排列的,即第一個fast bin中全部fast chunk size均爲16字節,第二個fast bin中爲24字節,依次類推。在進行malloc初始化的時候,最大的fast chunk size被設置爲80字節(chunk unused size爲64字節),所以默認狀況下大小爲16到80字節的chunk被分類到fast chunk。詳情如圖2-1所示。
4) 不會對free chunk進行合併操做。鑑於設計fast bin的初衷就是進行快速的小內存分配和釋放,所以系統將屬於fast bin的chunk的P(未使用標誌位)老是設置爲1,這樣即便當fast bin中有某個chunk同一個free chunk相鄰的時候,系統也不會進行自動合併操做,而是保留二者。雖然這樣作可能會形成額外的碎片化問題,但瑕不掩瑜。
5) malloc(fast chunk)操做:即用戶經過malloc請求的大小屬於fast chunk的大小範圍(注意:用戶請求size加上16字節就是實際內存chunk size)。在初始化的時候fast bin支持的最大內存大小以及全部fast bin鏈表都是空的,因此當最開始使用malloc申請內存的時候,即便申請的內存大小屬於fast chunk的內存大小(即16到80字節),它也不會交由fast bin來處理,而是向下傳遞交由small bin來處理,若是small bin也爲空的話就交給unsorted bin處理:
/* Maximum size of memory handled in fastbins. */ static INTERNAL_SIZE_T global_max_fast;
/* offset 2 to use otherwise unindexable first 2 bins */ /*這裏SIZE_SZ就是sizeof(size_t),在32位系統爲4,64位爲8,fastbin_index就是根據要malloc的size來快速計算該size應該屬於哪個fast bin,即該fast bin的索引。由於fast bin中chunk是從16字節開始的,全部這裏以8字節爲單位(32位系統爲例)有減2*8 = 16的操做!*/ #define fastbin_index(sz) \ ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
/* The maximum fastbin request size we support */ #define MAX_FAST_SIZE (80 * SIZE_SZ / 4)
#define NFASTBINS (fastbin_index (request2size (MAX_FAST_SIZE)) + 1)
|
那麼fast bin 是在哪?怎麼進行初始化的呢?當咱們第一次調用malloc(fast bin)的時候,系統執行_int_malloc函數,該函數首先會發現當前fast bin爲空,就轉交給small bin處理,進而又發現small bin 也爲空,就調用malloc_consolidate函數對malloc_state結構體進行初始化,malloc_consolidate函數主要完成如下幾個功能:
a. 首先判斷當前malloc_state結構體中的fast bin是否爲空,若是爲空就說明整個malloc_state都沒有完成初始化,須要對malloc_state進行初始化。
b. malloc_state的初始化操做由函數malloc_init_state(av)完成,該函數先初始化除fast bin以外的全部的bins(構建雙鏈表,詳情見後文small bins介紹),再初始化fast bins。
而後當再次執行malloc(fast chunk)函數的時候,此時fast bin相關數據不爲空了,就開始使用fast bin(見下面代碼中的※1部分):
static void * _int_malloc (mstate av, size_t bytes) { …… /* If the size qualifies as a fastbin, first check corresponding bin. This code is safe to execute even if av is not yet initialized, so we can try it without checking, which saves some time on this fast path. */ //第一次執行malloc(fast chunk)時這裏判斷爲false,由於此時get_max_fast ()爲0 if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ())) { ※1 idx = fastbin_index (nb); mfastbinptr *fb = &fastbin (av, idx); mchunkptr pp = *fb; do { victim = pp; if (victim == NULL) break; } ※2 while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))!= victim); if (victim != 0) { if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0)) { errstr = "malloc(): memory corruption (fast)"; errout: malloc_printerr (check_action, errstr, chunk2mem (victim)); return NULL; } check_remalloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } |
獲得第一個來自於fast bin的chunk以後,系統就將該chunk從對應的fast bin中移除,並將其地址返回給用戶,見上面代碼※2處。
6) free(fast chunk)操做:這個操做很簡單,主要分爲兩步:先經過chunksize函數根據傳入的地址指針獲取該指針對應的chunk的大小;而後根據這個chunk大小獲取該chunk所屬的fast bin,而後再將此chunk添加到該fast bin的鏈尾便可。整個操做都是在_int_free函數中完成。
在main arena中Fast bins(即數組fastbinsY)的總體操做示意圖以下圖所示:
圖2-1 fast bin示意圖
當釋放較小或較大的chunk的時候,若是系統沒有將它們添加到對應的bins中(爲何,在什麼狀況下會發生這種事情呢?詳情見後文),系統就將這些chunk添加到unsorted bin中。爲何要這麼作呢?這主要是爲了讓「glibc malloc機制」可以有第二次機會從新利用最近釋放的chunk(第一次機會就是fast bin機制)。利用unsorted bin,能夠加快內存的分配和釋放操做,由於整個操做都再也不須要花費額外的時間去查找合適的bin了。
Unsorted bin的特性以下:
1) unsorted bin的個數: 1個。unsorted bin是一個由free chunks組成的循環雙鏈表。
2) Chunk size: 在unsorted bin中,對chunk的大小並無限制,任何大小的chunk均可以歸屬到unsorted bin中。這就是前言說的特例了,不過特例並不是僅僅這一個,後文會介紹。
小於512字節的chunk稱之爲small chunk,small bin就是用於管理small chunk的。就內存的分配和釋放速度而言,small bin比larger bin快,但比fast bin慢。
Small bin的特性以下:
1) small bin個數:62個。每一個small bin也是一個由對應free chunk組成的循環雙鏈表。同時Small bin採用FIFO(先入先出)算法:內存釋放操做就將新釋放的chunk添加到鏈表的front end(前端),分配操做就從鏈表的rear end(尾端)中獲取chunk。
2) chunk size: 同一個small bin中全部chunk大小是一?樣的,且第一個small bin中chunk大小爲16字節,後續每一個small bin中chunk的大小依次增長8字節,即最後一個small bin的chunk爲16 + 62 * 8 = 512字節。
3) 合併操做:相鄰的free chunk須要進行合併操做,即合併成一個大的free chunk。具體操做見下文free(small chunk)介紹。
4) malloc(small chunk)操做:相似於fast bins,最初全部的small bin都是空的,所以在對這些small bin完成初始化以前,即便用戶請求的內存大小屬於small chunk也不會交由small bin進行處理,而是交由unsorted bin處理,若是unsorted bin也不能處理的話,glibc malloc就依次遍歷後續的全部bins,找出第一個知足要求的bin,若是全部的bin都不知足的話,就轉而使用top chunk,若是top chunk大小不夠,那麼就擴充top chunk,這樣就必定能知足需求了(還記得上一篇文章中在Top Chunk中留下的問題麼?答案就在這裏)。注意遍歷後續bins以及以後的操做一樣被large bin所使用,所以,將這部份內容放到large bin的malloc操做中加以介紹。
那麼glibc malloc是如何初始化這些bins的呢?由於這些bin屬於malloc_state結構體,因此在初始化malloc_state的時候就會對這些bin進行初始化,代碼以下:
malloc_init_state (mstate av) { int i; mbinptr bin;
/* Establish circular links for normal bins */ for (i = 1; i < NBINS; ++i) { bin = bin_at (av, i); bin->fd = bin->bk = bin; } …… } |
注意在malloc源碼中,將bins數組中的第一個成員索引值設置爲了1,而不是咱們經常使用的0(在bin_at宏中,自動將i進行了減1處理…)。從上面代碼能夠看出在初始化的時候glibc malloc將全部bin的指針都指向了本身——這就表明這些bin都是空的。
事後,當再次調用malloc(small chunk)的時候,若是該chunk size對應的small bin不爲空,就從該small bin鏈表中取得small chunk,不然就須要交給unsorted bin及以後的邏輯來處理了。
5) free(small chunk):當釋放small chunk的時候,先檢查該chunk相鄰的chunk是否爲free,若是是的話就進行合併操做:將這些chunks合併成新的chunk,而後將它們從small bin中移除,最後將新的chunk添加到unsorted bin中。
大於512字節的chunk稱之爲large chunk,large bin就是用於管理這些large chunk的。
Large bin的特性以下:
1) large bin的數量:63個。Large bin相似於small bin,只是須要注意兩點:一是同一個large bin中每一個chunk的大小能夠不同,但必須處於某個給定的範圍(特例2) ;二是large chunk能夠添加、刪除在large bin的任何一個位置。
在這63個large bins中,前32個large bin依次以64字節步長爲間隔,即第一個large bin中chunk size爲512~575字節,第二個large bin中chunk size爲576 ~ 639字節。緊隨其後的16個large bin依次以512字節步長爲間隔;以後的8個bin以步長4096爲間隔;再以後的4個bin以32768字節爲間隔;以後的2個bin以262144字節爲間隔;剩下的chunk就放在最後一個large bin中。
鑑於同一個large bin中每一個chunk的大小不必定相同,所以爲了加快內存分配和釋放的速度,就將同一個large bin中的全部chunk按照chunk size進行從大到小的排列:最大的chunk放在鏈表的front end,最小的chunk放在rear end。
2) 合併操做:相似於small bin。
3) malloc(large chunk)操做:
初始化完成以前的操做相似於small bin,這裏主要討論large bins初始化完成以後的操做。首先肯定用戶請求的大小屬於哪個large bin,而後判斷該large bin中最大的chunk的size是否大於用戶請求的size(只須要對比鏈表中front end的size便可)。若是大於,就從rear end開始遍歷該large bin,找到第一個size相等或接近的chunk,分配給用戶。若是該chunk大於用戶請求的size的話,就將該chunk拆分爲兩個chunk:前者返回給用戶,且size等同於用戶請求的size;剩餘的部分作爲一個新的chunk添加到unsorted bin中。
若是該large bin中最大的chunk的size小於用戶請求的size的話,那麼就依次查看後續的large bin中是否有知足需求的chunk,不過須要注意的是鑑於bin的個數較多(不一樣bin中的chunk極有可能在不一樣的內存頁中),若是按照上一段中介紹的方法進行遍歷的話(即遍歷每一個bin中的chunk),就可能會發生屢次內存頁中斷操做,進而嚴重影響檢索速度,因此glibc malloc設計了Binmap結構體來幫助提升bin-by-bin檢索的速度。Binmap記錄了各個bin中是否爲空,經過bitmap能夠避免檢索一些空的bin。若是經過binmap找到了下一個非空的large bin的話,就按照上一段中的方法分配chunk,不然就使用top chunk來分配合適的內存。
4) Free(large chunk):相似於small chunk。
瞭解上面知識以後,再結合下圖5-1,就不難理解各種bins的處理邏輯了:
至此glibc malloc中涉及到的全部顯示鏈表技術已經介紹完畢。鑑於篇幅和精力有限,本文沒能詳細介紹完全部的技術細節,可是我相信帶着這些知識點再去研究glibc malloc的話,定能起到事半功倍的效果。
另外,就我我的所瞭解到的基於堆溢出攻擊而言,掌握以上知識,已經足夠理解絕大部分堆溢出攻擊技術了。所以,後面的文章將會結合這些知識詳細介紹各個攻擊技術的實現原理。
老規矩:若有錯誤,歡迎斧正!
做者:走位@阿里聚安全,更多安全技術文章,請點擊阿里聚安全博客