malloc笔记

malloc笔记

五月 11, 2021

学习时的参考文章

glibc源码

以前刚学堆的时候记的笔记,但是以前学的时候不知其所以然,也不会调试分析,近期重新看以前的笔记感觉明了了许多

main_arena and non_main_arena

又称为主分配区和非主分配区,main_arena 是一个结构体

glibc2.27源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct malloc_state
{
/* Serialize access. */
__libc_lock_define (, mutex);
/* Flags (formerly in max_fast). */
int flags;
/* Set if the fastbin chunks contain recently inserted free blocks. */
/* Note this is a bool but not all targets support atomics on booleans. */
int have_fastchunks;//glibc2.27以下没有
/* 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. 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;
};

结构体对每一个成员给出了解释,第一个成员是 linux 下的锁,Doug Lea 实现的内存分配器只有一个主分配区,为了兼容多线程,每次分配内存之前都要对主分配区加锁,防止多线程对内存分配造成影响,这样就导致多线程锁的激烈竞争,降低了内存分配效率,而 ptmalloc 支持多线程,增加了 non_main_arena (非主分配区),所谓 non_main_arena 其结构和主分配区相同,很多分配区通过环形链表相互串联,这样,多个线程就无需争夺同一个分配区了。但是分配区的数量毕竟是有限的,在极端情况下多个线程还是会竞争同一个分配区,所以锁依旧有用,先加锁的进程可以优先使用分配区,如果全部分配区都被加锁,那么后面的进程就会进入阻塞状态。对于 32 位系统来说,arena 最多为核心数量的 2 倍,64 位系统下 arena 最多为核心数量的 8 倍。
第二个成员是标志位,第三个成员用来标识最近是否有新的内存块被插入 fastbin 链表(glibc2.27中新加入)。
第四个成员是 fastbin 链表,第五个成员是 top chunk 的地址,在堆利用中可能会用到。第六个成员标识最后一次拆分 top chunk 得到的剩余部分,第七个成员是 smallbin、largebin 和 unsortedbin 的集合体,一共有 126 个表项。

补:为什么有 126 个表项?这是由于 bin[0] 和 bin[127] 没有被使用,并且 bin[1] 是整个 bin 的头部。 注意 bin 定义的数量为 NBINS * 2 – 2 = 254,为什么是 254? 这是由于缓冲区链表主要有 fd 和 bk 两个指针,smallbin 62 个、largebin 63 个,加在一起是 125 个,再加上一个头结点 bin[1] 共 126 个表项,换算成 index 一共有 252 个,所以 254 个指针空间是完全足够的!

第八个成员 binmap 可以视为一张地图,标识链表是否为空。第九个成员是 next 指针,指向下一个 arena。
第十个成员指向下一个为空的 arena。第十一个成员用来标识绑定在当前 arena 线程的总量。
最后两个成员用来跟踪当前被系统分配的内存总量。
这个 glibc 版本比较新的,有一些新加入的定义。

常用的是在64bit的glibc2.27以下的版本中unsortedbins链表头在main_arena+0x58处,之后从main_arena+0x68开始是smallbinslargebins,从0x20开始按照bin大小排列不同大小的bin链表头;glibc2.27中多了一个have_fastchunks,因为对齐,后面的结构都往后多偏移了0x8字节。每个链表头之间差0x10字节,需要注意的是链表头的fdbk指针在链表头+0x10链表头+0x18的位置,也就是下一个链表头中。

chunk

chunk 称为堆块,是堆的重要组成部分,当用户申请内存块时,系统就会将空间以堆块的形式返回,堆块具有一定的结构,且按照大小分为 4 类,堆块的结构定义在 malloc.c 中,代码如下

1
2
3
4
5
6
7
8
9
10
//glibc-2.26/malloc/malloc.c
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
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;
};

基本结构包含 6 个成员,首先是 mchunk_prev_size,如果当前堆块的前一个堆块是空闲的,那么此字段就是前一个堆块的 size,若前一个堆块在使用则此成员无效,且则前一个堆块可以使用这里的空间当作自己的数据区域。

接着是当前堆块的 size,然后有两个指针,由于各种 bin 的存在,当堆块被释放后会进入对应的缓冲区中,并且以链表的形式存在,这里的 fd 和 bk 就是链表的前向后向指针,最后两个也是指针,但是它们只会出现在 largebin chunk 中,具体会在后面提到。

一个堆块可能会是下面的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

需要注意 size 标志位的最低三位 A、M、P,由于对齐的原因,程序中size只会是0x8倍数,64位程序中是0x10的倍数,如果把 size 转换成二进制,它的最低三个 bit 始终都是 0,所以它们就有了了新的用途。
A(NON_MAIN_ARENA) 用来表示当前堆块是否属于 main_arena,M(IS_MAPPED)用来表示当前堆块是否由 mmap 分配,P(PREV_INUSE)是最为常用的标志位,用来表示当前堆块的前一个堆块是否空闲。

bins

为了加快内存分配效率,ptmalloc 引入了缓冲区,把较小的堆块保存在缓冲区中,这样就可以减少和操作系统申请内存的次数,提高效率。缓冲区有一定的格式,按照堆块的大小分成了 4 类即 fastbin、smallbin、largebin、unsortedbin。

fastbin

结构

1
2
3
4
5
6
7
8
9
10
11
12
13
+-----------------+-----------------+
| | |
| prev_size | size |
| | |
+-----------------------------------+
| | |
| fd | |
| | |
+-----------------+ |
| |
| user data |
| |
+-----------------------------------+

fastbin chunk 的大小限制在 0x10 ~ 0x40(0x20 ~ 0x80 if OS is 64 bit),这些 chunk 通过 fd 连接成一条单向链表,在主分配区中定义了 fastbins 指针,我们可以将它展开(64bit):

1
2
3
4
5
6
7
8
9
10
11
   index         size
fastbinY[0] 0x20
fastbinY[1] 0x30
fastbinY[2] 0x40
fastbinY[3] 0x50
fastbinY[4] 0x60
fastbinY[5] 0x70
fastbinY[6] 0x80
fastbinY[7] N/A
fastbinY[8] N/A
fastbinY[9] N/A

最后三个是保留项,暂时没有使用。

fastbin 顾名思义,它分配堆块的速度很快,且仅仅保存很小的堆块,fastbin chunk 的两个特点是没有 bk 指针并且 PREV_INUSE 标志位一定是 1,也就是说 fastbin chunk 不会和其他堆块合并(在特殊情况下还是会发生合并)。另外,fastbin 采用 LIFO 策略,从头部插入,头部取出,这样可以进一步提高分配效率。

附:fastbin 链表大致结构

smallbin

第二类是 smallbin,这也是很常用的链表,smallbin chunk 近似于一个标准格式的 chunk,结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+-----------------+-----------------+
| | |
| prev_size | size |
| | |
+-----------------------------------+
| | |
| fd | bk |
| | |
+-----------------------------------+
| |
| |
| user data |
| |
| |
+-----------------+-----------------+

相比于 fastbin chunk,这里多出了 bk 指针,需要注意的是 fd 和 bk 指针(以及 fd_nextsize、bk_nextsize 指针)都是可以作为用户数据被覆盖的,它们只会在堆块空闲时发挥作用,堆块使用时这些指针的位置都是数据区。

smallbin 的范围在 0x10 ~ 0x1f8(0x20 ~ 0x3f0 if OS is 64 bit),smallbin 和 fastbin 有一部分是重合的,其实 fastbin 中的堆块在一定情况下可以进入到 smallbin 中(当发生 consolidate 时)。一些 smallbin chunk 相互串联形成了一条双向链表

附:smallbin 链表大致结构

smallbin 采用FIFO,链表从头部插入,尾部取出。

largebin

第三类是 largebin,专门用来保存一些较大的堆块,范围从 0x200(0x400 if OS is 64bit) 开始。一个 largebin chunk 结构可能如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+---------------+---------------+
| | |
| prev_size | size |
| | |
+-------------------------------+
| | |
| fd | bk |
| | |
+-------------------------------+
| | |
| fd_nextsize | bk_nextsize |
| | |
+---------------+---------------+
| |
| |
| user_data |
| |
+-------------------------------+

largebin共63个,32bit组织方法如下:

32个bin 每0x40个字节一个阶层,比如第一个0x200-0x238字节,第二个0x240 – 0x278字节……

16个bin 每0x200字节一个阶层

8个bin每0x1000字节一个阶层

4个bin每0x8000字节一个阶层

2个bin每0x40000字节一个阶层

64bit一样的组织方式只是从0x400开始

最后一个bin包括所有剩下的大小。不同于其他链表,largebin 每一个表项保存的是一个范围,所以会用到 fd_nextsize 和 bk_nextsize 指针。largebin 中的chunk是按 size 排序的,bin->fd指向最大的 chunk,bin->bk指向最小的,每个 chunk 的 fd 指针指向下一个 比该 chunk 小或 size 相同的 chunk,bk 指针则相反。fd_nextsize指针指向下一个比该 chunk 小的 chunk,bk_nextsize则相反。

还有最大的 chunk 的bk_nextsize指针指向最小的 chunk,反之亦然。而最大 chunk 的 bk 指针和最小 chunk 的 fd 指针都指向 bin。

若 bin 中没有相同大小的 chunks,那么除了上面讲的最大最小 chunks 以外的 chunks 的 fd 指针和 fd_nextsize、bk 指针和 bk_nextsize指针完全相同。如果有相同大小的 chunks,nextsize 指针以从 bin->fd 方向出来最靠前的 chunk 为准。

unsortedbin

第四类是 unsortedbin,这个链表比较特殊,它没有针对大小进行排序,这一点从名字也能看出来,它可以被视为 smallbin 和 largebin 的缓冲区,当用户释放一个堆块之后,会先进入 unsortedbin,再次分配堆块时,ptmalloc 会优先检查这个链表中是否存在合适的堆块,如果找到了,就直接返回给用户(这个过程可能会对 unsortedbin 中的堆块进行切割),若没有找到合适的,系统会清空这个链表,将堆块插入对应的链表中。下面引用 malloc.c 中的注释

1
2
3
4
5
6
7
8
9
Unsorted chunks
All remainders from chunk splits, as well as all returned chunks,
are first placed in the "unsorted" bin. They are then placed
in regular bins after malloc gives them ONE chance to be used before
binning. So, basically, the unsorted_chunks list acts as a queue,
with chunks being placed on it in free (and malloc_consolidate),
and taken off (to be either used or placed in bins) in malloc.
The NON_MAIN_ARENA flag is never set for unsorted chunks, so it
does not have to be taken into account in size comparisons.

malloc 流程

源码来自glibc2.23

malloc的外层函数是__libc_malloc,而实现分配的函数是在__libc_malloc中调用的_int_malloc

__libc_malloc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void *
__libc_malloc (size_t bytes)
{
mstate ar_ptr;
void *victim;

void *(*hook) (size_t, const void *)
= atomic_forced_read (__malloc_hook);
if (__builtin_expect (hook != NULL, 0)) //检查hook
return (*hook)(bytes, RETURN_ADDRESS (0));

arena_get (ar_ptr, bytes); //获取分配区并将其上锁 函数在arena.c中宏定义

victim = _int_malloc (ar_ptr, bytes); //调用_int_malloc分配chunk
/* Retry with another arena only if we were able to find a usable arena
before. */
if (!victim && ar_ptr != NULL) //分配失败就找其他分配区
{
LIBC_PROBE (memory_malloc_retry, 1, bytes);
ar_ptr = arena_get_retry (ar_ptr, bytes); //retry函数中将之前的分配区解锁新的分配区加锁
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;
}

_int_malloc

代码太长不全贴

先用checked_request2size函数把请求的size转化成真实chunk的size,因为对齐还有 chunk 头等原因,返回的 chunk 大小和用户请求的大小会不同

1
2
3
4
5
6
7
8
9
10
11
12
13
... 
/*
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);
...

若没有可用的分配区,则调用 sysmalloc 用 mmap 或者 brk 获取一块。

1
2
3
4
5
6
7
8
9
10
11
...
/* There are no usable arenas. Fall back to sysmalloc to get a chunk from
mmap. */
if (__glibc_unlikely (av == NULL))
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}
...

之后就判断属于哪一种 bins 的尺寸范围执行不同的代码,fastbins 优先

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
...  
/*
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 ())) //判断size
{
idx = fastbin_index (nb); //获取index
mfastbinptr *fb = &fastbin (av, idx); //取该index的第一给块
mchunkptr pp = *fb;
do
{
victim = pp;
if (victim == NULL) //找到NULL表示遍历结束
break;
}
while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))
!= victim); //遍历找到一个合适的chunk
if (victim != 0) //判断是否找到匹配chunk
{
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0)) //检查size
{
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim), av);
return NULL;
}
check_remalloced_chunk (av, victim, nb); //debug用的函数,用macro宏声明,只有debug模式才有内容,在此无视就好
void *p = chunk2mem (victim); //指针指向数据部分
alloc_perturb (p, bytes); //如果设置了perturb_type, 则将获取到的chunk初始化为 perturb_type ^ 0xff
return p; //返回chunk
}
}
...

check_remalloced_chunk研究了好一会儿,网上大佬说这个函数没用但是没讲理由,我就看不懂,最后找到相关 debug 函数的宏定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
#if !MALLOC_DEBUG

# define check_chunk(A, P)
# define check_free_chunk(A, P)
# define check_inuse_chunk(A, P)
# define check_remalloced_chunk(A, P, N)
# define check_malloced_chunk(A, P, N)
# define check_malloc_state(A)

#else

# define check_chunk(A, P) do_check_chunk (A, P)
# define check_free_chunk(A, P) do_check_free_chunk (A, P)
# define check_inuse_chunk(A, P) do_check_inuse_chunk (A, P)
# define check_remalloced_chunk(A, P, N) do_check_remalloced_chunk (A, P, N)
# define check_malloced_chunk(A, P, N) do_check_malloced_chunk (A, P, N)
# define check_malloc_state(A) do_check_malloc_state (A)
...

之后定义加了”do_”的函数名,所以如果没有设置MALLOC_DEBUG是无法通过check_remalloc_chunk找到do_check_remalloc_chunk的,就什么都不会执行。

之后进入 smallbins 的判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
...
/*
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)) //判断size
{
idx = smallbin_index (nb); //获取idnex
bin = bin_at (av, idx); //获取bins[index]

if ((victim = last (bin)) != bin) //判断是否为空,把bin->bk赋给victim
{
if (victim == 0) /* initialization check */
malloc_consolidate (av); //整理fastbins中的chunk加入到unsortedbins
else
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim)) //若victim->bk->fd!=victim
{
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
set_inuse_bit_at_offset (victim, nb); //设置P位
bin->bk = bck; //从链表尾取出
bck->fd = bin;

if (av != &main_arena) //设置A位
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk (av, victim, nb); //debug函数
void *p = chunk2mem (victim); //指针指向数据区
alloc_perturb (p, bytes); //如果设置了perturb_type, 则将获取到的chunk初始化为 perturb_type ^ 0xff
return p; //返回chunk
}
}
}
...

若 size 输入 smallbins,则先通过 size 获取 index,再通过 index 获取该分配区对应的 smallbin,检查是否为空,检查是否初始化,检查victim->bk->fd==victim,满足则取出该 chunk 设置相关标识返回。

若 size 满足 largebins,会先将 fastbins 整理到 unsortedbin中,若只是在之前的 bins 中没有找到合适的 chunk 导致分配失败则不执行这段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
/*
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);
}
...

之后才是 malloc 的核心代码,代码量很大就不全贴了需要自查,这段使用了很多循环嵌套,主要目的是处理之前分配失败的请求

malloc.c注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
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.
*/

先遍历查找 unsorted bin

1
2
3
4
5
6
7
8
9
10
11
12
13
...
for (;; )
{
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) //取unsortedbin->bk并判断是否为空
{
bck = victim->bk; //取victim->bk
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0)) //判断size
malloc_printerr (check_action, "malloc(): memory corruption",
chunk2mem (victim), av);
size = chunksize (victim);
...

尝试切割 unsorted bin 中的chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
...
/*
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.
*/

if (in_smallbin_range (nb) && //size在smallbin范围
bck == unsorted_chunks (av) && //unsorted bin中只有一个chunk
victim == av->last_remainder && //该chunk是当前分配区的last_remainder
(unsigned long) (size) > (unsigned long) (nb + MINSIZE)) //该chunk的size大于所需size
{
/* split and reattach remainder */
remainder_size = size - nb; //计算切割后剩余size
remainder = chunk_at_offset (victim, nb); //切割chunk
unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder; //将remainder放回unsorted bin
av->last_remainder = remainder; //修改last_remainder
remainder->bk = remainder->fd = unsorted_chunks (av); //remainder的fd和bk指针指向unsorted bin
if (!in_smallbin_range (remainder_size)) //若remainder的size属于largebins,因为不在largebins中,清空fd_nextsize和bk_nextsize
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}

set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0)); //给victim设置chunk结构
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size); //给remainder设置chunk结构

check_malloced_chunk (av, victim, nb); //debug函数
void *p = chunk2mem (victim); //获取指向数据区的指针
alloc_perturb (p, bytes); //如果设置了perturb_type, 则将获取到的chunk初始化为 perturb_type ^ 0xff
return p;
}
...

若不满足切割条件则尝试精准匹配 unsorted bin,遍历 unsorted bin,精准匹配成功则返回,匹配失败则将该 chunk 整理到对应的 bin 中,所以若遍历之后都没有精准匹配 unsorted bin 就会被清空。

取出 unsorted bin 中最后一个 chunk,即之前的 victim,这里没有使用 unlink,unsorted bin attack 的漏洞就在这里

1
2
3
4
5
...
/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
...

若实现精准匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
/* Take now instead of binning if exact fit */

if (size == nb) //若精准匹配
{
set_inuse_bit_at_offset (victim, size); //设置P位
if (av != &main_arena) //根据是否是主分配区设置A位
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk (av, victim, nb); //debug函数
void *p = chunk2mem (victim); //获取指向数据区的指针
alloc_perturb (p, bytes); //如果设置了perturb_type, 则将获取到的chunk初始化为 perturb_type ^ 0xff
return p; //返回指向数据区的指针
}
...

若不能精准匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
...
/* place chunk in bin */

if (in_smallbin_range (size)) //size属于smallbins
{
victim_index = smallbin_index (size); //获取index
bck = bin_at (av, victim_index); //获取bins[index]
fwd = bck->fd; //获取bins[index]->fd
}
else
{
victim_index = largebin_index (size); //获取index
bck = bin_at (av, victim_index); //获取bins[index]
fwd = bck->fd; //获取bins[index]->fd

/* maintain large bins in sorted order */
if (fwd != bck) //largebin不为空则按照size将victim插入largebin的size link中
{
/* 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;
}
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;
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}

mark_bin (av, victim_index); //将victim插入到bin
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

#define MAX_ITERS 10000
if (++iters >= MAX_ITERS) //若寻找次数太多则跳出循环
break;
}
...

不能匹配的情况下若 size 属于smallbin 则加入到对应的 smallbin 中,若 size 属于 largebin 则加入到对应的 largebin 中,不过需要注意 largebin 的fd_nextsizebk_nextsize字段的特殊性,加入时需要额外代码处理这两个字段达到排序目的,这段代码有点怪怪的我没有完全搞懂….

unsorted bin 分配过程图示:

如果到这里都无法找到合适的 chunk,说明请求可能是 large request,或者fastbins、smallbins、unsortedbin都无法满足条件,就会搜索 largebins

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
...
/*
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.
*/

if (!in_smallbin_range (nb)) //判断是否属于smallbin
{
bin = bin_at (av, idx); //获取largebin,idx在之前判断size属于largebin时被赋值

/* skip scan if empty or largest chunk is too small */
if ((victim = first (bin)) != bin && //bin非空
(unsigned long) (victim->size) >= (unsigned long) (nb)) //size小于链表中的max size
{
victim = victim->bk_nextsize; //从最小的chunk开始
while (((unsigned long) (size = chunksize (victim)) <
(unsigned long) (nb))) //按照size从小到大依次寻找
victim = victim->bk_nextsize;

/* Avoid removing the first entry for a size so that the skip
list does not have to be rerouted. */
if (victim != last (bin) && victim->size == victim->fd->size) //避免删掉作为长度标识的chunk
victim = victim->fd;

remainder_size = size - nb; //计算切割后的神予size
unlink (av, victim, bck, fwd); //将该chunk unlink

/* Exhaust */
if (remainder_size < MINSIZE) //切割后剩余大小不足以单独使用则不切割全部分配
{
set_inuse_bit_at_offset (victim, size); //设置P位
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA; //设置A位
}
/* 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)) //unsortedbin链表检查
{
errstr = "malloc(): corrupted unsorted chunks";
goto errout;
}
remainder->bk = bck; //将切割剩余部分放入unsortedbin
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;
if (!in_smallbin_range (remainder_size)) //若remainder的size属于largebins,因为不在largebins中,清空fd_nextsize和bk_nextsize
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0)); //设置victim的chunk结构
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size); //设置remainder的chunk结构
}
check_malloced_chunk (av, victim, nb); //debug函数
void *p = chunk2mem (victim); //获取指向数据区的指针
alloc_perturb (p, bytes); //如果设置了perturb_type, 则将获取到的chunk初始化为 perturb_type ^ 0xff
return p; //返回指向数据区的指针
}
}
...

从该 largebin 中按照尺寸大小寻找匹配的 chunk,若有相同大小则尽量不返回其中排在最前面的作为尺寸标识的那一个,找到后如果可以分割,剩下的 chunk 还可以使用就分割,否则全部返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
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.
*/

++idx;
bin = bin_at (av, idx);
block = idx2block (idx);
map = av->binmap[block];
bit = idx2bit (idx);

若在该 largebin 没有找到合适的 chunk,找下一个 largebin,查询 binmap 表,binmap 表的作用是标识每一个 bin 是否为空,之前 main_arena 中讲过,用 binmap 标识可以加快查找速度。

binmap 的大致原理。binmap 一共 128 bit,16 个字节,分成 4 个 int 变量,每一个 int 变量称为一个 block,每个 block 有 32 个 bit,最多可以表示 32 个 bin 的状态,使用宏 idx2block 可以计算出一个 index(bin) 在 binmap 中属于哪个 block。 idx2bit 宏取第 i 位为1,剩下的置 0,例如 idx2bit(2) 会生成 “00000000000000000000000000000100”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
   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); //遍历binmap表找有空闲chunk的largebin

bin = bin_at (av, (block << BINMAPSHIFT));
bit = 1;
}

/* Advance to bin with set bit. There must be one. */
while ((bit & map) == 0)
{
bin = next_bin (bin);
bit <<= 1;
assert (bit != 0);
}

/* Inspect the bin. It is likely to be non-empty */
victim = last (bin);

/* 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;
}

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;
}
}

前面部分的代码是用 binmap 快速查询,后面的代码和之前一样。

largebin 分配过程图示:

largebin 的分配过程是最复杂的,原因是largebin chunk的结构本身就更复杂,还有之前在 fastbin、smallbin、unsorted bin的分配都失败了,但是之前分配时对 chunk 进行了整理后可能就出现了合适的 chunk,为了减少对操作系统申请内存的次数在这里就要尽量把可能的 chunk 全部检查一遍。

若到这里都不能成功分配 chunk,就使用 top chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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; //获取top chunk的起始地址
size = chunksize (victim); //获取top chunk 的大小

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)) //不够但还有fastbins就整理fastbins再尝试
{
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 //fastbins也空了就调用sysmalloc
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}
}

尝试在 top chunk 分配,若 top chunk 足够则直接切割,若不够但还有 fastbin 就执行 malloc_consolidate整理 fastbin 之后再尝试,fastbin 也没有则调用 sysmalloc 申请新的空间。

为什么还要检查 fastbin? 两个原因,一是如果开启了 ATOMIC_FASTBINS ,由于 free fastbin chunk 的时候不需要加锁,所以 malloc 走到这一步的时候可能已经有其他线程向 fastbin 中注入了新的 chunk,另外一个原因是如果 nb 是一个 smallbin chunk,走到这一步说明之前所有的分配操作都失败了,但是在分配 smallbin chunk 的时候始终都没有调用过 malloc_consolidate,所以在 malloc 尾声的时候可以尝试合并 fastbin chunk 构造出符合要求的 chunk。

总结

  1. 获取分配区的锁。
  2. 将用户的请求大小转换为实际需要分配的 chunk 空间大小。
  3. 判断所需分配 chunk 是否在 fastbin 区域,如果是的话, 则转下一步,否则跳到第 5 步。
  4. 首先尝试在 fastbins 中取一个所需大小的 chunk 分配给用户。 如果可以找到, 则分配结束。 否则转到下一步。
  5. 判断所需大小是否处在 small bins 中,如果 chunk 大小处在 smallbins 中,则转下一步,否则转到第 7 步。
  6. 根据所需分配的 chunk 的大小, 找到具体所在的某个 smallbin,从该 bin 的尾部摘取一个恰好满足大小的 chunk。 若成功,则分配结束,否则转到下一步。
  7. 到了这一步, 说明需要分配的是一块大的内存,或者 small bins 中找不到合适的chunk。于是,ptmalloc 首先会遍历 fastbins 中的 chunk,将相邻的 chunk 进行合并,并链接到 unsorted bin 中, 然后遍历 unsorted bin 中的 chunk,如果 unsorted bin 只有一个 chunk,并且这个 chunk 在上次分配时被使用过,并且所需分配的 chunk 大小属于 small bins,并且 chunk 的大小大于等于需要分配的大小,这种情况下就直接将该 chunk 进行切割,分配结束,否则将根据 chunk 的空间大小将其放入 smallbins 或是 large bins 中,遍历完成后,转入下一步。
  8. 到了这一步,说明需要分配的是一块大的内存,或者 small bins 和 unsorted bin 中都找不到合适的 chunk,并且 fast bins 和 unsorted bin 中所有的 chunk 都清除干净了。 从 large bins 中按照“smallest-first, best-fit”原则, 找一个合适的 chunk, 从中划分一块所需大小的 chunk, 并将剩下的部分链接回到 bins 中。 若操作成功, 则分配结束, 否则转到下一步。
  9. 如果搜索 fast bins 和 bins 都没有找到合适的 chunk, 那么就需要操作 top chunk 来进行分配了。 判断 top chunk 大小是否满足所需 chunk 的大小, 如果是, 则从 topchunk 中分出一块来。 否则转到下一步。
  10. 到了这一步, 说明 top chunk 也不能满足分配要求, 所以, 于是就有了两个选择: 如果是主分配区, 调用 sbrk(), 增加 top chunk 大小; 如果是非主分配区,调用 mmap来分配一个新的 sub-heap,增加 top chunk 大小; 或者使用 mmap()来直接分配。 在这里, 需要依靠 chunk 的大小来决定到底使用哪种方法。 判断所需分配的 chunk大小是否大于等于 mmap 分配阈值, 如果是的话, 则转下一步, 调用 mmap 分配,否则跳到第 12 步, 增加 top chunk 的大小。
  11. 使用 mmap 系统调用为程序的内存空间映射一块 chunk_size align 4kB 大小的空间。然后将内存指针返回给用户。
  12. 判断是否为第一次调用 malloc, 若是主分配区, 则需要进行一次初始化工作, 分配一块(chunk_size + 128KB) align 4KB 大小的空间作为初始的 heap。 若已经初始化过了, 主分配区则调用 sbrk()增加 heap 空间, 分主分配区则在 top chunk 中切割出一个 chunk, 使之满足分配需求, 并将内存指针返回给用户。