1. 基礎知識
1.1 Linux 程序記憶體佈局
Linux 系統在裝載 elf 格式的程式檔案時,會呼叫 loader
把可執行檔案中的各個段依次載入到從某一地址開始的空間中(載入地址取決於 link editor(ld)
和機器地址位數,在 32 位機器上是 0x8048000,即 128M 處)。以 32 位機器為例,首先被載入的是 .text
段,然後是 .data
段,最後是 .bss
段。上面的空間供核心使用,應用程式不可以直接訪問。應用程式的堆疊從最高地址處開始向下生長,.bss
段與棧之間的空間是空閒的,空閒空間被分成兩部分,一部分為 heap,一部分為 mmap 對映區域。Heap 和 mmap 區域都可以供使用者自由使用,但是它在剛開始的時候並沒有對映到記憶體空間內,是不可訪問的。
在向核心請求分配 heap, mmap 空間之前,對這些空間進行訪問會導致 segmentation fault
。使用者程式可以直接使用系統呼叫來管理 heap 和 mmap 對映區域,但更多的時候程式都是使用 C 語言提供的 malloc()
和 free()
函式來動態的分配和釋放記憶體。
stack 區域是唯一不需要對映,使用者卻可以訪問的記憶體區域,這也是利用堆疊溢位進行攻擊的基礎。
結構圖如下。
棧至頂向下擴充套件,並且棧是有界的。堆至底向上擴充套件,mmap 對映區域至頂向下擴充套件,mmap 對映區域和堆相對擴充套件,直至耗盡虛擬地址空間中的剩餘區域,這種結構便於 C 執行時庫使用 mmap 對映區域和堆進行記憶體分配。
1.2 作業系統記憶體分配函式
heap 和 mmap 對映區域是可以提供給使用者程式使用的虛擬記憶體空間,作業系統提供了相關的系統呼叫來分配該區域的記憶體。對 heap 的操作,作業系統提供了 brk()
函式,C 執行時庫提供了 sbrk()
函式;對 mmap 對映區域的操作,作業系統提供了 mmap()
和 munmap()
函式。Glibc 同樣是使用這些函式向作業系統申請虛擬記憶體。
這裡提一個很重要的概念,記憶體的延遲分配,只有在真正訪問一個地址的時候才建立這個地址的物理對映,這是 Linux 記憶體管理的基本思想之一。Linux 核心在使用者申請記憶體的時候,只是給它分配了一個線性區(也就是虛擬記憶體),並沒有分配實際實體記憶體;只有當使用者真正使用到這塊記憶體的時候,核心才會分配具體的物理頁面給使用者,這時候才佔用寶貴的實體記憶體。核心釋放物理頁面是透過釋放線性區,找到其所對應的物理頁面,將其全部釋放的過程。
1.3 ptmalloc 記憶體分配
首先要知道,當一個 chunk 被鏈入 fast bins 或者 tcache 的時候,它的下一個堆塊的 pre_inuse
位是不會被改變的,依舊為 1,這就意味著在沒有觸發 malloc_consolidate
等記憶體整理的機制時,他們是不會被其它堆塊合併、整理的,當 chunk 被鏈入 unsorted bin 時,下一個堆塊的 pre_inuse
位便會改成 0,在下一次記憶體整理的時候它們就會被鏈入對應的真正的 bins,即 small bins 或者 large bins。
初學者很容易卡在 small bins 和 large bins 的分配過程上,不清楚這些 chunk 怎麼來的,這需要讀完 malloc 和 free 的原始碼才會有比較深的認識。
簡單講一下當使用者正常請求分配記憶體時的大致流程(這裡省略了一些類似判斷分配區、申請系統資源之類的操作,只關注我們應該主要了解的、正常使用下常用的,詳細見 2.1 的 malloc 原始碼分析)。
-
ptmalloc 首先會查詢使用者所需的分配空間是否在 fast bins 範圍內(0x80),如果是的話就去所屬 fast bin 鏈找有沒有合適的空閒 chunk,可以找到就結束分配。
-
檢查是否在 small bins 範圍內(0x400),如果是的話就去所屬 small bin 鏈查詢是否有空閒 chunk,可以找到就在那條鏈的尾部摘出一個 chunk,結束分配。
-
此時說明使用者申請的空間比較大,或者說相應的 bin 鏈中沒有 chunk,ptmalloc 開始遍歷 fast bins 中的 chunk,將相鄰的空閒 chunk 進行合併後鏈入 unsorted bin,然後遍歷 unsorted bin 中的 chunk。
3.1 如果 unsorted bin 只有一個 chunk,並且這個 chunk 在上次分配時被使用過,並且所需分配的 chunk 大小屬於 small bins,且 chunk 的大小大於等於需要分配的大小,這種情況下就直接將該 chunk 進行切割,剩下的部分繼續留在 unsorted bin 裡,分配結束;
3.2 否則比如 unsorted bin 裡有若干 chunk,就會從後往前一直整理這些 chunk,根據 chunk 的空間大小將其放入所屬 small bin 鏈或是 large bin 鏈中,一直整理直到遇到
chunk_size = nb
的 chunk,或者說整理到 bin 鏈為空。寫了個 demo 測試 unsorted bin 鏈裡有多個 chunk 的情況,假如不清楚 small/large bins 的入鏈機制,就很容易感到疑惑,這裡丟擲一個問題:透過除錯我們可以發現最後進行的 malloc,切割的是最後入 unsorted 鏈的 chunk,就是 unsorted bin 的鏈頭,那為什麼不是鏈尾的那個 chunk 呢,畢竟 unsorted bin 是 FIFO 的啊。
實際上,chunk 不是直接在 unsorted bin 裡面被切割的,因為此時 unsorted bin 裡面有不止一個 chunk,如果是隻有一個的話就是直接切割。這裡將所有 unsorted chunk 從後往前先鏈入了 large bin,這倆堆塊大小一致,被鏈入同一條 bin 鏈,高地址的那個堆塊是最後被鏈入的,然後根據 best-fit 原則找到了相應的 bin 鏈後,高地址的那個堆塊被當成了”候選“的,從中切割出 0x220 後,剩下的部分被鏈入 unsorted,這個“候選“詳見 2.1 的原始碼的註釋裡面的 5.1 之前那一段原始碼註釋,如下。
“如果從 large bin 連結串列中選取的 chunk victim 不是連結串列中的最後一個 chunk,並且與 victim 大小相同的chunk不止一個,那麼意味著 victim 為 chunk size 連結串列中的節點,為了不調整 chunk size 連結串列,需要避免將 chunk size 連結串列中的節點取出,所以取 victim->fd 節點對應的 chunk 作為候選 chunk。由於 large bin 連結串列中的 chunk 也是按大小排序,同一大小的 chunk 有多個時,這些 chunk 必定排在一起,所以 victim->fd 節點對應的 chunk 的大小必定與 victim 的大小一樣。”
-
如果到了這一步還是沒能分配出去,說明需要分配的是一塊比較大的記憶體,或者在 small bins 和 unsorted bin 中都找不到合適的 chunk,此時 fast bins 和 unsorted bin 中所有的 chunk 都清理乾淨了,合併了一部分 chunk 進入 small/large bins,那接下來繼續找對應的 bin 鏈子,如果還沒成功,就遍歷 small bins 和 large bins,按照 “smallest-first,best-fit” 原則,找一個合適的 chunk,從中劃分一塊所需大小的 chunk,並將剩下的部分鏈入到 unsorted bin 中。若操作成功,則分配結束。
-
若是還沒能分配成功的話就到
sbrk
,mmap
了。
2. 原始碼分析
主要分析的檔案包括 arena.c
和 malloc.c
,這兩個檔案包括了 ptmalloc
的核心實現,其中 arena.c
主要是對多執行緒支援的實現,malloc.c
定義了公用的 malloc(),free()等函式,實現了基於分配區的記憶體管理演算法。
2.1 記憶體分配 malloc
ptmalloc2 主要的記憶體分配函式為 malloc()
,即原始碼裡的 __libc_malloc()
,它其實是對 _int_malloc()
函式的簡單封裝,_int_malloc()
函式才是記憶體分配的核心。
首先看 __libc_malloc()
。
void *
__libc_malloc(size_t bytes)
{
//首先檢查是否存在記憶體分配的 hook 函式,如果存在,呼叫 hook 函式,並返回,hook 函式主要用於程序在建立新執行緒過程中分配記憶體,或者支援使用者提供的記憶體分配函式。
mstate ar_ptr;
void *victim;
void *(*hook)(size_t, const void *) = atomic_forced_read(__malloc_hook);
if (__builtin_expect(hook != NULL, 0))
return (*hook)(bytes, RETURN_ADDRESS(0));
//獲取分配區指標,如果獲取分配區失敗,返回退出,否則,呼叫 _int_malloc() 函式分配記憶體。
arena_get(ar_ptr, bytes);
victim = _int_malloc(ar_ptr, bytes);
/* Retry with another arena only if we were able to find a usable arena
before. */
//如果 _int_malloc() 函式分配記憶體失敗,就會判斷使用的分配區是不是主分配區,然後是一些獲取分配區,解鎖之類的操作。
if (!victim && ar_ptr != NULL)
{
LIBC_PROBE(memory_malloc_retry, 1, bytes);
ar_ptr = arena_get_retry(ar_ptr, bytes);
victim = _int_malloc(ar_ptr, bytes);
}
if (ar_ptr != NULL)
(void)mutex_unlock(&ar_ptr->mutex);
assert(!victim || chunk_is_mmapped(mem2chunk(victim)) ||
ar_ptr == arena_for_chunk(mem2chunk(victim)));
return victim;
}
ar_ptr
是指向全域性記憶體分配器的指標,說白了就是全域性記憶體分配器狀態機,main_arena
也是 mstate
結構。
atomic_forced_read
是彙編語句,用於原子讀操作,每次只會讀取一次,例如呼叫 malloc_hook_ini
初始化,只會呼叫一次。
__malloc_hook
指向 malloc_hook_ini
,該函式為 ptmalloc 的初始化函式。主要用於初始化全域性狀態機和 chunk 的資料結構,首先來看看 malloc_hook_ini
函式。
/**
* 初始化。
*/
static void *
malloc_hook_ini (size_t sz, const void *caller){
//先將 malloc_hook 的值設定為 NULL,然後呼叫 ptmalloc_init 函式,最後又回撥了 libc_malloc 函式。
__malloc_hook = NULL;
ptmalloc_init ();
return __libc_malloc (sz);
}
總結一下執行流程:
第一次呼叫 malloc 申請堆空間:首先會跟著 hook 指標進入 malloc_hook_ini()
函式里面進行對 ptmalloc 的初始化工作,並置空 hook,再呼叫 ptmalloc_init()
和 __libc_malloc()
;
再次呼叫 malloc 申請堆空間:malloc() -> __libc_malloc() -> _int_malloc()。
然後終於分析到核心函式 _int_malloc() 了。
static void *
_int_malloc(mstate av, size_t bytes)
{
INTERNAL_SIZE_T nb; /* 符合要求的請求大小 */
unsigned int idx; /* 相關的bin指數 */
mbinptr bin; /* 相關的bin */
mchunkptr victim; /* 檢查/選擇的塊 */
INTERNAL_SIZE_T size; /* its size */
int victim_index; /* its bin index */
mchunkptr remainder; /* 被分割的剩餘部分 */
unsigned long remainder_size; /* its size */
unsigned int block; /* bit map traverser */
unsigned int bit; /* bit map traverser */
unsigned int map; /* current word of binmap */
mchunkptr fwd; /* misc temp for linking */
mchunkptr bck; /* misc temp for linking */
const char *errstr = NULL;
/*
Convert request size to internal form by adding SIZE_SZ bytes
overhead plus possibly more to obtain necessary alignment and/or
to obtain a size of at least MINSIZE, the smallest allocatable
size. Also, checked_request2size traps (returning 0) request sizes
that are so large that they wrap around zero when padded and
aligned.
*/
checked_request2size(bytes, nb);
checked_request2size()
函式將請求分配的記憶體大小 bytes 轉換為需要分配下去的 chunk 實際大小 nb。Ptmalloc 內部分配都是以 chunk 為單位,根據 chunk 的大小,決定如何分配滿足條件的 chunk,達到分配最小的 size 的同時符合對齊要求的目的。
如果所需的 chunk 大小小於等於 fast bins 中的最大 chunk 大小,首先嚐試從 fast bins 中 分配 chunk。
先檢查是否屬於 fast bins 範圍內,嘗試分配鏈中的 chunk 出去,原始碼如下。
/*
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.
*/
if ((unsigned long)(nb) <= (unsigned long)(get_max_fast()))
{
//根據所需 chunk 的大小獲得該 chunk 所屬 fast bin 的 index。
idx = fastbin_index(nb);
//從鏈中取出第一個 chunk,並呼叫 chunk2mem() 函式返回使用者所需的記憶體塊。
mfastbinptr *fb = &fastbin(av, idx);
mchunkptr pp = *fb;
do
{
victim = pp;
if (victim == NULL)
break;
} 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), av);
return NULL;
}
check_remalloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
}
然後檢查是否屬於 small bins 範圍內,是的話則會執行如下的程式碼:
/*
If a small request, check regular bin. Since these "smallbins"
hold one size each, no searching within bins is necessary.
(For a large request, we need to wait until unsorted chunks are
processed to find best fit. But for small ones, fits are exact
anyway, so we can check now, which is faster.)
*/
if (in_smallbin_range(nb))
{
idx = smallbin_index(nb);
//根據 index 獲得某個 small bin 的空閒 chunk 雙向迴圈連結串列表頭,在 if 語句裡將最後一個 chunk 賦值給 victim。
bin = bin_at(av, idx);
//如果 victim 與表頭相同,表示該連結串列為空,不能從 small bin 的空閒 chunk 連結串列中分配。
//下面都是 victim 與表頭不相同的情況。
if ((victim = last(bin)) != bin)
{
//如果 victim 為 0,表示所屬 small bin 還沒有初始化為雙向迴圈連結串列,呼叫 malloc_consolidate() 函式將 fast bins 中的 chunk 合併。
if (victim == 0) /* initialization check */
malloc_consolidate(av);
//否則說明有合適的 chunk 在對應的 bin 鏈,將 victim 從 small bin 的雙向迴圈連結串列中取出,設定 victim chunk 的 inuse 標誌,該標誌處於 victim chunk 的下一個相鄰 chunk 的 size 欄位的第一個 bit。從 small bin 中取出 victim 也可以用 unlink() 宏函式,只是這裡沒有使用。
else
{
bck = victim->bk;
//經典的透過檢查 victim 的 bck 的 fd 指標是否指向 victim,來確定連結串列是否有被破壞。
if (__glibc_unlikely(bck->fd != victim))
{
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
//脫鏈。
set_inuse_bit_at_offset(victim, nb);
bin->bk = bck;
bck->fd = bin;
//接著判斷當前分配區是否為非主分配區,如果是,將 victim chunk 的 size 欄位中的表示非主分配區的標誌 bit 清零,最後呼叫 chunk2mem() 函式獲得 chunk 的實際可用的記憶體指標,將該記憶體指標返回給應用層。
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
}
}
這裡要注意的是進行 malloc_consolidate
的時機,以及在這段程式碼中有兩種情況是沒有獲取到 chunk 的,需要下一步的程式來處理:
- 當對應的 small bin 中沒有空閒 chunk。
- 對應的 small bin 還沒有初始化完成,這時就會呼叫 malloc_consolidate。
接下來,如果 chunk 不屬於 small bins,那麼就一定屬於 large bins,這裡只直接進行 malloc_consolidate
,然後去走下面的大迴圈了。
/*
If this is a large request, consolidate fastbins before continuing.
While it might look excessive to kill all fastbins before
even seeing if there is space available, this avoids
fragmentation problems normally associated with fastbins.
Also, in practice, programs tend to have runs of either small or
large requests, but less often mixtures, so consolidation is not
invoked all that often in most programs. And the programs that
it is called frequently in otherwise tend to fragment.
*/
else
{
idx = largebin_index(nb);
if (have_fastchunks(av))
malloc_consolidate(av);
}
最後是一個多重迴圈,此時 fast bins 裡的 chunk 騰完到 unsorted 裡了,所以接下來開始搗騰 unsorted bin。
/*
Process recently freed or remaindered chunks, taking one only if
it is exact fit, or, if this a small request, the chunk is remainder from
the most recent non-exact fit. Place other traversed chunks in
bins. Note that this step is the only place in any routine where
chunks are placed in bins.
The outer loop here is needed because we might not realize until
near the end of malloc that we should have consolidated, so must
do so and retry. This happens at most once, and only when we would
otherwise need to expand memory to service a "small" request.
*/
for (;;)
{
int iters = 0;
//反向遍歷 unsorted bin 的雙向迴圈連結串列,遍歷結束的條件是迴圈連結串列中只剩下一個頭結點。
while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av))
{
//檢查當前遍歷的 chunk 是否合法。
bck = victim->bk;
if (__builtin_expect(victim->size <= 2 * SIZE_SZ, 0) || __builtin_expect(victim->size > av->system_mem, 0))
malloc_printerr(check_action, "malloc(): memory corruption",
chunk2mem(victim), av);
size = chunksize(victim);
/*
If a small request, try to use last remainder if it is the
only chunk in unsorted bin. This helps promote locality for
runs of consecutive small requests. This is the only
exception to best-fit, and applies only when there is
no exact fit for a small chunk.
*/
//1.如果需要分配一個 small bin chunk,且 unsorted bin 中只有一個 chunk,且這個 chunk 為 last remainder chunk,且這個 chunk 的大小大於所需 chunk 的大小加上 MINSIZE,在滿足這些條件的情況下,可以使用這個chunk切分出需要的small bin chunk。
//這是唯一的從 unsorted bin 中分配出 small bin chunk 的情況,這種最佳化利於 cpu 的快取記憶體命中。
if (in_smallbin_range(nb) &&
bck == unsorted_chunks(av) &&
victim == av->last_remainder &&
(unsigned long)(size) > (unsigned long)(nb + MINSIZE))
{
/* split and reattach remainder */
//切割這個 chunk。
remainder_size = size - nb;
remainder = chunk_at_offset(victim, nb);
unsorted_chunks(av)->bk = unsorted_chunks(av)->fd = remainder;
av->last_remainder = remainder;
remainder->bk = remainder->fd = unsorted_chunks(av);
if (!in_smallbin_range(remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
//設定被分割出去的 chunk 和 剩下的 last remainder chunk 的資訊。
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);
set_foot(remainder, remainder_size);
check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
//將當前遍歷的 chunk 脫鏈。
/* remove from unsorted list */
unsorted_chunks(av)->bk = bck;
bck->fd = unsorted_chunks(av);
/* Take now instead of binning if exact fit */
//2.若當前遍歷的 chunk 的 size 與 nb 一致,設定物理相鄰的下一個堆塊的 pre_inuse 位,返回指標,結束分配。
if (size == nb)
{
set_inuse_bit_at_offset(victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
/* place chunk in bin */
//3.如果當前遍歷的 chunk 屬於 small bins,那就將它鏈入 small bins。
if (in_smallbin_range(size))
{
victim_index = smallbin_index(size);
bck = bin_at(av, victim_index);
fwd = bck->fd;
}
else
{
//4.如果當前遍歷的 chunk 屬於 large bins,那就將它鏈入 large bins。
victim_index = largebin_index(size);
bck = bin_at(av, victim_index);
fwd = bck->fd;
/* maintain large bins in sorted order */
//當 large bin 鏈中存在 bins 時,要將該 chunk 鏈入合適的位置。
//從這段原始碼就可以看出來一個 chunk 存在於兩個雙向迴圈連結串列中,一個連結串列包含了 large bin 中所有的 chunk,另一個連結串列為 chunk size 連結串列,該連結串列從每個相同大小的 chunk 的取出第一個 chunk 按照大小順序連結在一起,便於一次跨域多個相同大小的 chunk 遍歷下一個不同大小的 chunk,這樣可以加快在 large bin 連結串列中的遍歷速度。
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert((bck->bk->size & NON_MAIN_ARENA) == 0);
if ((unsigned long)(size) < (unsigned long)(bck->bk->size))
{
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
//正向遍歷 chunk size 連結串列,直到在鏈中找到第一個大小小於等於當前 chunk 大小的塊。
else
{
assert((fwd->size & NON_MAIN_ARENA) == 0);
while ((unsigned long)size < fwd->size)
{
fwd = fwd->fd_nextsize;
assert((fwd->size & NON_MAIN_ARENA) == 0);
}
if ((unsigned long)size == (unsigned long)fwd->size)
/* Always insert in the second position. */
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
}
//當 large bin 鏈中沒有 bins 時,直接將該 chunk 入鏈。
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}
mark_bin(av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
//如果 unsorted bin 中的 chunk 超過了 10000 個,最多遍歷 10000 個就退出,避免長時間處理 unsorted bin 影響記憶體分配的效率。
#define MAX_ITERS 10000
if (++iters >= MAX_ITERS)
break;
}
前面將 unsorted bin 中的空閒 chunk 加入到相應的 small bins 和 large bins 後,(如果 nb 不屬於 small bins size 範圍的話)先使用最佳匹配法分配 large bin chunk,即首先到所屬的 large bin 鏈中查詢。
/*
If a large request, scan through the chunks of current bin in
sorted order to find smallest that fits. Use the skip list for this.
*/
//如果所需分配的 chunk 為 large bin chunk,查詢對應的 large bin 連結串列,如果 large bin 連結串列為空,或者連結串列中最大的 chunk 也不能滿足要求,則不能從 large bin 中分配。否則,遍歷 large bin 連結串列,找到合適的 chunk。
if (!in_smallbin_range(nb))
{
bin = bin_at(av, idx);
/* skip scan if empty or largest chunk is too small */
if ((victim = first(bin)) != bin &&
(unsigned long)(victim->size) >= (unsigned long)(nb))
{
victim = victim->bk_nextsize;
//反向遍歷 chunk size 連結串列,直到找到第一個大於等於所需 chunk 大小的 chunk 退出迴圈。
while (((unsigned long)(size = chunksize(victim)) <
(unsigned long)(nb)))
victim = victim->bk_nextsize;
/* Avoid removing the first entry for a size so that the skip
list does not have to be rerouted. */
//如果從 large bin 連結串列中選取的 chunk victim 不是連結串列中的最後一個 chunk,並且與 victim 大小相同的chunk不止一個,那麼意味著 victim 為 chunk size 連結串列中的節點,為了不調整 chunk size 連結串列,需要避免將 chunk size 連結串列中的節點取出,所以取 victim->fd 節點對應的 chunk 作為候選 chunk。由於 large bin 連結串列中的 chunk 也是按大小排序,同一大小的 chunk 有多個時,這些 chunk 必定排在一起,所以 victim->fd 節點對應的 chunk 的大小必定與 victim 的大小一樣。
//這樣脫鏈的就變成了 victim 的下一個同樣大小的堆塊了,減少了工作量,因為不用去修改 chunk size 連結串列。
if (victim != last(bin) && victim->size == victim->fd->size)
victim = victim->fd;
//計算將 victim 切分後剩餘大小,並呼叫 unlink() 宏函式將 victim 從 large bin 連結串列中取出。
remainder_size = size - nb;
unlink(av, victim, bck, fwd);
//5.1.如果將 victim 切分後剩餘大小小於 MINSIZE,則將整個 victim 分配出去。
/* Exhaust */
if (remainder_size < MINSIZE)
{
set_inuse_bit_at_offset(victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
}
//5.2.從 victim 中切分出所需的 chunk,剩餘部分作為一個新的 chunk 加入到 unsorted bin 中。如果剩餘部分 chunk 屬於 large bins,將剩餘部分 chunk 的 chunk size 連結串列指標設定為 NULL,因為 unsorted bin 中的 chunk 是不排序的,這兩個指標無用,必須清零。
//劃重點了,這裡被切割了的 chunk 剩餘部分會進入 unsorted bin 鏈中。
/* Split */
else
{
remainder = chunk_at_offset(victim, nb);
/* We cannot assume the unsorted list is empty and therefore
have to perform a complete insert here. */
bck = unsorted_chunks(av);
fwd = bck->fd;
if (__glibc_unlikely(fwd->bk != bck))
{
errstr = "malloc(): corrupted unsorted chunks";
goto errout;
}
remainder->bk = bck;
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;
if (!in_smallbin_range(remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);
set_foot(remainder, remainder_size);
}
//至此已經從 large bin 中使用最佳匹配法找到了合適的 chunk,呼叫 chunk2mem() 獲得 chunk 中可用的記憶體指標,返回給應用層。
check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
}
如果透過上面的方式從最合適的 large bin 中都沒有分配到需要的 chunk,則檢視比當前 bin 的 index 大的 small bin 或 large bin 是否有空閒 chunk 可利用來分配所需的 chunk。原始碼實現如下。
/*
Search for a chunk by scanning bins, starting with next largest
bin. This search is strictly by best-fit; i.e., the smallest
(with ties going to approximately the least recently used) chunk
that fits is selected.
The bitmap avoids needing to check that most blocks are nonempty.
The particular case of skipping all bins during warm-up phases
when no chunks have been returned yet is faster than it might look.
*/
//獲取下一個相鄰 bin 的空閒 chunk 連結串列,並獲取該 bin 對於 binmap 中的 bit 位的值。Binmap 中的標識了相應的 bin 中是否有空閒 chunk 存在。Binmap 按 block 管理,每個 block 為一個int,共 32 個 bit,可以表示 32 個 bin 中是否有空閒 chunk 存在。使用 binmap 可以加快查詢 bin 是否包含空閒 chunk。這裡只查詢比所需 chunk 大的 bin 中是否有空閒 chunk 可用。
++idx;
bin = bin_at(av, idx);
block = idx2block(idx);
map = av->binmap[block];
bit = idx2bit(idx);
//遍歷 binmap 的每一個 block,直到找到一個不為 0 的 block 或者遍歷完所有的 block。退出迴圈遍歷後,設定 bin 指向 block 的第一個 bit 對應的 bin,並將 bit 置為 1,表示該 block 中 bit 1 對應的 bin,就是能夠取 chunk 的 bin 鏈,這個 bin 中如果有空閒 chunk,它的 chunk 的大小一定滿足要求。
for (;;)
{
/* Skip rest of block if there are no more set bits in this block. */
if (bit > map || bit == 0)
{
do
{
if (++block >= BINMAPSIZE) /* out of bins */
goto use_top;
} while ((map = av->binmap[block]) == 0);
bin = bin_at(av, (block << BINMAPSHIFT));
bit = 1;
}
//在一個 block 遍歷對應的 bin,直到找到一個 bit 不為 0 退出遍歷,則該 bit 對於的 bin 中有空閒 chunk 存在。
/* Advance to bin with set bit. There must be one. */
while ((bit & map) == 0)
{
bin = next_bin(bin);
bit <<= 1;
assert(bit != 0);
}
//將 bin 連結串列中的最後一個 chunk 賦值為 victim。
/* Inspect the bin. It is likely to be non-empty */
victim = last(bin);
//如果 victim 與 bin 連結串列頭指標相同,表示該 bin 中沒有空閒 chunk,binmap 中的相應位設定不準確,將 binmap 的相應 bit 位清零,獲取當前 bin 下一個 bin,將 bit 移到下一個 bit 位,即乘以 2。
/* If a false alarm (empty bin), clear the bit. */
if (victim == bin)
{
av->binmap[block] = map &= ~bit; /* Write through */
bin = next_bin(bin);
bit <<= 1;
}
//6.當前 bin 中的最後一個 chunk 滿足要求,獲取該 chunk 的大小,計算切分出所需 chunk 後剩餘部分的大小,然後將 victim 從 bin 的連結串列中取出。接下來的操作跟“5”的基本差不多,有剩剩餘部分會進 unsorted。
else
{
size = chunksize(victim);
/* We know the first chunk in this bin is big enough to use. */
assert((unsigned long)(size) >= (unsigned long)(nb));
remainder_size = size - nb;
/* unlink */
unlink(av, victim, bck, fwd);
/* Exhaust */
if (remainder_size < MINSIZE)
{
set_inuse_bit_at_offset(victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
}
/* Split */
else
{
remainder = chunk_at_offset(victim, nb);
/* We cannot assume the unsorted list is empty and therefore
have to perform a complete insert here. */
bck = unsorted_chunks(av);
fwd = bck->fd;
if (__glibc_unlikely(fwd->bk != bck))
{
errstr = "malloc(): corrupted unsorted chunks 2";
goto errout;
}
remainder->bk = bck;
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;
/* advertise as last remainder */
if (in_smallbin_range(nb))
av->last_remainder = remainder;
if (!in_smallbin_range(remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);
set_foot(remainder, remainder_size);
}
check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
}
如果從所有的 bins 中都沒有獲得所需的 chunk,可能的情況為 bins 中沒有空閒 chunk,或者所需的 chunk 大小很大,下一步將嘗試從 top chunk 中分配所需 chunk。原始碼實現如下,其實主要是切割 top chunk 或者申請系統資源了。
use_top:
/*
If large enough, split off the chunk bordering the end of memory
(held in av->top). Note that this is in accord with the best-fit
search rule. In effect, av->top is treated as larger (and thus
less well fitting) than any other available chunk since it can
be extended to be as large as necessary (up to system
limitations).
We require that av->top always exists (i.e., has size >=
MINSIZE) after initialization, so if it would otherwise be
exhausted by current request, it is replenished. (The main
reason for ensuring it exists is that we may need MINSIZE space
to put in fenceposts in sysmalloc.)
*/
victim = av->top;
size = chunksize(victim);
if ((unsigned long)(size) >= (unsigned long)(nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset(victim, nb);
av->top = remainder;
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);
check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
/* When we are using atomic ops to free fast chunks we can get
here for all block sizes. */
else if (have_fastchunks(av))
{
malloc_consolidate(av);
/* restore original bin index */
if (in_smallbin_range(nb))
idx = smallbin_index(nb);
else
idx = largebin_index(nb);
}
/*
Otherwise, relay to handle system-dependent cases
*/
else
{
void *p = sysmalloc(nb, av);
if (p != NULL)
alloc_perturb(p, bytes);
return p;
}
}
2.2 記憶體整理 malloc_consolidate
malloc_consolidate(
) 函式用於將 fast bins 中的 chunk 合併,並加入 unsorted bin 中,原始碼如下。
static void malloc_consolidate(mstate av)
{
mfastbinptr *fb; /* current fastbin being consolidated */
mfastbinptr *maxfb; /* last fastbin (for loop control) */
mchunkptr p; /* current chunk being consolidated */
mchunkptr nextp; /* next chunk to consolidate */
mchunkptr unsorted_bin; /* bin header */
mchunkptr first_unsorted; /* chunk to link to */
/* These have same use as in free() */
mchunkptr nextchunk;
INTERNAL_SIZE_T size;
INTERNAL_SIZE_T nextsize;
INTERNAL_SIZE_T prevsize;
int nextinuse;
mchunkptr bck;
mchunkptr fwd;
/*
If max_fast is 0, we know that av hasn't
yet been initialized, in which case do so below
*/
//如果全域性變數 global_max_fast 不為零,表示 ptmalloc 已經初始化,然後清除分配區 flag 中 fast bin 的標誌位,該標誌位表示分配區的 fast bins 中包含空閒 chunk,表示將要把裡面的所有 chunk 都清空。
if (get_max_fast() != 0)
{
clear_fastchunks(av);
unsorted_bin = unsorted_chunks(av);
/*
Remove each chunk from fast bin and consolidate it, placing it
then in unsorted bin. Among other reasons for doing this,
placing in unsorted bin avoids needing to calculate actual bins
until malloc is sure that chunks aren't immediately going to be
reused anyway.
*/
//將分配區最大的 fast bin 鏈指標賦值給 maxfb,第一條 fast bin 鏈指標賦值給 fb,然後遍歷 fast bins 的每條鏈。
maxfb = &fastbin(av, NFASTBINS - 1);
fb = &fastbin(av, 0);
do
{
//獲取當前 bin 鏈的頭指標賦值給 p,如果 p 不為 0,則說明當前 bin 鏈中存在 chunk,所有將當前 fast bin 連結串列的頭指標賦值為 0,即刪除了該 fast bin 中的空閒 chunk 連結串列,然後對這條鏈中的 chunk 進行遍歷。
p = atomic_exchange_acq(fb, 0);
if (p != 0)
{
do
{
check_inuse_chunk(av, p);
nextp = p->fd;
/* Slightly streamlined version of consolidation code in free() */
size = p->size & ~(PREV_INUSE | NON_MAIN_ARENA);
nextchunk = chunk_at_offset(p, size);
nextsize = chunksize(nextchunk);
//檢查當前 chunk 的前一個 chunk 是否空閒,先合併,沒有直接被鏈入 unsorted,因為還沒檢查物理相鄰的下一個 chunk 是否空閒。
//如果當前 chunk 的前一個 chunk 空閒,則將當前 chunk 與前一個 chunk 合併成一個空閒 chunk,由於前一個 chunk 空閒,則當前 chunk 的 prev_size 儲存了前一個 chunk 的大小,計算出合併後的 chunk 大小,並獲取前一個 chunk 的指標,將前一個 chunk 從空閒連結串列中刪除。
if (!prev_inuse(p))
{
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long)prevsize));
unlink(av, p, bck, fwd);
}
//如果與當前 chunk 相鄰的下一個 chunk 不是分配區的 top chunk,檢視與當前 chunk 相鄰的下一個 chunk 是否處於 inuse 狀態。
if (nextchunk != av->top)
{
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
//如果與當前 chunk 相鄰的下一個 chunk 不處於 inuse 狀態,,將相鄰的下一個空閒 chunk 從空閒連結串列中刪除,並計算當前 chunk 與下一個 chunk 合併後的 chunk 大小。
if (!nextinuse)
{
size += nextsize;
unlink(av, nextchunk, bck, fwd);
}
//如果與當前 chunk 相鄰的下一個 chunk 處於 inuse 狀態,清除當前 chunk 的 inuse 狀態。
else
clear_inuse_bit_at_offset(nextchunk, 0);
//將合併後的 chunk 加入 unsorted bin 的雙向迴圈連結串列中。
first_unsorted = unsorted_bin->fd;
unsorted_bin->fd = p;
first_unsorted->bk = p;
//如果合併後的 chunk 屬於 large bin,將 chunk 的 fd_nextsize 和 bk_nextsize 設定為 NULL,因為在 unsorted bin 中這兩個欄位無用。
//這裡注意一下,特意清了資料。
if (!in_smallbin_range(size))
{
p->fd_nextsize = NULL;
p->bk_nextsize = NULL;
}
//設定合併後的空閒 chunk 大小,並標識前一個 chunk 處於 inuse 狀態,因為必須保證不能有兩個相鄰的 chunk 都處於空閒狀態。然後將合併後的 chunk 加入 unsorted bin 的雙向迴圈連結串列中。最後設定合併後的空閒 chunk 的 foot 為自身的 size,chunk 空閒時必須設定 foot,該 foot 處於下一個 chunk 的 prev_size 中,只有 chunk 空閒是 foot 才是有效的。
set_head(p, size | PREV_INUSE);
p->bk = unsorted_bin;
p->fd = first_unsorted;
set_foot(p, size);
}
//如果當前 chunk 的下一個 chunk 為 top chunk,則將當前 chunk 合併入 top chunk,修改 top chunk 的大小。
else
{
size += nextsize;
set_head(p, size | PREV_INUSE);
av->top = p;
}
//直到遍歷完當前 bin 鏈中的所有空閒 chunk。
} while ((p = nextp) != 0);
}
//直到遍歷完 fast bins 的每一條 bin 鏈。
} while (fb++ != maxfb);
}
//如果 ptmalloc 沒有初始化,初始化 ptmalloc。
else
{
malloc_init_state(av);
check_malloc_state(av);
}
}
2.3 記憶體釋放 free
一樣的,ptmalloc2 裡,__libc_free()
是對 _int_free()
函式的簡單封裝,首先看__libc_free()
。
void __libc_free(void *mem)
{
mstate ar_ptr;
mchunkptr p; /* chunk corresponding to mem */
//如果存在 free 的 hook 函式,執行該 hook 函式返回,free 的 hook 函式主要用於建立新執行緒使用或使用使用者提供的 free 函式。
void (*hook)(void *, const void *) = atomic_forced_read(__free_hook);
if (__builtin_expect(hook != NULL, 0))
{
(*hook)(mem, RETURN_ADDRESS(0));
return;
}
if (mem == 0) /* free(0) has no effect */
return;
//根據要釋放的記憶體空間指標獲取 chunk 指標。
p = mem2chunk(mem);
//如果當前 free 的 chunk 是透過 mmap() 分配的,呼叫 munmap_chunk() 函式 unmap 本 chunk。munmap_chunk() 函式呼叫 munmap() 函式釋放 mmap() 分配的記憶體塊。同時檢視是否開啟了 mmap 分配閾值動態調整機制,預設是開啟的,如果當前 free 的 chunk 的大小大於設定的 mmap 分配閾值,小於 mmap 分配閾值的最大值,將當前 chunk 的大小賦值給 mmap 分配閾值,並修改 mmap 收縮閾值為 mmap 分配閾值的 2 倍。預設情況下 mmap 分配閾值與 mmap 收縮閾值相等,都為 128KB。程式返回。
if (chunk_is_mmapped(p)) /* release mmapped memory. */
{
/* see if the dynamic brk/mmap threshold needs adjusting */
if (!mp_.no_dyn_threshold && p->size > mp_.mmap_threshold && p->size <= DEFAULT_MMAP_THRESHOLD_MAX)
{
mp_.mmap_threshold = chunksize(p);
mp_.trim_threshold = 2 * mp_.mmap_threshold;
LIBC_PROBE(memory_mallopt_free_dyn_thresholds, 2,
mp_.mmap_threshold, mp_.trim_threshold);
}
munmap_chunk(p);
return;
}
//根據 chunk 指標獲得分配區的指標,即 chunk 的管理塊 arena,然後呼叫 _int_free() 函式執行實際的釋放工作。
ar_ptr = arena_for_chunk(p);
_int_free(ar_ptr, p, 0);
}
然後是 _int_free()
函式,實現原始碼如下。
static void
_int_free(mstate av, mchunkptr p, int have_lock)
{
INTERNAL_SIZE_T size; /* its size */
mfastbinptr *fb; /* associated fastbin */
mchunkptr nextchunk; /* next contiguous chunk */
INTERNAL_SIZE_T nextsize; /* its size */
int nextinuse; /* true if nextchunk is used */
INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */
mchunkptr bck; /* misc temp for linking */
mchunkptr fwd; /* misc temp for linking */
const char *errstr = NULL;
int locked = 0;
size = chunksize(p);
//首先進行一系列的安全檢查。chunk 的指標地址不能溢位,chunk 的大小必須大於等於 MINSIZE。
/* Little security check which won't hurt performance: the
allocator never wrapps around at the end of the address space.
Therefore we can exclude some size values which might appear
here by accident or by "design" from some intruder. */
if (__builtin_expect((uintptr_t)p > (uintptr_t)-size, 0) || __builtin_expect(misaligned_chunk(p), 0))
{
errstr = "free(): invalid pointer";
errout:
if (!have_lock && locked)
(void)mutex_unlock(&av->mutex);
malloc_printerr(check_action, errstr, chunk2mem(p), av);
return;
}
/* We know that each chunk is at least MINSIZE bytes in size or a
multiple of MALLOC_ALIGNMENT. */
if (__glibc_unlikely(size < MINSIZE || !aligned_OK(size)))
{
errstr = "free(): invalid size";
goto errout;
}
check_inuse_chunk(av, p);
2.3.1 檢查是否能被鏈進 fast bins。
/*
If eligible, place chunk on a fastbin so it can be found
and used quickly in malloc.
*/
//如果當前 free 的 chunk 屬於 fast bins 且下一個 chunk 不是 top chunk,檢視下一個相鄰的 chunk 的大小是否小於等於 2*SIZE_SZ,且是否大於分配區,即檢查下一個相鄰 chunk 的大小有沒有問題。
if ((unsigned long)(size) <= (unsigned long)(get_max_fast())
#if TRIM_FASTBINS
/*
If TRIM_FASTBINS set, don't place chunks
bordering top into fastbins
*/
&& (chunk_at_offset(p, size) != av->top)
#endif
)
{
if (__builtin_expect(chunk_at_offset(p, size)->size <= 2 * SIZE_SZ, 0) || __builtin_expect(chunksize(chunk_at_offset(p, size)) >= av->system_mem, 0))
{
/* We might not have a lock at this point and concurrent modifications
of system_mem might have let to a false positive. Redo the test
after getting the lock. */
if (have_lock || ({
assert(locked == 0);
mutex_lock(&av->mutex);
locked = 1;
chunk_at_offset(p, size)->size <= 2 * SIZE_SZ || chunksize(chunk_at_offset(p, size)) >= av->system_mem;
}))
{
errstr = "free(): invalid next size (fast)";
goto errout;
}
if (!have_lock)
{
(void)mutex_unlock(&av->mutex);
locked = 0;
}
}
//設定當前分配區的 fast bin flag,表示當前分配區的 fast bins 中已有空閒 chunk。然後根據當前 free 的 chunk 大小獲取其所屬的 fast bin 頭指標。
free_perturb(chunk2mem(p), size - 2 * SIZE_SZ);
set_fastchunks(av);
unsigned int idx = fastbin_index(size);
fb = &fastbin(av, idx);
//檢查 double free 的,即檢查 fast bin 鏈頭的 chunk 和要釋放的 chunk 是否一致。
/* Atomically link P to its fastbin: P->FD = *FB; *FB = P; */
mchunkptr old = *fb, old2;
unsigned int old_idx = ~0u;
do
{
/* Check that the top of the bin is not the record we are going to add
(i.e., double free). */
if (__builtin_expect(old == p, 0))
{
errstr = "double free or corruption (fasttop)";
goto errout;
}
//檢查頂部 fastbin 塊的大小是否與我們要新增的塊的大小相同。
/* Check that size of fastbin chunk at the top is the same as
size of the chunk that we are adding. We can dereference OLD
only if we have the lock, otherwise it might have already been
deallocated. See use of OLD_IDX below for the actual check. */
if (have_lock && old != NULL)
old_idx = fastbin_index(chunksize(old));
p->fd = old2 = old;
} while ((old = catomic_compare_and_exchange_val_rel(fb, p, old2)) != old2);
if (have_lock && old != NULL && __builtin_expect(old_idx != idx, 0))
{
errstr = "invalid fastbin entry (free)";
goto errout;
}
}
2.3.2 如果當前 free 的 chunk 的大小不在 fast bins 的範圍內,且不是透過 mmap() 分配的,首先進行一系列檢查。
/*
Consolidate other non-mmapped chunks as they arrive.
*/
else if (!chunk_is_mmapped(p))
{
//當前還沒有獲得分配區的鎖,獲取分配區的鎖。
if (!have_lock)
{
(void)mutex_lock(&av->mutex);
locked = 1;
}
//獲取當前 free 的 chunk 的下一個相鄰的 chunk。
nextchunk = chunk_at_offset(p, size);
//進行安全檢查,當前 free 的 chunk 不能為 top chunk,因為 top chunk 為空閒 chunk,如果再次 free 就可能為 double free 錯誤了。
/* Lightweight tests: check whether the block is already the
top block. */
if (__glibc_unlikely(p == av->top))
{
errstr = "double free or corruption (top)";
goto errout;
}
//如果當前 free 的 chunk 是透過 sbrk() 分配的,並且下一個相鄰的 chunk 的地址已經超過了 top chunk 的結束地址,即超過了當前分配區的結束地址,報錯。
/* Or whether the next chunk is beyond the boundaries of the arena. */
if (__builtin_expect(contiguous(av) && (char *)nextchunk >= ((char *)av->top + chunksize(av->top)), 0))
{
errstr = "double free or corruption (out)";
goto errout;
}
//如果當前 free 的 chunk 的下一個相鄰 chunk 的 size 中標誌位沒有標識當前 free chunk 為 inuse 狀態,可能為 double free 錯誤。
//這就是為什麼 fast bin 的 double free 這麼容易利用,因為 chunk 被鏈入 fast bin 是不會將下一個 chunk 的 pre_inuse 位置 0 的。
/* Or whether the block is actually not marked used. */
if (__glibc_unlikely(!prev_inuse(nextchunk)))
{
errstr = "double free or corruption (!prev)";
goto errout;
}
//計算當前 free 的 chunk 的下一個相鄰 chunk 的大小,該大小如果小於等於 2*SIZE_SZ 或是大於了分配區所分配區的記憶體總量,報錯。
nextsize = chunksize(nextchunk);
if (__builtin_expect(nextchunk->size <= 2 * SIZE_SZ, 0) || __builtin_expect(nextsize >= av->system_mem, 0))
{
errstr = "free(): invalid next size (normal)";
goto errout;
}
free_perturb(chunk2mem(p), size - 2 * SIZE_SZ);
2.3.3 然後進行 consolidate。
/* consolidate backward */
//如果當前 free 的 chunk 的前一個相鄰 chunk 為空閒狀態,與前一個空閒 chunk 合併。計算合併後的 chunk 大小,並將前一個相鄰空閒 chunk 從空閒 chunk 連結串列中刪除。
if (!prev_inuse(p))
{
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long)prevsize));
unlink(av, p, bck, fwd);
}
//如果與當前 free 的 chunk 相鄰的下一個 chunk 不是分配區的 top chunk,檢視與當前 chunk 相鄰的下一個 chunk 是否處於 inuse 狀態。如果與當前 free 的 chunk 相鄰的下一個 chunk 處於 inuse 狀態,清除當前 chunk 的 inuse 狀態,則當前 chunk 空閒了。
//否則,將相鄰的下一個空閒 chunk 從空閒連結串列中刪除,並計算當前 chunk 與下一個 chunk 合併後的 chunk 大小。
if (nextchunk != av->top)
{
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
/* consolidate forward */
if (!nextinuse)
{
unlink(av, nextchunk, bck, fwd);
size += nextsize;
}
else
clear_inuse_bit_at_offset(nextchunk, 0);
/*
Place the chunk in unsorted chunk list. Chunks are
not placed into regular bins until after they have
been given one chance to be used in malloc.
*/
//將合併後的 chunk 加入 unsorted bin 的雙向迴圈連結串列中。如果合併後的 chunk 屬於 large bins,將 chunk 的 fd_nextsize 和 bk_nextsize 設定為 NULL,因為在 unsorted bin 中這兩個欄位無用。
bck = unsorted_chunks(av);
fwd = bck->fd;
if (__glibc_unlikely(fwd->bk != bck))
{
errstr = "free(): corrupted unsorted chunks";
goto errout;
}
p->fd = fwd;
p->bk = bck;
if (!in_smallbin_range(size))
{
p->fd_nextsize = NULL;
p->bk_nextsize = NULL;
}
bck->fd = p;
fwd->bk = p;
//設定合併後的空閒 chunk 大小,並標識前一個 chunk 處於 inuse 狀態,因為必須保證不能有兩個相鄰的 chunk 都處於空閒狀態。然後將合併後的 chunk 加入 unsorted bin 的雙向迴圈連結串列中。最後設定合併後的空閒 chunk 的 foot,chunk 空閒時必須設定 foot,該 foot 處於下一個 chunk 的 prev_size 中,只有 chunk 空閒是 foot 才是有效的。
set_head(p, size | PREV_INUSE);
set_foot(p, size);
check_free_chunk(av, p);
}
/*
If the chunk borders the current high end of memory,
consolidate into top
*/
//如果當前 free 的 chunk 下一個相鄰的 chunk 為 top chunk,則將當前 chunk 合併入 top chunk,修改 top chunk 的大小。
else
{
size += nextsize;
set_head(p, size | PREV_INUSE);
av->top = p;
check_chunk(av, p);
}
2.3.4 對合並後的 chunk 進行相關操作,注意這裡是有機會觸發 malloc_consolidate()
的。
/*
If freeing a large space, consolidate possibly-surrounding
chunks. Then, if the total unused topmost memory exceeds trim
threshold, ask malloc_trim to reduce top.
Unless max_fast is 0, we don't know if there are fastbins
bordering top, so we cannot tell for sure whether threshold
has been reached unless fastbins are consolidated. But we
don't want to consolidate on each free. As a compromise,
consolidation is performed if FASTBIN_CONSOLIDATION_THRESHOLD
is reached.
*/
//如果合併後的 chunk 大小大於 64KB(0x10000),並且 fast bins 中存在空閒 chunk,呼叫 malloc_consolidate() 函式合併 fast bins 中的空閒 chunk 到 unsorted bin 中。
//這裡也很重要,就是判斷得到的 unsorted bin size 是否大於 FASTBIN_CONSOLIDATION_THRESHOLD,就會觸發 malloc_consolidate。
if ((unsigned long)(size) >= FASTBIN_CONSOLIDATION_THRESHOLD)
{
if (have_fastchunks(av))
malloc_consolidate(av);
//如果當前分配區為主分配區,並且 top chunk 的大小大於 heap 的收縮閾值,呼叫 systrim() 函式收縮 heap。
if (av == &main_arena)
{
#ifndef MORECORE_CANNOT_TRIM
if ((unsigned long)(chunksize(av->top)) >=
(unsigned long)(mp_.trim_threshold))
systrim(mp_.top_pad, av);
#endif
}
//如果為非主分配區,呼叫 heap_trim()函式收縮非主分配區的 sub_heap。
else
{
/* Always try heap_trim(), even if the top chunk is not
large, because the corresponding heap might go away. */
heap_info *heap = heap_for_ptr(top(av));
assert(heap->ar_ptr == av);
heap_trim(heap, mp_.top_pad);
}
}
//如果獲得了分配區的鎖,則對分配區解鎖。
if (!have_lock)
{
assert(locked);
(void)mutex_unlock(&av->mutex);
}
}
2.3.5 如果當前 free 的 chunk 是透過 mmap() 分配的。
/*
If the chunk was allocated via mmap, release via munmap().
*/
//如果當前 free 的 chunk 是透過 mmap()分配的,呼叫 munma_chunk()釋放記憶體。
else
{
munmap_chunk(p);
}
}
3. 補充
3.1 觸發 malloc_consolidate
2.23 下的 malloc_consolidate
fast bins 會在以下情況下進行與前後堆塊的合併(合併是對所有 fast bins 中的 chunk 而言)並鏈入 unsorted bin。
malloc:
1、在申請 fast bins 以外、small bins 以內的大小的堆塊時,去查詢對應的 small bin 還沒有初始化完成。
2、在申請 large chunk 時。
3、當申請的 size 大於 top chunk size,需要申請新的 top chunk 時。
free:
1、free 的堆塊大小若是大於 fast bins 中的最大 size(注意這裡並不是指當前 fast bins 中最大 chunk 的 size,而是指 fast bins 中所定義的最大 chunk 的 size,是一個固定值。),就會嘗試進行與前後塊的合併,合併後的 chunk 會被鏈入 unsorted bin,如果這個被合併過後的 chunk 的大小大於 FASTBIN_CONSOLIDATION_THRESHOLD
(64KB,即 0x10000 B),且 fast bins 鏈中有 chunk,就會觸發 malloc_consolidate
。
另外:malloc_consolidate
既可以作為 fast bins 的初始化函式,也可以作為 fast bins 的合併函式。
合併過程(會迴圈 fast bins 中的每一塊,並對此塊進行操作):
1、首先將與該塊相鄰的下一塊的 PREV_INUSE
置為 1。
2、如果相鄰的上一塊未被佔用,則合併,再判斷相鄰的下一塊是否被佔用,若未被佔用,則合併。
3、不管是否有進行合併,這個 chunk 放到 unsorted bin 中,(如果與top chunk相鄰,則合併到top chunk中),這點尤其要注意,可以用來洩 libc 基地址。
3.2 unlink
unlink 用於從 bin 雙向連結串列刪除某個空閒塊。是把 free 掉的 chunk 從所屬的 bins 鏈中,摘下來的操作(當然還包括一系列的檢測機制)。fast bins 沒有 unlink 機制,它們利用已知的堆塊大小或地址來進行分配和釋放操作,以避免頻繁呼叫 unlink 操作,這就是為什麼漏洞會經常出現在它們身上的原因。
在 free 掉一個堆塊(除 fast bins 大小的 chunk 外)後,glibc 會檢查這塊 chunk 相鄰的上下兩塊 chunk 的狀態,然後判斷是否進行後向合併或前向合併,若要進行合併,這時先觸發對被合併 chunk 的 unlink 操作。
讀下原始碼,P
是指向當前要操作的 chunk 的指標。
#define unlink(AV, P, BK, FD) {
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr ("corrupted double-linked list");
else {
FD->bk = BK;
BK->fd = FD;
具體操作如下:
- 檢查當前 chunk 的 size 欄位與它相鄰的下一塊 chunk 中記錄的
pre_size
是否一樣,如果不一樣,就報corrupted size vs. prev_size
的錯誤。 - 檢查是否滿足
P->fd->bk==P
和P->bk->fd==P
,否則報corrupted double-linked list
的錯誤。 - 解鏈操作:
FD->bk=BK
,BK->fd=FD
(就是迴圈雙連結串列的脫鏈操作)。
對於初學者來說,一開始直接看程式碼的話可能會感覺有點抽象難懂,不過你想,假如要 free chunk P,他們的檢查機制是 FD->bk=P
, BK->fd=P
,如果透過了檢查的話,那麼將執行 FD->bk=BK
, BK->fd=FD
,有沒發現,其實它就是把前面檢查的地方進行了修改,用指標 P 指向的記憶體存放的資料去替換。
怎麼替換呢?就是將 FD->bk 變成了 P->bk,BK->fd 變成了 P->fd。
所以 unlink 能怎麼利用呢?看似無懈可擊的檢查機制下,此時假如有一個管理堆塊的陣列,list[x]
處存放了 P 的指標,如果將 P 的 fd 和 bk 域分別修改為 list[x]-0x18
和 list[x]-0x10
不就妥妥能過檢查了嗎?最後的結果是能將 list[x]
處存放的 P 指標改成 list[x]-0x18
(64位下),此時程式要是想對 chunk P 進行寫操作,不就會寫到 list[x]-0x18
處了嗎。
large bins 的 unlink 還會麻煩些,在此基礎上還要去處理 fd_nextsize 和 bk_nextsize。
if (!in_smallbin_range (chunksize_nomask (P))
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr ("corrupted double-linked list (not small)");
if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
else {
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
} else {
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
} } \
}
}
接下來,結束 unlink 後。
不管是向前合併還是向後合併,unlink 後都會將合併好的 chunk 加入到 unsorted bin 鏈的鏈頭,所以這個 chunk 無論屬於 small bins 還是 large bins 都是沒有 fd_nextsize
和 bk_nextsize
的,會將這兩處置 NULL。最後就設定合併過後的 chunk 的 head 和 foot(設定當前 chunk 的 size,下一塊 chunk 的 pre_size 欄位)。
4. 內容來源
reference
本文以 《Glibc 記憶體管理 Ptmalloc2 原始碼分析》為參考對 malloc 與 free 的過程進行分析。
相關宏定義可查閱
heap - 5 - malloc、free函式相關的宏定義 | Kiprey's Blog
2.23 的 glibc 的版本確實過老,相比 2.31 的原始碼有許多不同之處
glibc 2.31 malloc與free 原始碼分析(持續更新) - PwnKi - 部落格園 (cnblogs.com)