Linux堆記憶體管理深入分析

FreeeLinux發表於2017-02-02
Linux堆記憶體管理深入分析
https://jaq.alibaba.com/community/art/show?articleid=315
http://www.cnblogs.com/alisecurity/p/5520847.html



前言

近年來,漏洞挖掘越來越火,各種漏洞挖掘、利用的分析文章層出不窮。從大方向來看,主要有基於棧溢位的漏洞利用和基於堆溢位的漏洞利用兩種。國內關於棧溢位的資料相對較多,這裡就不累述了,但是關於堆溢位的漏洞利用資料就很少了。鄙人以為主要是堆溢位漏洞的門檻較高,需要先吃透相應作業系統的堆記憶體管理機制,而這部分內容一直是一個難點。因此本系列文章主要從Linux系統堆記憶體管理機制出發,逐步介紹諸如基本堆溢位漏洞、基於unlink的堆溢位漏洞利用、double freeuse-after-free等等常見的堆溢位漏洞利用技術。

 

前段時間偶然學習了這篇文章:

https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/comment-page-1/


該文是我近段時間以來讀到的最好文章之一,文章淺顯易懂,條例清晰,作為初學者的我從中學到了很多linux堆記憶體管理相關的知識。但是估計由於篇幅的限制,該文對很多難點一帶而過,造成部分知識點理解上的困難。因此我決定以該文為藍本,結合其他參考資料和自己的理解,寫一篇足夠詳細、完整的linux堆管理介紹文章,希冀能夠給其他初學者獻上微末之力。所以就內容來源而言,本文主要由兩部分組成:一部分是翻譯的上面提及的文章;另一部分是筆者結合其他參考資料和自己的理解新增的補充說明。鑑於筆者知識能力上的不足,如有問題歡迎各位大牛斧正!


同樣的,鑑於篇幅過長,我將文章分成了上下兩部分,上部分主要介紹堆記憶體管理中的一些基本概念以及相互關係,同時也著重介紹了堆中chunk分配和釋放策略中使用到的隱式連結串列技術。後半部分主要介紹glibc malloc為了提高堆記憶體分配和釋放的效率,引入的顯示連結串列技術,即binlist的概念和核心原理。其中使用到的原始碼在:

https://github.com/sploitfun/lsploits/tree/master/glibc


堆記憶體管理簡介


當前針對各大平臺主要有如下幾種堆記憶體管理機制:

dlmalloc – General purpose allocator

ptmalloc2 – glibc

jemalloc – FreeBSD and Firefox

tcmalloc – Google

libumem – Solaris

 

本文主要學習介紹在linux glibc使用的ptmalloc2實現原理。

本來linux預設的是dlmalloc,但是由於其不支援多執行緒堆管理,所以後來被支援多執行緒的prmalloc2代替了。

當然在linux平臺*malloc本質上都是通過系統呼叫brk或者mmap實現的。關於這部分內容,一定要學習下面這篇文章:

https://sploitfun.wordpress.com/2015/02/11/syscalls-used-by-malloc/


鑑於篇幅,本文就不加以詳細說明了,只是為了方便後面對堆記憶體管理的理解,擷取其中函式呼叫關係圖:

1-1 函式呼叫關係圖


系統記憶體分佈圖:

1-2系統記憶體分佈圖

 

實驗演示


試想有如下程式碼:


/* Per thread arena example. */

#include <stdio.h>

#include <stdlib.h>

#include <pthread.h>

#include <unistd.h>

#include <sys/types.h>

 

void* threadFunc(void* arg) {

        printf("Before malloc in thread 1\n");

        getchar();

        char* addr = (char*) malloc(1000);

        printf("After malloc and before free in thread 1\n");

        getchar();

        free(addr);

        printf("After free in thread 1\n");

        getchar();

}

 

int main() {

        pthread_t t1;

        void* s;

        int ret;

        char* addr;

 

        printf("Welcome to per thread arena example::%d\n",getpid());

        printf("Before malloc in main thread\n");

        getchar();

        addr = (char*) malloc(1000);

        printf("After malloc and before free in main thread\n");

        getchar();

        free(addr);

        printf("After free in main thread\n");

        getchar();

        ret = pthread_create(&t1, NULL, threadFunc, NULL);

        if(ret)

        {

                printf("Thread creation error\n");

                return -1;

        }

        ret = pthread_join(t1, &s);

        if(ret)

        {

                printf("Thread join error\n");

                return -1;

        }

        return 0;

}

 

下面我們依次分析其各個階段的堆記憶體分佈狀況。

1. Before malloc in main thread :

在程式呼叫malloc之前程式程式中是沒有heap segment的,並且在建立在建立執行緒前,也是沒有執行緒堆疊的。

2. After malloc in main thread :

在主執行緒中呼叫malloc之後,就會發現系統給程式分配了堆疊,且這個堆疊剛好在資料段之上:



這就說明它是通過brk系統呼叫實現的。並且,還可以看出雖然我們只申請了1000位元組的資料,但是系統卻分配了132KB大小的堆,這是為什麼呢?原來這132KB的堆空間叫做arena,此時因為是主執行緒分配的,所以叫做main arena(每個arena中含有多個chunk,這些chunk以連結串列的形式加以組織)。由於132KB1000位元組大很多,所以主執行緒後續再聲請堆空間的話,就會先從這132KB的剩餘部分中申請,直到用完或不夠用的時候,再通過增加program break location的方式來增加main arena的大小。同理,當main arena中有過多空閒記憶體的時候,也會通過減小program break location的方式來縮小main arena的大小。


3. After free in main thread :

在主執行緒呼叫free之後:從記憶體佈局可以看出程式的堆空間並沒有被釋放掉,原來呼叫free函式釋放已經分配了的空間並非直接返還給系統,而是由glibc malloc庫函式加以管理。它會將釋放的chunk新增到main arenasbin(這是一種用於儲存同型別free chunk的雙連結串列資料結構,後問會加以詳細介紹)中。在這裡,記錄空閒空間的freelist資料結構稱之為bins。之後當使用者再次呼叫malloc申請堆空間的時候,glibc malloc會先嚐試從bins中找到一個滿足要求的chunk,如果沒有才會向作業系統申請新的堆空間。如下圖所示:


 

4. Before malloc in thread1 :

thread1呼叫malloc之前:從輸出結果可以看出thread1中並沒有heap segment,但是此時thread1自己的棧空間已經分配完畢了:


 

5. After malloc in thread1 :

thread1呼叫malloc之後:從輸出結果可以看出thread1heap segment已經分配完畢了,同時從這個區域的起始地址可以看出,它並不是通過brk分配的,而是通過mmap分配,因為它的區域為b7500000-b76000001MB,並不是同程式的data segment相鄰。同時,我們還能看出在這1MB中,根據記憶體屬性分為了2部分:0xb7500000-0xb7520000132KB大小的空間是可讀可寫屬性;後面的是不可讀寫屬性。原來,這裡只有可讀寫的132KB空間才是thread1的堆空間,即thread1 arena


 

6. thread1呼叫free之後:同main thread


3 Arena介紹


3.1 Arena數量限制


在第2章中我們提到main threadthread1有自己獨立的arena,那麼是不是無論有多少個執行緒,每個執行緒都有自己獨立的arena呢?答案是否定的。事實上,arena的個數是跟系統中處理器核心個數相關的,如下表所示:


For 32 bit systems:

     Number of arena = 2 * number of cores + 1.

For 64 bit systems:

     Number of arena = 8 * number of cores + 1.

3.2 Arena的管理


假設有如下情境:一臺只含有一個處理器核心的PC機安裝有32位作業系統,其上執行了一個多執行緒應用程式,共含有4個執行緒——主執行緒和三個使用者執行緒。顯然執行緒個數大於系統能維護的最大arena個數(2*核心數 + 1= 3),那麼此時glibc malloc就需要確保這4個執行緒能夠正確地共享這3arena,那麼它是如何實現的呢?


當主執行緒首次呼叫malloc的時候,glibc malloc會直接為它分配一個main arena,而不需要任何附加條件。


當使用者執行緒1和使用者執行緒2首次呼叫malloc的時候,glibc malloc會分別為每個使用者執行緒建立一個新的thread arena。此時,各個執行緒與arena是一一對應的。但是,當使用者執行緒3呼叫malloc的時候,就出現問題了。因為此時glibc malloc能維護的arena個數已經達到上限,無法再為執行緒3分配新的arena了,那麼就需要重複使用已經分配好的3arena中的一個(main arena, arena 1或者arena 2)。那麼該選擇哪個arena進行重複利用呢?


1)首先,glibc malloc迴圈遍歷所有可用的arenas,在遍歷的過程中,它會嘗試lockarena。如果成功lock(arena當前對應的執行緒並未使用堆記憶體則表示可lock),比如將main arena成功lock住,那麼就將main arena返回給使用者,即表示該arena被執行緒3共享使用。


2)而如果沒能找到可用的arena,那麼就將執行緒3malloc操作阻塞,直到有可用的arena為止。


3)現在,如果執行緒3再次呼叫malloc的話,glibc malloc就會先嚐試使用最近訪問的arena(此時為main arena)。如果此時main arena可用的話,就直接使用,否則就將執行緒3阻塞,直到main arena再次可用為止。

這樣執行緒3與主執行緒就共享main arena了。至於其他更復雜的情況,以此類推。


堆管理介紹


4.1 整體介紹


glibc malloc中針對堆管理,主要涉及到以下3種資料結構:


1. heap_info: Heap Header,因為一個thread arena(注意:不包含main thread可以包含多個heaps,所以為了便於管理,就給每個heap分配一個heap header。那麼在什麼情況下一個thread arena會包含多個heaps?在當前heap不夠用的時候,malloc會通過系統呼叫mmap申請新的堆空間,新的堆空間會被新增到當前thread arena中,便於管理。


typedef struct _heap_info

{

  mstate ar_ptr; /* Arena for this heap. */

  struct _heap_info *prev; /* Previous heap. */

  size_t size;   /* Current size in bytes. */

  size_t mprotect_size; /* Size in bytes that has been mprotected

                           PROT_READ|PROT_WRITE.  */

  /* Make sure the following data is properly aligned, particularly

     that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of

     MALLOC_ALIGNMENT. */

  char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK];

} heap_info;

 

2. malloc_state: Arena Header,每個thread只含有一個Arena HeaderArena Header包含bins的資訊、top chunk以及最後一個remainder chunk(這些概念會在後文詳細介紹):


struct malloc_state

{

  /* Serialize access.  */

  mutex_t mutex;

 

  /* Flags (formerly in max_fast).  */

  int flags;

 

  /* Fastbins */

  mfastbinptr fastbinsY[NFASTBINS];

 

  /* Base of the topmost chunk -- not otherwise kept in a bin */

  mchunkptr top;

 

  /* The remainder from the most recent split of a small request */

  mchunkptr last_remainder;

 

  /* Normal bins packed as described above */

  mchunkptr bins[NBINS * 2 - 2];

 

  /* Bitmap of bins */

  unsigned int binmap[BINMAPSIZE];

 

  /* Linked list */

  struct malloc_state *next;

 

  /* Linked list for free arenas.  */

  struct malloc_state *next_free;

 

  /* Memory allocated from the system in this arena.  */

  INTERNAL_SIZE_T system_mem;

  INTERNAL_SIZE_T max_system_mem;

};

 

3. malloc_chunk: Chunk Header,一個heap被分為多個chunk,至於每個chunk的大小,這是根據使用者的請求決定的,也就是說使用者呼叫malloc(size)傳遞的size引數就是”chunk的大小(這裡給就是加上引號,說明這種表示並不準確,但是為了方便理解就暫時這麼描述了,詳細說明見後文)。每個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;         /* double links -- used only if free. 這兩個指標只在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;

};


可能有很多讀者會疑惑:該結構體裡面並沒有一個類似於data的欄位來表示使用者申請到的堆記憶體空間啊?且該結構體明確含有2size_t型別的成員,4個指標,這不就意味著malloc_chunk的大小是固定的了麼?那它又如何能夠根據使用者的請求分配不同大小的記憶體呢?要想回答清楚這個問題,需要我們完全理解整個glibc malloc的堆記憶體管理機制,同時,本文的主要目的之一就是希冀解釋清楚這個概念,鑑於這部分內容較多,我將在後文的第5章加以詳細介紹。


NOTE:

1. Main thread不含有多個heaps所以也就不含有heap_info結構體。當需要更多堆空間的時候,就通過擴充套件sbrkheap segment來獲取更多的空間,直到它碰到記憶體mapping區域為止。

2. 不同於thread arenamain arenaarena header並不是sbrk heap segment的一部分,而是一個全域性變數!因此它屬於libc.sodata segment


4.2 heap segmentarena關係


首先,通過記憶體分佈圖理清malloc_stateheap_info之間的組織關係。

下圖是隻有一個heap segmentmain arenathread arena的記憶體分佈圖:

4-1 只含一個heap segmentmain arenathread arena


下圖是一個thread arena中含有多個heap segments的情況:

4-2 一個thread arena含有多個heap segments的記憶體分佈圖

 

從上圖可以看出,thread arena只含有一個malloc_state(arena header),卻有兩個heap_info(heap header)。由於兩個heap segments是通過mmap分配的記憶體,兩者在記憶體佈局上並不相鄰而是分屬於不同的記憶體區間,所以為了便於管理,libc malloc將第二個heap_info結構體的prev成員指向了第一個heap_info結構體的起始位置(即ar_ptr成員),而第一個heap_info結構體的ar_ptr成員指向了malloc_state,這樣就構成了一個單連結串列,方便後續管理。


chunk的理解


glibc malloc中將整個堆記憶體空間分成了連續的、大小不一的chunk,即對於堆記憶體管理而言chunk就是最小操作單位。Chunk總共分為4類:1)allocated chunk; 2)free chunk; 3)top chunk; 4)Last remainder chunk。從本質上來說,所有型別的chunk都是記憶體中一塊連續的區域,只是通過該區域中特定位置的某些識別符號加以區分。為了簡便,我們先將這4chunk簡化為2類:allocated chunk以及free chunk,前者表示已經分配給使用者使用的chunk,後者表示未使用的chunk


眾所周知,無論是何種堆記憶體管理器,其完成的核心目的都是能夠高效地分配和回收記憶體塊(chunk)。因此,它需要設計好相關演算法以及相應的資料結構,而資料結構往往是根據演算法的需要加以改變的。既然是演算法,那麼演算法肯定有一個優化改進的過程,所以本文將根據堆記憶體管理器的演變歷程,逐步介紹在glibc mallocchunk這種資料結構是如何設計出來的,以及這樣設計的優缺點。


PS:鑑於時間和精力有限,後文介紹的演變歷程並沒有加以嚴格考證,筆者只是按照一些參考書籍、自己的理解以及便於文章內容安排做出的善意的捏造,如有錯誤,歡迎大家斧正!

 

5.1 隱式連結串列技術


前文說過,任何堆記憶體管理器都是以chunk為單位進行堆記憶體管理的,而這就需要一些資料結構來標誌各個塊的邊界,以及區分已分配塊和空閒塊。大多數堆記憶體管理器都將這些邊界資訊作為chunk的一部分嵌入到chunk內部,典型的設計如下所示:


5-1 簡單的allocated chunk格式

5-2 簡單的free chunk格式


堆記憶體中要求每個chunk的大小必須為8的整數倍,因此chunk size的後3位是無效的,為了充分利用記憶體,堆管理器將這3個位元位用作chunk的標誌位,典型的就是將第0位元位用於標記該chunk是否已經被分配。這樣的設計很巧妙,因為我們只要獲取了一個指向chunk size的指標,就能知道該chunk的大小,即確定了此chunk的邊界,且利用chunk size的第0位元位還能知道該chunk是否已經分配,這樣就成功地將各個chunk區分開來。注意在allocated chunkpadding部分主要是用於地址對齊的(也可用於對付外部碎片),即讓整個chunk的大小為8的整數倍。


通過上面的設計,我們就能將整個堆記憶體組織成一個連續的已分配或未分配chunk序列:

5-3 簡單的chunk序列


上面的這種結構就叫做隱式連結串列。該連結串列隱式地由每個chunksize欄位連結起來,在進行分配操作的時候,堆記憶體管理器可以通過遍歷整個堆記憶體的chunk,分析每個chunksize欄位,進而找到合適的chunk

 

細心的讀者可能發現:這種隱式連結串列效率其實是相當低的,特別是在記憶體回收方面,它難以進行相鄰多個free chunk的合併操作。我們知道,如果只對free chunk進行分割,而不進行合併的話,就會產生大量小的、無法繼續使用的內部碎片,直至整個記憶體消耗殆盡。因此堆記憶體管理器設計了帶邊界標記的chunk合併技術。


帶邊界標記的合併技術


試想如下場景:假設我們要釋放的chunkP,它緊鄰的前一個chunkFD,緊鄰的後一個chunkBK,且BKFD都為free chunk。將PBK合併在一起是很容易的,因為可以通過Psize欄位輕鬆定位到BK的開始位置,進而獲取BKsize等等,但是將PFD合併卻很難,我們必須從頭遍歷整個堆,找到FD,然後加以合併,這就意味著每次進行chunk釋放操作消耗的時間與堆的大小成線性關係。為了解決這個問題,Knuth提出了一種聰明而通用的技術——邊界標記。


Knuth在每個chunk的最後新增了一個腳部(Footer),它就是該chunk 頭部(header)的一個副本,我們稱之為邊界標記:

5-4 改進版的chunk格式之Knuth邊界標記


顯然每個chunk的腳部都在其相鄰的下一個chunk的頭部的前4個位元組處。通過這個腳部,堆記憶體管理器就可以很容易地得到前一個chunk的起始位置和分配狀態,進而加以合併了。


但是,邊界標記同時帶來了一個問題:它要求每個塊都包含一個頭部和腳部,如果應用程式頻繁地進行小記憶體的申請和釋放操作的話(比如12個位元組),就會造成很大的效能損耗。同時,考慮到只有在對free chunk進行合併的時候才需要腳部,也就是說對於allocated chunk而言它並不需要腳部,因此我們可以對這個腳部加以優化——將前一個chunk的已分配/空閒標記位儲存在當前chunksize欄位的第1,或2位元位上,這樣如果我們通過當前chunksize欄位知道了前一個chunkfree chunk,那麼就可得出結論:當前chunk地址之前的4個位元組為前一個free chunk的腳部,我們可以通過該腳部獲取前一個chunk的起始位置;如果當前chunksize欄位的標記位表明前一個chunkallocated chunk的話,那麼就可得出另一個結論:前一個chunk沒有腳部,即當前chunk地址之前的4個位元組為前一個allocated chunkpayloadpadding的最後部分。新的chunk格式圖如下:

5-5 改進版的Knuth邊界標記allocated chunk格式

 

5-6 改進版的Knuth邊界標記free chunk格式

 

再進化——支援多執行緒


隨著技術的發展,特別是堆記憶體管理器新增對多執行緒的支援,前述的chunk格式已經難以滿足需求,比如,我們需要標誌位來標記當前chunk是否屬於非主執行緒即thread arena,以及該chunkmmap得來還是通過brk實現等等。但此時chunk size只剩下一個位元位未使用了,怎麼辦呢?這需要對chunk格式進行大手術!


首先思考:是否有必要同時儲存當前chunk前一個chunk的已分配/空閒標記位?答案是否定的,因為我們只需要儲存前一個chunk的分配標誌位就可以了,至於當前chunk的分配標誌位,可以通過查詢下一個chunksize欄位得到。那麼size欄位中剩下的兩個位元位就可以用於滿足多執行緒的標誌需求了:

5-7 多執行緒版本Knuth邊界標記allocated chunk格式

       

5-8 多執行緒版本Knuth邊界標記free chunk格式


這裡的P,M,N的含義如下:

PREV_INUSE(P): 表示前一個chunk是否為allocated

IS_MMAPPED(M):表示當前chunk是否是通過mmap系統呼叫產生的。

NON_MAIN_ARENA(N):表示當前chunk是否是thread arena


再進一步,發現沒必要儲存chunk size的副本,也就是說Footer的作用並不大,但是如果前一個chunkfree的話,在合併的時候我們又需要知道前一個chunk的大小,怎麼辦呢?將Footer從尾部移到首部,同時其不再儲存當前chunksize,而是前一個free chunksize不就行了。同樣的,為了提高記憶體利用率,如果前一個chunkallocated chunk的話,這個Footer就作為allocated chunkpayloadpadding的一部分,結構圖如下:


5-9 當前glibc malloc allocated chunk格式


5-10 當前glibc malloc free chunk格式


 

至此,glibc malloc堆記憶體管理器中使用的隱式連結串列技術就介紹完畢了。現在我們再回過頭去看malloc_chunk結構體就很好理解了:該結構體通過每個chunkprev_sizesize構成了隱式連結串列,而後續的fd, bk等指標並不是作用於隱式連結串列的,而是用於後文會介紹的用於加快記憶體分配和釋放效率的顯示連結串列bin(還記得bin麼?用於記錄同一型別free chunk的連結串列),並且這些指標跟prev_size一樣只在free chunk中存在。關於顯示連結串列bin的原理比較複雜,讓我們帶著疑惑,暫時略過這部分資訊,等介紹完所有chunk之後再加以詳細介紹。

 

5.2 Top Chunk


當一個chunk處於一個arena的最頂部(即最高記憶體地址處)的時候,就稱之為top chunk。該chunk不屬於任何bin,而是在系統當前的所有free chunk(無論那種bin)都無法滿足使用者請求的記憶體大小的時候,將此chunk當做一個應急消防員,分配給使用者使用。如果top chunk的大小比使用者請求的大小要大的話,就將該top chunk分作兩部分:1)使用者請求的chunk2)剩餘的部分成為新的top chunk。否則,就需要擴充套件heap或分配新的heap——main arena中通過sbrk擴充套件heap,而在thread arena中通過mmap分配新的heap

 

5.3 Last Remainder Chunk


要想理解此chunk就必須先理解glibc malloc中的bin機制。如果你已經看了第二部分文章,那麼下面的原理就很好理解了,否則建議你先閱讀第二部分文章。對於Last remainder chunk,我們主要有兩個問題:1)它是怎麼產生的;2)它的作用是什麼?


先回答第一個問題。還記得第二部分文章中對small binmalloc機制的介紹麼?當使用者請求的是一個small chunk,且該請求無法被small binunsorted bin滿足的時候,就通過binmaps遍歷bin查詢最合適的chunk,如果該chunk有剩餘部分的話,就將該剩餘部分變成一個新的chunk加入到unsorted bin中,另外,再將該新的chunk變成新的last remainder chunk


然後回答第二個問題。此型別的chunk用於提高連續malloc(small chunk)的效率,主要是提高記憶體分配的區域性性。那麼具體是怎麼提高區域性性的呢?舉例說明。當使用者請求一個small chunk,且該請求無法被small bin滿足,那麼就轉而交由unsorted bin處理。同時,假設當前unsorted bin中只有一個chunk的話——就是last remainder chunk,那麼就將該chunk分成兩部分:前者分配給使用者,剩下的部分放到unsorted bin中,併成為新的last remainder chunk。這樣就保證了連續malloc(small chunk)中,各個small chunk在記憶體分佈中是相鄰的,即提高了記憶體分配的區域性性。


 Linux堆記憶體管理深入分析

(下半部)

作者@走位,阿里聚安全

0 前言回顧

在上一篇文章中(連結見文章底部),詳細介紹了堆記憶體管理中涉及到的基本概念以及相互關係,同時也著重介紹了堆中chunk分配和釋放策略中使用到的隱式連結串列技術。通過前面的介紹,我們知道使用隱式連結串列來管理記憶體chunk總會涉及到記憶體的遍歷,效率極低。對此glibc malloc引入了顯示連結串列技術來提高堆記憶體分配和釋放的效率。

所謂的顯示連結串列就是我們在資料結構中常用的連結串列,而連結串列本質上就是將一些屬性相同的“結點”串聯起來,方便管理。在glibc malloc中這些連結串列統稱為bin,連結串列中的“結點”就是各個chunk,結點的共同屬性就是:1)均為free chunk;2)同一個連結串列中各個chunk的大小相等(有一個特例,詳情見後文)。

 

1 bin介紹

如前文所述,bin是一種記錄free chunk的連結串列資料結構。系統針對不同大小的free chunk,將bin分為了4類:1) Fast bin; 2) Unsorted bin; 3) Small bin; 4) Large bin。

 

在glibc中用於記錄bin的資料結構有兩種,分別如下所示:

fastbinsY: 這是一個陣列,用於記錄所有的fast bins;

bins: 這也是一個陣列,用於記錄除fast bins之外的所有bins。事實上,一共有126個bins,分別是:

bin 1 為unsorted bin;

bin 2 到63為small bin;

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。

 

2 Fast bin

 

既然有fast bin,那就肯定有fast chunk——chunk size為1680位元組的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示意圖

 

3 Unsorted 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中。這就是前言說的特例了,不過特例並非僅僅這一個,後文會介紹。


4 Small 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中。

 

5 Large 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的處理邏輯了:

6 總結

至此glibc malloc中涉及到的所有顯示連結串列技術已經介紹完畢。鑑於篇幅和精力有限,本文沒能詳細介紹完所有的技術細節,但是我相信帶著這些知識點再去研究glibc malloc的話,定能起到事半功倍的效果。

另外,就我個人所瞭解到的基於堆溢位攻擊而言,掌握以上知識,已經足夠理解絕大部分堆溢位攻擊技術了。因此,後面的文章將會結合這些知識詳細介紹各個攻擊技術的實現原理。


相關文章