前言

本文通过为 glibc 2.23 malloc 源码添加注解的形式,详细阐述了程序第一次 malloc 时,从进入 malloc 函数到返回一个地址的过程中都经历了什么,对于高版本的 ptmalloc 也可以按照本文的思路和方法来调试分析。这里默认读者具备 malloc 系统的基本概念,知晓 malloc_chunk、malloc_state、malloc_par 等常见堆相关结构体的字段含义


环境

env

测试代码

1
2
3
4
5
6
7
8
// gcc -m32 -no-pie -o test test.c
#include <stdlib.h>
#include <stdio.h>

void main()
{
malloc(20);
}

推荐下载 glibc 2.23 源码,对照 malloc/ 目录下文件来阅读本文,下面进入正片


流程跟踪

malloc.c 中无法找到 malloc 的直接实现,只有 __libc_malloc,而 malloc 实际上是它的别名:

1
strong_alias (__libc_malloc, __malloc) strong_alias (__libc_malloc, malloc)

调用 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
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))
return (*hook)(bytes, RETURN_ADDRESS (0));

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

由于全局变量 __malloc_hook 被赋予初值 malloc_hook_ini:

1
2
void *weak_variable (*__malloc_hook)
(size_t __size, const void *) = malloc_hook_ini;

所以在 __libc_malloc 开头的检测中,会调用 malloc_hook_ini 并返回:

1
2
3
4
void *(*hook) (size_t, const void *)
= atomic_forced_read (__malloc_hook); // hook = __malloc_hook = malloc_hook_ini
if (__builtin_expect (hook != NULL, 0)) // if(hook != NULL)
return (*hook)(bytes, RETURN_ADDRESS (0)); // 调用 malloc_hook_ini,传入申请的 bytes 和返回到调用者(main 函数)的地址

malloc_hook_ini 函数:

1
2
3
4
5
6
7
static void *
malloc_hook_ini (size_t sz, const void *caller)
{
__malloc_hook = NULL; // 将 __malloc_hook 置为空,后面再调用 malloc 就不会再进到这个函数了
ptmalloc_init (); // 调用 ptmalloc_init 进行初始化
return __libc_malloc (sz); // 初始化完成后,再次调用 __libc_malloc
}

ptmalloc_init 函数:

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
static void
ptmalloc_init (void)
{
if (__malloc_initialized >= 0)
return;

__malloc_initialized = 0;

#ifdef SHARED
/* In case this libc copy is in a non-default namespace, never use brk.
Likewise if dlopened from statically linked program. */
Dl_info di;
struct link_map *l;

if (_dl_open_hook != NULL
|| (_dl_addr (ptmalloc_init, &di, &l, NULL) != 0
&& l->l_ns != LM_ID_BASE))
__morecore = __failing_morecore;
#endif

thread_arena = &main_arena;
const char *s = NULL;
if (__glibc_likely (_environ != NULL))
{
char **runp = _environ;
char *envline;

while (__builtin_expect ((envline = next_env_entry (&runp)) != NULL,
0))
{
size_t len = strcspn (envline, "=");

if (envline[len] != '=')
/* This is a "MALLOC_" variable at the end of the string
without a '=' character. Ignore it since otherwise we
will access invalid memory below. */
continue;

switch (len)
{
case 6:
if (memcmp (envline, "CHECK_", 6) == 0)
s = &envline[7];
break;
case 8:
if (!__builtin_expect (__libc_enable_secure, 0))
{
if (memcmp (envline, "TOP_PAD_", 8) == 0)
__libc_mallopt (M_TOP_PAD, atoi (&envline[9]));
else if (memcmp (envline, "PERTURB_", 8) == 0)
__libc_mallopt (M_PERTURB, atoi (&envline[9]));
}
break;
case 9:
if (!__builtin_expect (__libc_enable_secure, 0))
{
if (memcmp (envline, "MMAP_MAX_", 9) == 0)
__libc_mallopt (M_MMAP_MAX, atoi (&envline[10]));
else if (memcmp (envline, "ARENA_MAX", 9) == 0)
__libc_mallopt (M_ARENA_MAX, atoi (&envline[10]));
}
break;
case 10:
if (!__builtin_expect (__libc_enable_secure, 0))
{
if (memcmp (envline, "ARENA_TEST", 10) == 0)
__libc_mallopt (M_ARENA_TEST, atoi (&envline[11]));
}
break;
case 15:
if (!__builtin_expect (__libc_enable_secure, 0))
{
if (memcmp (envline, "TRIM_THRESHOLD_", 15) == 0)
__libc_mallopt (M_TRIM_THRESHOLD, atoi (&envline[16]));
else if (memcmp (envline, "MMAP_THRESHOLD_", 15) == 0)
__libc_mallopt (M_MMAP_THRESHOLD, atoi (&envline[16]));
}
break;
default:
break;
}
}
}
if (s && s[0])
{
__libc_mallopt (M_CHECK_ACTION, (int) (s[0] - '0'));
if (check_action != 0)
__malloc_check_init ();
}
void (*hook) (void) = atomic_forced_read (__malloc_initialize_hook);
if (hook != NULL)
(*hook)();
__malloc_initialized = 1;
}

__malloc_initialized 是一个用于表示初始化是否完成的变量,默认被赋值为 -1,在 ptmalloc_init 开头部分赋值为 0,初始化完成后赋值为 1,故该函数也只会在初始化过程中被调用一次。接着设置线程的 thread_arena 为 &main_arena,然后通过一个 while 循环遍历所有进程的环境变量,进行内存分配策略的控制(mallopt)。最后当 __malloc_initialize_hook 不为 NULL 时,调用该函数(不过该值默认为 NULL),返回到 malloc_hook_ini,再次调用 __libc_malloc 完成剩余的初始化工作、处理用户的需求

这次 __libc_malloc 由于 __malloc_hook 已被置为 NULL,故顺序往下执行,通过 arena_get 取来当前线程的 arena,并为其上锁:

1
2
3
4
#define arena_get(ptr, size) do { \
ptr = thread_arena; \ // 这里取到的就是 main_arena 旳地址
arena_lock (ptr, size); \ // 将 (struct malloc_state *)ptr->mutex 设置为 1
} while (0)

接着将 arena 地址和所申请的字节数传递给 _int_malloc,该函数才是真正在做内存分配的地方,当找到合适的空间时,该函数会返回这段空间的用户可见首地址(chunk2mem),放在变量 victim 中

下面来跟一下 _int_malloc(函数太长,就不完整贴出来了),首先根据用户需要的 bytes 计算出 chunk 的实际大小,并判断 arena 是否为空:

1
2
3
4
5
6
7
8
9
10
 checked_request2size (bytes, nb);	// 计算结果放在 nb,这里是 24

// av = NULL 说明没有可用的 arena 了,需要通过 sysmalloc 从 mmap 分配 chunk
if (__glibc_unlikely (av == NULL))
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}

但这个时候 av 是等于 &main_arena 的,所以接着往下,判断申请的 chunk 大小是否在 fastbin 范围内:

1
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))

此时 global_max_fast 还没有被初始化(get_max_fast 得到 0),显然不会进到 fastbin 中去寻找空闲 chunk,接着判断大小是否在 smallbin 中,24 小于 512,满足条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb); // 取 24 在 smallbin 中的下标,idx = 3
bin = bin_at (av, idx); // 取 bin3 地址

if ((victim = last (bin)) != bin) // bin->bk = 0 != bin,满足条件,此时 victim = 0
{
if (victim == 0) // 说明初始化未彻底完成
malloc_consolidate (av); // 调用 malloc_consolidate
else
...
}
}

malloc_consolidate 上来就判断 global_max_fast 是否为 0,为 0 表明初始化工作还未完成,进 else 里调用 malloc_init_state:

1
2
3
4
5
6
7
if (get_max_fast () != 0) {
...
}
else {
malloc_init_state(av);
check_malloc_state(av); // 非 DEBUG 版本,没有定义 check_malloc_state,这里的调用不会发生
}

malloc_init_state 主要初始化 arena 的 flags、bins、top 字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void
malloc_init_state (mstate av)
{
int i;
mbinptr bin;

for (i = 1; i < NBINS; ++i) // 初始化所有 bin 的 fd 和 bk 都指向自己
{
bin = bin_at (av, i);
bin->fd = bin->bk = bin;
}

#if MORECORE_CONTIGUOUS
if (av != &main_arena)
#endif
set_noncontiguous (av); // av = &main_arena,不会设置该标志位
if (av == &main_arena)
set_max_fast (DEFAULT_MXFAST); // 设置 global_max_fast 为 64
av->flags |= FASTCHUNKS_BIT; // 设置标志位 FASTCHUNKS_BIT,表示 fastbin 中当前没有 fastchunk

av->top = initial_top (av); // 设置 top chunk 为 bin1
}

然后回到 _int_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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
for (;; )
{
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) // unsorted bin 中没有 chunk,跳出
{
...
}
if (!in_smallbin_range (nb)) // 不满足条件
{
...
}
++idx;
bin = bin_at (av, idx);
block = idx2block (idx);
map = av->binmap[block]; // 此时 av->binmap 数组为全零
bit = idx2bit (idx);

for (;; ) // 使用 binmap 找寻满足需求的最小空闲块
{
if (bit > map || bit == 0)
{
do
{
if (++block >= BINMAPSIZE) /* out of bins */
goto use_top; // 会执行到这里来,去到 use_top 标签
}
while ((map = av->binmap[block]) == 0);
...
}
...
}

use_top:
victim = av->top;
size = chunksize (victim); // 此时 top chunk 的大小为 0

if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE)) // size 为 0
{
...
}
else if (have_fastchunks (av)) // 没有 fastchunk
{
...
}
else // 进到这个判断
{
void *p = sysmalloc (nb, av); // 通过 sysmalloc 申请空间来设置 top chunk,并获得用户需求的 chunk 地址
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}

sysmalloc 也很长,我们只关心核心的申请代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
old_top = av->top;
old_size = chunksize (old_top); // old_size = 0
old_end = (char *) (chunk_at_offset (old_top, old_size));
...
if (av != &main_arena)
{
...
}
else // 进入 else
{ /* Request enough space for nb + pad + overhead */
size = nb + mp_.top_pad + MINSIZE; // top_pad 默认为 0x20000,size = 0x20028
if (contiguous (av))
size -= old_size; // size -= 0
size = ALIGN_UP (size, pagesize); // 内存页对齐,size = 0x21000
if (size > 0)
{
brk = (char *) (MORECORE (size)); // 调用 sbrk,申请 0x21000 的空间
LIBC_PROBE (memory_sbrk_more, 2, brk, size); // 无事发生
}

MORECORE 默认赋值为 __default_morecore,该函数通过 sbrk 来增加 brk:

1
2
3
4
5
6
7
8
9
void *
__default_morecore (ptrdiff_t increment)
{
void *result = (void *) __sbrk (increment);
if (result == (void *) -1)
return NULL;

return result;
}

MORECORE 之前地址映射关系:

before

执行后多出了一个大小为 0x21000 的内存区域:

after

接着看 sysmalloc,现在已经通过 MORECORE 申请到了堆空间,需要做一些扫尾工作:

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
    if (brk != (char *) (MORECORE_FAILURE))	// brk != 0
{
//
void (*hook) (void) = atomic_forced_read (__after_morecore_hook);
if (__builtin_expect (hook != NULL, 0)) // 该 hook 默认为 NULL,不调用
(*hook)();
}
else
{
...
}
if (brk != (char *) (MORECORE_FAILURE))
{
if (mp_.sbrk_base == 0) // 此时 sbrk_base 为 0
mp_.sbrk_base = brk; // 设置 sbrk_base 为堆起始地址
av->system_mem += size; // 该 arena 拥有的 system_mem 加上 size

if (brk == old_end && snd_brk == (char *) (MORECORE_FAILURE)) // brk != old_end
set_head (old_top, (size + old_size) | PREV_INUSE);

else if (contiguous (av) && old_size && brk < old_end) // old_size = 0
{
/* Oops! Someone else killed our space.. Can't touch anything. */
malloc_printerr (3, "break adjusted to free malloc space", brk,
av);
}
else // 进入 else
{
front_misalign = 0;
end_misalign = 0;
correction = 0;
aligned_brk = brk;

if (contiguous (av))
{
if (old_size) // old_size = 0
av->system_mem += brk - old_end;
// 计算 correction 的值
...
snd_brk = (char *) (MORECORE (correction)); // correction 一般为 0,这一步的目的是使 snd_brk = 堆结束地址
if (snd_brk == (char *) (MORECORE_FAILURE))
{
...
}
else // 进入 else
{
// __after_morecore_hook 默认为 NULL
void (*hook) (void) = atomic_forced_read (__after_morecore_hook);
if (__builtin_expect (hook != NULL, 0))
(*hook)();
}
}
else
{
...
}
if (snd_brk != (char *) (MORECORE_FAILURE)) // 满足条件
{
av->top = (mchunkptr) aligned_brk; // av->top = brk
set_head (av->top, (snd_brk - aligned_brk + correction) | PREV_INUSE); // 设置 top chunk 头部
av->system_mem += correction;
...
}
}
}
} /* if (av != &main_arena) */
if ((unsigned long) av->system_mem > (unsigned long) (av->max_system_mem))
av->max_system_mem = av->system_mem; // 设置 max_system_mem

p = av->top;
size = chunksize (p); // size = 0x21000
// 最后在这里做分配,满足用户的第一次申请
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb; // 分出去 nb 还剩多少
remainder = chunk_at_offset (p, nb);
av->top = remainder; // 重新设置 top chunk 地址
set_head (p, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE); // 设置它们的头
check_malloced_chunk (av, p, nb); // 无事发生
return chunk2mem (p); // 返回用户可见地址
}

/* catch all failure paths */
__set_errno (ENOMEM);
return 0;

sysmalloc 返回后回到 _int_malloc,再从不远处的 return p; 返回到 __libc_malloc,还记得吗:

1
2
3
4
5
6
7
8
9
10
11
if (!victim && ar_ptr != NULL)	// victim 此时为 heap 起始地址 + 8
{
...
}

if (ar_ptr != NULL) // 之前对 thread_arena 上锁了,这里解锁
(void) mutex_unlock (&ar_ptr->mutex);

assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
ar_ptr == arena_for_chunk (mem2chunk (victim)));
return victim; // 返回用户期待的空间

最后返回到 __malloc_hook,再返回到最初调用的 __libc_malloc,因为 __libc_malloc 中调用 __malloc_hook 后会直接返回,所以用户就拿到了申请的空间。至此,第一次 malloc 调用全流程分析完毕。


流程总结

  • __libc_malloc
    • __malloc_hook(malloc_hook_ini)
      • ptmalloc_init:根据环境变量配置内存分配选项
      • __libc_malloc
        • arena_get:获取线程的 malloc_state 结构体地址,作为 _int_malloc 的一个参数
        • _int_malloc
          • malloc_consolidate:进入 else 分支
            • malloc_init_state:初始化 malloc_state 结构体的一些字段
          • sysmalloc(use top):正式申请、设置 top chunk,分割 top chunk,返回满足用户申请大小的空间
        • 返回到 __libc_malloc
      • 返回到 __malloc_hook
    • 返回到 __libc_malloc
  • 返回申请的空间

PS. 如果第一次 malloc 的大小超出了 smallbin 的范围,怎么调用 malloc_consolidate 来初始化?
答:还是可以在 _int_malloc 的大循环前调用 malloc_consolidate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  if (in_smallbin_range (nb))
{
... // nb 在 smallbin 范围内时,在这里调用 malloc_consolidate
}
else
{
idx = largebin_index (nb);
if (have_fastchunks (av)) // 此时 av->flags 为 0,判断成立
malloc_consolidate (av); // nb 超出 smallbin 范围,在这里调用 malloc_consolidate
}

// 关于 have_fastchunks
#define FASTCHUNKS_BIT (1U)
#define have_fastchunks(M) (((M)->flags & FASTCHUNKS_BIT) == 0)

参考资料

gdb 源码级调试 malloc:https://moddemod.blog.csdn.net/article/details/106580966