Linux堆內存管理深刻分析(上)

Linux堆內存管理深刻分析
(上半部)linux

做者:走位@阿里聚安全git

  

0 前言

近年來,漏洞挖掘愈來愈火,各類漏洞挖掘、利用的分析文章層出不窮。從大方向來看,主要有基於棧溢出的漏洞利用和基於堆溢出的漏洞利用兩種。國內關於棧溢出的資料相對較多,這裏就不累述了,可是關於堆溢出的漏洞利用資料就不多了。鄙人覺得主要是堆溢出漏洞的門檻較高,須要先吃透相應操做系統的堆內存管理機制,而這部份內容一直是一個難點。所以本系列文章主要從Linux系統堆內存管理機制出發,逐步介紹諸如基本堆溢出漏洞、基於unlink的堆溢出漏洞利用、double free、use-after-free等等常見的堆溢出漏洞利用技術。github

 

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

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/glibcapp

 

1 堆內存管理簡介

 

當前針對各大平臺主要有以下幾種堆內存管理機制:wordpress

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系統內存分佈圖

 

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以鏈表的形式加以組織)。因爲132KB比1000字節大不少,因此主線程後續再聲請堆空間的話,就會先從這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 arenas的bin(這是一種用於存儲同類型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以後:從輸出結果能夠看出thread1的heap segment已經分配完畢了,同時從這個區域的起始地址能夠看出,它並非經過brk分配的,而是經過mmap分配,由於它的區域爲b7500000-b7600000共1MB,並非同程序的data segment相鄰。同時,咱們還能看出在這1MB中,根據內存屬性分爲了2部分:0xb7500000-0xb7520000共132KB大小的空間是可讀可寫屬性;後面的是不可讀寫屬性。原來,這裏只有可讀寫的132KB空間纔是thread1的堆空間,即thread1 arena。

 

6. 在thread1調用free以後:同main thread。

 

3 Arena介紹

3.1 Arena數量限制

在第2章中咱們提到main thread和thread1有本身獨立的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個線程可以正確地共享這3個arena,那麼它是如何實現的呢?

當主線程首次調用malloc的時候,glibc malloc會直接爲它分配一個main arena,而不須要任何附加條件。

當用戶線程1和用戶線程2首次調用malloc的時候,glibc malloc會分別爲每一個用戶線程建立一個新的thread arena。此時,各個線程與arena是一一對應的。可是,當用戶線程3調用malloc的時候,就出現問題了。由於此時glibc malloc能維護的arena個數已經達到上限,沒法再爲線程3分配新的arena了,那麼就須要重複使用已經分配好的3個arena中的一個(main arena, arena 1或者arena 2)。那麼該選擇哪一個arena進行重複利用呢?

 

1)首先,glibc malloc循環遍歷全部可用的arenas,在遍歷的過程當中,它會嘗試lock該arena。若是成功lock(該arena當前對應的線程並未使用堆內存則表示可lock),好比將main arena成功lock住,那麼就將main arena返回給用戶,即表示該arena被線程3共享使用。

2)而若是沒能找到可用的arena,那麼就將線程3的malloc操做阻塞,直到有可用的arena爲止。

3)如今,若是線程3再次調用malloc的話,glibc malloc就會先嚐試使用最近訪問的arena(此時爲main arena)。若是此時main arena可用的話,就直接使用,不然就將線程3阻塞,直到main arena再次可用爲止。

這樣線程3與主線程就共享main arena了。至於其餘更復雜的狀況,以此類推。

 

4 堆管理介紹

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 Header。Arena 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的字段來表示用戶申請到的堆內存空間啊?且該結構體明確含有2個size_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_state與heap_info之間的組織關係。

下圖是隻有一個heap segment的main arena和thread arena的內存分佈圖:

圖4-1 只含一個heap segment的main arena與thread 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,這樣就構成了一個單鏈表,方便後續管理。

 

5 對chunk的理解

在glibc malloc中將整個堆內存空間分紅了連續的、大小不一的chunk,即對於堆內存管理而言chunk就是最小操做單位。Chunk總共分爲4類:1)allocated chunk; 2)free chunk; 3)top chunk; 4)Last remainder chunk。從本質上來講,全部類型的chunk都是內存中一塊連續的區域,只是經過該區域中特定位置的某些標識符加以區分。爲了簡便,咱們先將這4類chunk簡化爲2類:allocated chunk以及free chunk,前者表示已經分配給用戶使用的chunk,後者表示未使用的chunk。

 

衆所周知,不管是何種堆內存管理器,其完成的核心目的都是可以高效地分配和回收內存塊(chunk)。所以,它須要設計好相關算法以及相應的數據結構,而數據結構每每是根據算法的須要加以改變的。既然是算法,那麼算法確定有一個優化改進的過程,因此本文將根據堆內存管理器的演變歷程,逐步介紹在glibc malloc中chunk這種數據結構是如何設計出來的,以及這樣設計的優缺點。

 

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 chunk中padding部分主要是用於地址對齊的(也可用於對付外部碎片),即讓整個chunk的大小爲8的整數倍。

 

經過上面的設計,咱們就能將整個堆內存組織成一個連續的已分配或未分配chunk序列:

圖5-3 簡單的chunk序列

 

上面的這種結構就叫作隱式鏈表。該鏈表隱式地由每一個chunk的size字段連接起來,在進行分配操做的時候,堆內存管理器能夠經過遍歷整個堆內存的chunk,分析每一個chunk的size字段,進而找到合適的chunk。

 

細心的讀者可能發現:這種隱式鏈表效率實際上是至關低的,特別是在內存回收方面,它難以進行相鄰多個free chunk的合併操做。咱們知道,若是隻對free chunk進行分割,而不進行合併的話,就會產生大量小的、沒法繼續使用的內部碎片,直至整個內存消耗殆盡。所以堆內存管理器設計了帶邊界標記的chunk合併技術。

 

帶邊界標記的合併技術

試想以下場景:假設咱們要釋放的chunk爲P,它緊鄰的前一個chunk爲FD,緊鄰的後一個chunk爲BK,且BK與FD都爲free chunk。將P於BK合併在一塊兒是很容易的,由於能夠經過P的size字段輕鬆定位到BK的開始位置,進而獲取BK的size等等,可是將P於FD合併卻很難,咱們必須從頭遍歷整個堆,找到FD,而後加以合併,這就意味着每次進行chunk釋放操做消耗的時間與堆的大小成線性關係。爲了解決這個問題,Knuth提出了一種聰明而通用的技術——邊界標記。

 

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

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

 

顯然每一個chunk的腳部都在其相鄰的下一個chunk的頭部的前4個字節處。經過這個腳部,堆內存管理器就能夠很容易地獲得前一個chunk的起始位置和分配狀態,進而加以合併了。

 

可是,邊界標記同時帶來了一個問題:它要求每一個塊都包含一個頭部和腳部,若是應用程序頻繁地進行小內存的申請和釋放操做的話(好比1,2個字節),就會形成很大的性能損耗。同時,考慮到只有在對free chunk進行合併的時候才須要腳部,也就是說對於allocated chunk而言它並不須要腳部,所以咱們能夠對這個腳部加以優化——將前一個chunk的已分配/空閒標記位存儲在當前chunk的size字段的第1,或2比特位上,這樣若是咱們經過當前chunk的size字段知道了前一個chunk爲free chunk,那麼就可得出結論:當前chunk地址以前的4個字節爲前一個free chunk的腳部,咱們能夠經過該腳部獲取前一個chunk的起始位置;若是當前chunk的size字段的標記位代表前一個chunk是allocated chunk的話,那麼就可得出另外一個結論:前一個chunk沒有腳部,即當前chunk地址以前的4個字節爲前一個allocated chunk的payload或padding的最後部分。新的chunk格式圖以下:

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

 

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

 

再進化——支持多線程

隨着技術的發展,特別是堆內存管理器添加對多線程的支持,前述的chunk格式已經難以知足需求,好比,咱們須要標誌位來標記當前chunk是否屬於非主線程即thread arena,以及該chunk由mmap得來仍是經過brk實現等等。但此時chunk size只剩下一個比特位未使用了,怎麼辦呢?這須要對chunk格式進行大手術!

 

首先思考:是否有必要同時保存當前chunk和前一個chunk的已分配/空閒標記位?答案是否認的,由於咱們只須要保存前一個chunk的分配標誌位就能夠了,至於當前chunk的分配標誌位,能夠經過查詢下一個chunk的size字段獲得。那麼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的做用並不大,可是若是前一個chunk是free的話,在合併的時候咱們又須要知道前一個chunk的大小,怎麼辦呢?將Footer從尾部移到首部,同時其再也不保存當前chunk的size,而是前一個free chunk的size不就好了。一樣的,爲了提升內存利用率,若是前一個chunk是allocated chunk的話,這個Footer就做爲allocated chunk的payload或padding的一部分,結構圖以下:

 

圖5-9 當前glibc malloc allocated chunk格式

 

圖5-10 當前glibc malloc free chunk格式

 

至此,glibc malloc堆內存管理器中使用的隱式鏈表技術就介紹完畢了。如今咱們再回過頭去看malloc_chunk結構體就很好理解了:該結構體經過每一個chunk的prev_size和size構成了隱式鏈表,然後續的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)用戶請求的chunk;2)剩餘的部分紅爲新的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 bin的malloc機制的介紹麼?當用戶請求的是一個small chunk,且該請求沒法被small bin、unsorted 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在內存分佈中是相鄰的,即提升了內存分配的局部性。

 

做者:走位@阿里聚安全,更多技術文章,請點擊阿里聚安全博客

相關文章
相關標籤/搜索