Linux堆的一些基礎知識


堆的概述

什么是堆

堆用來在程序運行時動態的分配內存,對其實就是虛擬空間里從地址向高地址增長的連續的線性區域。

堆的基本操作

  • void *malloc(unsigned int size):作用是在內存的動態存儲區中分配一個長度為size的連續空間。此函數的返回值是分配區域的起始地址,或者說,此函數是一個指針型函數,返回的指針指向該分配域的開頭位置。
  • void free(void *ptr):釋放之前調用 calloc、malloc 或 realloc 所分配的內存空間。

堆操作背后的系統調用

  • brk():將數據段(.data)的最高地址指針_edata往高地址推。(從堆頭開始,參數為地址)
  • mmap():在進程的虛擬地址空間中(堆和棧中間,稱為文件映射區域的地方)找一塊空閑的虛擬內存。(分配大於128k)
  • sbrk():將地址指針往高地址推。(從當前指針位置開始,參數為指針增量)
  • mummap():刪除地址空間。

堆的相關數據結構

微觀結構

malloc_chuck

申請的內存chunk在ptmalloc內部用malloc_chunk結構體表示。

malloc_chunk的一些字段

  • prev_size:如果該 chunk 的物理相鄰的前一地址chunk(兩個指針的地址差值為前一chunk大小)是空閑的話,那該字段記錄的是前一個 chunk 的大小(包括 chunk 頭)。否則,該字段可以用來存儲物理相鄰的前一個chunk 的數據。這里的前一 chunk 指的是較低地址的 chunk。
  • size:該 chunk 的大小,大小必須是 2 * SIZE_SZ 的整數倍。如果申請的內存大小不是 2 * SIZE_SZ 的整數倍,會被轉換滿足大小的最小的 2 * SIZE_SZ 的倍數。32 位系統中,SIZE_SZ 是 4;64 位系統中,SIZE_SZ 是 8。 該字段的低三個比特位對 chunk 的大小沒有影響,它們從高到低分別表示:
    • NON_MAIN_ARENA:記錄當前 chunk 是否不屬於主線程,1表示不屬於,0表示屬於。
    • IS_MAPPED:記錄當前 chunk 是否是由 mmap 分配的。
    • PREV_INUSE:記錄前一個 chunk 塊是否被分配。一般來說,堆中第一個被分配的內存塊的 size 字段的P位都會被設置為1,以便於防止訪問前面的非法內存。當一個 chunk 的 size 的 P 位為 0 時,我們能通過 prev_size 字段來獲取上一個 chunk 的大小以及地址。這也方便進行空閑chunk之間的合並。
  • fd,bk:chunk 處於分配狀態時,從 fd 字段開始是用戶的數據。chunk 空閑時,會被添加到對應的空閑管理鏈表中,其字段的含義如下:
    • fd:指向下一個(非物理相鄰)空閑的 chunk。
    • bk:指向上一個(非物理相鄰)空閑的 chunk。
    • 通過 fd 和 bk 可以將空閑的 chunk 塊加入到空閑的 chunk 塊鏈表進行統一管理。
  • fd_nextsize, bk_nextsize:也是只有 chunk 空閑的時候才使用,不過其用於較大的 chunk(large chunk)。
    • fd_nextsize:指向前一個與當前 chunk 大小不同的第一個空閑塊,不包含 bin 的頭指針。(指向比它大的空閑塊)
    • bk_nextsize:指向后一個與當前 chunk 大小不同的第一個空閑塊,不包含 bin 的頭指針。(指向比它小的空閑塊)
    • 一般空閑的 large chunk 在 fd 的遍歷順序中,按照由大到小的順序排列。這樣做可以避免在尋找合適chunk 時挨個遍歷。

chunk相關宏

  • chunk與mem指針頭部的轉換
    • define chunk2mem(p) ((void *) ((char *) (p) + 2 * SIZE_SZ))
    • define mem2chunk(mem) ((mchunkptr)((char *) (mem) -2 * SIZE_SZ))
  • 最小的chunk大小
    • define MIN_CHUNK_SIZE (offsetof(struct malloc_chunk, fd_nextsize))(offsetof 函數計算出 fd_nextsize 在 malloc_chunk 中的偏移,說明最小的 chunk 至少要包含 bk 指針。)
  • define MINSIZE (unsigned long) (((MIN_CHUNK_SIZE + MALLOC_ALIGN_MASK) &~MALLOC_ALIGN_MASK))(滿足SIZE_SZ的最小上界)
  • 檢查分配給用戶的內存是否對齊
    • define aligned_OK(m) (((unsigned long) (m) & MALLOC_ALIGN_MASK) == 0)
    • define misaligned_chunk(p) ((uintptr_t)(MALLOC_ALIGNMENT == 2 * SIZE_SZ ? (p) : chunk2mem(p)) & MALLOC_ALIGN_MASK)
  • 請求字節數判斷
    • define REQUEST_OUT_OF_RANGE(req) ((unsigned long) (req) >= (unsigned long) (INTERNAL_SIZE_T)(-2 * MINSIZE))
  • 將用戶請求內存大小轉為實際分配內存大小
    • define request2size(req) (((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) ? MINSIZE : ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
    • define checked_request2size(req, sz) if (REQUEST_OUT_OF_RANGE(req)) { __set_errno(ENOMEM); return 0;} (sz) = request2size(req);
  • 標記位相關
    • define PREV_INUSE 0x1
    • define prev_inuse(p) ((p)->mchunk_size & PREV_INUSE)
    • size field is or'ed with IS_MMAPPED if the chunk was obtained with mmap()
    • define chunk_is_mmapped(p) ((p)->mchunk_size & IS_MMAPPED)
    • define NON_MAIN_ARENA 0x4
    • define SIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)+
  • 獲取chunk size
    • define chunksize(p) (chunksize_nomask(p) & ~(SIZE_BITS))
    • define chunksize_nomask(p) ((p)->mchunk_size)
  • 獲取下一個物理相鄰的chunk
    • define next_chunk(p) ((mchunkptr)(((char *) (p)) + chunksize(p)))
  • 獲取前一個chunk的信息
    • define prev_size(p) ((p)->mchunk_prev_size)
    • define prev_chunk(p) ((mchunkptr)(((char *) (p)) - prev_size(p)))
  • 當前chunk使用狀態相關操作
    • define inuse(p)((((mchunkptr)(((char *) (p)) + chunksize(p)))->mchunk_size) & PREV_INUSE)
    • define set_inuse(p)((mchunkptr)(((char *) (p)) + chunksize(p)))->mchunk_size |= PREV_INUSE
    • define clear_inuse(p)((mchunkptr)(((char *) (p)) + chunksize(p)))->mchunk_size &= ~(PREV_INUSE)
  • 設置chunk的size字段
    • define set_head_size(p, s)((p)->mchunk_size = (((p)->mchunk_size & SIZE_BITS) | (s)))
    • define set_head(p, s) ((p)->mchunk_size = (s))
    • define set_foot(p, s) (((mchunkptr)((char *) (p) + (s)))->mchunk_prev_size = (s))
  • 獲取指定偏移的chunk
    • define chunk_at_offset(p, s) ((mchunkptr)(((char *) (p)) + (s)))
  • 指定偏移處chunk使用狀態相關操作
    • define inuse_bit_at_offset(p, s)(((mchunkptr)(((char *) (p)) + (s)))->mchunk_size & PREV_INUSE)
    • define set_inuse_bit_at_offset(p, s)(((mchunkptr)(((char *) (p)) + (s)))->mchunk_size |= PREV_INUSE)
    • define clear_inuse_bit_at_offset(p, s)(((mchunkptr)(((char *) (p)) + (s)))->mchunk_size &= ~(PREV_INUSE))

bin

根據空閑的 chunk 的大小以及使用狀態將 chunk 初步分為4類:fast bins,small bins,large bins,unsorted bin

數組中的bin

  • unsorted bin:這里面的chunk沒有進行排序,存儲的chunk比較雜。
  • small bin:索引從 2 到 63 的 bin,同一個 small bin 鏈表中的 chunk 的大小相同。兩個相鄰索引的 small bin 鏈表中的 chunk 大小相差的字節數為2個機器字長,即32位相差8字節,64位相差16字節。
  • large bins:small bins后面的bin,large bins中的每一個 bin 都包含一定范圍內的chunk,其中的chunk按fd指針的順序從大到小排列。相同大小的chunk同樣按照最近使用順序排列。
  • 上述這些bin的排布都會遵循一個原則:任意兩個物理相鄰的空閑chunk不能在一起。

fastbin

unsorted bin的來源

  • 當一個較大的chunk被分割成兩半后,如果剩下的部分大於MINSIZE,就會被放到unsorted bin中。
  • 釋放一個不屬於fast bin的chunk,並且該chunk不和top chunk緊鄰時,該chunk會被首先放到unsorted bin中。

last remainder

在用戶使用malloc請求分配內存時,ptmalloc2找到的chunk可能並不和申請的內存大小一致,這時候就將分割之后的剩余部分稱之為last remainder chunk,unsort bin也會存這一塊。top chunk分割剩下的部分不會作為last remainer。

宏觀結構

arena

我們知道一個線程申請的1個/多個堆包含很多的信息:二進制位信息,多個malloc_chunk信息等這些堆需要東西來進行管理,那么Arena就是來管理線程中的這些堆的。

heap_info

程序剛開始執行時,每個線程是沒有heap區域的。當其申請內存時,就需要一個結構來記錄對應的信息,而heap_info的作用就是這個。而且當該heap的資源被使用完后,就必須得再次申請內存了。此外,一般申請的heap是不連續的,因此需要記錄不同heap之間的鏈接結構。

該數據結構是專門為從Memory Mapping Segment處申請的內存准備的,即為非主線程准備的。
主線程可以通過sbrk()函數擴展program break location獲得(直到觸及Memory Mapping Segment),只有一個heap,沒有heap_info數據結構。

typedef struct _heap_info
{
  mstate ar_ptr; /* 堆對應的 arena 的地址 */
  struct _heap_info *prev; /* 由於一個線程申請一個堆之后,可能會使用完,之后就必須得再次申請。因此,一個可能會有多個堆。prev即記錄了上一個 heap_info 的地址。這里可以看到每個堆的 heap_info 是通過單向鏈表進行鏈接的 */
  size_t size;   /* size 表示當前堆的大小 */
  size_t mprotect_size; /* 最后一部分確保對齊  */
  /* 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;

malloc_state

該結構用於管理堆,記錄每個arena當前申請的內存的具體狀態,比如說是否有空閑chunk,有什么大小的空閑chunk等等。無論是thread arena還是main arena,它們都只有一個malloc state結構。由於thread的arena可能有多個,malloc state結構會在最新申請的arena中。注意,main arena的malloc_state並不是 heap segment的一部分,而是一個全局變量,存儲在libc.so的數據段。
struct malloc_state {
    /* 該變量用於控制程序串行訪問同一個分配區,當一個線程獲取了分配區之后,其它線程要想訪問該分配區,就必須等待該線程分配完成候才能夠使用。  */
    __libc_lock_define(, mutex);

    /* flags記錄了分配區的一些標志,比如 bit0 記錄了分配區是否有 fast bin chunk ,bit1 標識分配區是否能返回連續的虛擬地址空間。  */
    int flags;

    /* 存放每個 fast chunk 鏈表頭部的指針 */
    mfastbinptr fastbinsY[ NFASTBINS ];

    /* 指向分配區的 top chunk */
    mchunkptr top;

    /* 最新的 chunk 分割之后剩下的那部分 */
    mchunkptr last_remainder;

    /* 用於存儲 unstored bin,small bins 和 large bins 的 chunk 鏈表。 */
    mchunkptr bins[ NBINS * 2 - 2 ];

    /* ptmalloc 用一個 bit 來標識某一個 bin 中是否包含空閑 chun..*/
    unsigned int binmap[ BINMAPSIZE ];

    /* Linked list, points to the next arena */
    struct malloc_state *next;

    /* Linked list for free arenas.  Access to this field is serialized
       by free_list_lock in arena.c.  */
    struct malloc_state *next_free;

    /* Number of threads attached to this arena.  0 if the arena is on
       the free list.  Access to this field is serialized by
       free_list_lock in arena.c.  */
    INTERNAL_SIZE_T attached_threads;

    /* Memory allocated from the system in this arena.  */
    INTERNAL_SIZE_T system_mem;
    INTERNAL_SIZE_T max_system_mem;
};

深入了解堆實現

堆初始化

malloc_consolidate()

函數實現步驟
1、若 get_max_fast() 返回 0,則進行堆的初始化工作,然后進入第 7 步。
2、從 fastbin 中獲取一個空閑 chunk。
3、嘗試向后合並。
4、若向前相鄰 top_chunk,則直接合並到 top_chunk,然后進入第 6 步。
5、否則嘗試向前合並后,插入到 unsorted_bin 中。
6、獲取下一個空閑 chunk,回到第 2 步,直到所有 fastbin 清空后進入第 7 步。
7、退出函數。

創建堆

unlink 用來將一個雙向鏈表(只存儲空閑的 chunk)中的一個元素取出來,可能在以下地方使用
  • malloc
    • 從恰好大小合適的 large bin 中獲取 chunk
    • 從比請求的 chunk 所在的 bin 大的 bin 中取 chunk
  • Free
    • 后向合並,合並物理相鄰低地址空閑 chunk
    • 前向合並,合並物理相鄰高地址空閑 chunk(除了 top chunk)
  • malloc_consolidate
    • 后向合並,合並物理相鄰低地址空閑 chunk
    • 前向合並,合並物理相鄰高地址空閑 chunk(除了 top chunk)
  • realloc
    • 前向擴展,合並物理相鄰高地址空閑 chunk(除了top chunk)
      在unlink后,拖鏈的p的fd跟bk的指針都沒有變化,我們可以利用這個泄露地址。
  • libc 地址
    • P 位於雙向鏈表頭部,bk 泄漏
    • P 位於雙向鏈表尾部,fd 泄漏
    • 雙向鏈表只包含一個空閑 chunk 時,P 位於雙向鏈表中,fd 和 bk 均可以泄漏
  • 泄漏堆地址,雙向鏈表包含多個空閑 chunk
    • P 位於雙向鏈表頭部,fd 泄漏
    • P 位於雙向鏈表中,fd 和 bk 均可以泄漏
    • P 位於雙向鏈表尾部,bk 泄漏

申請內存塊

_libc_malloc

函數實現步驟
1、該函數會首先檢查是否有內存分配函數的鈎子函數(__malloc_hook)
2、接着會尋找一個 arena 來試圖分配內存
3、然后調用 _int_malloc 函數去申請對應的內存
4、如果分配失敗的話,ptmalloc 會嘗試再去尋找一個可用的 arena,並分配內存
5、如果申請到了 arena,那么在退出之前還得解鎖。
6、判斷目前的狀態是否滿足以下條件
+ 要么沒有申請到內存
+ 要么是 mmap 的內存
+ 要么申請到的內存必須在其所分配的arena中
7、最后返回內存

_int_malloc

1、它根據用戶申請的內存塊大小以及相應大小 chunk 通常使用的頻度(fastbin chunk, small chunk, large chunk),依次實現了不同的分配方法
2、它由小到大依次檢查不同的 bin 中是否有相應的空閑塊可以滿足用戶請求的內存
3、當所有的空閑 chunk 都無法滿足時,它會考慮 top chunk
4、當 top chunk 也無法滿足時,堆分配器才會進行內存塊申請

chunk不同范圍申請實現

  • fastbin

    • 得到對應的fastbin的下標
    • 得到對應的fastbin的頭指針
    • 利用fd遍歷對應的bin內是否有空閑的chunk塊
    • 檢查取到的 chunk 大小是否與相應的 fastbin 索引一致
    • 根據取得的 victim ,利用 chunksize 計算其大小
    • 利用fastbin_index 計算 chunk 的索引
    • 將獲取的到chunk轉換為mem模式
    • 如果設置了perturb_type, 則將獲取到的chunk初始化為 perturb_type ^ 0xff
  • small bin

    • 獲取 small bin 的索引
    • 獲取對應 small bin 中的 chunk 指針
    • 先執行 victim = last(bin),獲取 small bin 的最后一個 chunk
    • 如果 victim = bin ,那說明該 bin 為空
    • 如果不相等,那么會有兩種情況
      • 第一種情況,small bin 還沒有初始化
        • 執行初始化,將 fast bins 中的 chunk 進行合並
      • 第二種情況,small bin 中存在空閑的 chunk
        • 獲取 small bin 中倒數第二個 chunk
        • 檢查 bck->fd 是不是 victim,防止偽造
        • 設置 victim 對應的 inuse 位
        • 修改 small bin 鏈表,將 small bin 的最后一個 chunk 取出
        • 如果不是 main_arena,設置對應的標志
        • 將申請到的 chunk 轉化為對應的 mem 狀態
        • 如果設置了 perturb_type , 則將獲取到的chunk初始化為 perturb_type ^ 0xff
  • large bin

    • 獲取large bin的下標
    • 如果存在fastbin的話,會處理 fastbin
  • 大循環
    如果程序執行到了這里,那么說明 與 chunk 大小正好一致的 bin (fast bin, small bin) 中沒有 chunk可以直接滿足需求 ,但是large chunk 則是在這個大循環中處理

    • 嘗試從 unsorted bin 中分配用戶所需的內存
      • unsort bin 遍歷
      • small request
      • 初始取出
      • exact fit
      • place chunk in small bin
      • place chunk in large bin
      • 最終取出
      • while 最多迭代10000次
    • 嘗試從 large bin 中分配用戶所需的內存
    • 尋找較大 chunk
      • 找到一個合適的 map
      • 找到合適的 bin
      • 簡單檢查 chunk
      • 真正取出chunk
    • 嘗試從 top chunk 中分配用戶所需內存

釋放內存塊

_libc_free

  • 判斷是否有鈎子函數 __free_hook
  • free NULL沒有作用
  • 將mem轉換為chunk狀態
  • 如果該塊內存是mmap得到的
  • 根據chunk獲得分配區的指針
  • 執行釋放

內容來源

Linux進程分配內存的兩種方式brk和mmap
sbrk與brk的使用
ctfwiki概述
堆漏洞挖掘:02---堆的glibc實現與Arena
ctfwiki堆相關數據結構
淺談 malloc_consolidate() 函數具體實現
ctfwiki深入理解堆實現


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM