计算机系统篇之虚拟内存(9):理解 glibc malloc 的工作原理(中)
Author: stormQ
Created: Wednesday, 25. November 2020 10:52PM
Last Modified: Sunday, 13. December 2020 04:38PM
本文结合 glibc-2.31 版本中的 malloc 的实现源码来分析上一篇中可执行目标文件 vm4_main 动态申请和释放堆内存的过程,并重点分析了 tcache 机制。
本文我们结合 glibc-2.31 版本中的malloc
的实现源码来分析上一篇中可执行目标文件vm4_main
动态申请和释放堆内存的过程。
step 0: 启动 vm4_main 并挂载 glibc-2.31 版本的源码
$ gdb -q vm4_main
Reading symbols from vm4_main...
(gdb) start
Temporary breakpoint 1 at 0x11a9: file vm4_main.cpp, line 37.
Starting program: /home/test/vm/vm4_main
Temporary breakpoint 1, main (argc=0, argv=0x0) at vm4_main.cpp:37
37 {
(gdb) directory /home/workspace/git-projects/glibc-2.31/malloc
Source directories searched: /home/workspace/git-projects/glibc-2.31/malloc:$cdir:$cwd
注:/home/workspace/git-projects/glibc-2.31/malloc
为malloc.c
所在的目录。
step 1: 研究 obj1 对象申请堆内存的过程
需要注意的是,虽然obj1
对象作为用户第一次发起的堆内存申请,但在真正为其分配chunk
之前会创建另外一个chunk
,作为事实上的第一个(位于堆底)chunk
,具体分析过程见本文中的 第一个 chunk 的来龙去脉。
1) 进入malloc
函数
(gdb) b malloc
Breakpoint 2 at 0x7ffff7e61260: malloc. (2 locations)
(gdb) c
Continuing.
Breakpoint 2, __GI___libc_malloc (bytes=8) at malloc.c:3023
3023 {
(gdb) bt
#0 __GI___libc_malloc (bytes=8) at malloc.c:3023
#1 0x00005555555552f3 in HeapObject::HeapObject (this=0x7fffffffdc40, size=8) at vm4_main.cpp:11
#2 0x00005555555551dd in main (argc=1, argv=0x7fffffffdda8) at vm4_main.cpp:38
从上面的输出结果中可以看出,我们正在跟踪的是obj1
对象申请堆内存的过程,并且malloc
的底层实现函数的名称为__GI___libc_malloc
。
2) 分析obj1
对象申请堆内存时,实际调用了malloc
函数的哪些代码
a)查看malloc
函数完整的实现源码
(gdb) l malloc.c:3021, malloc.c:3082
3021 void *
3022 __libc_malloc (size_t bytes)
3023 {
3024 mstate ar_ptr;
3025 void *victim;
3026
3027 _Static_assert (PTRDIFF_MAX <= SIZE_MAX / 2,
3028 "PTRDIFF_MAX is not more than half of SIZE_MAX");
3029
3030 void *(*hook) (size_t, const void *)
3031 = atomic_forced_read (__malloc_hook);
3032 if (__builtin_expect (hook != NULL, 0))
3033 return (*hook)(bytes, RETURN_ADDRESS (0));
3034 #if USE_TCACHE
3035 /* int_free also calls request2size, be careful to not pad twice. */
3036 size_t tbytes;
3037 if (!checked_request2size (bytes, &tbytes))
3038 {
3039 __set_errno (ENOMEM);
3040 return NULL;
3041 }
3042 size_t tc_idx = csize2tidx (tbytes);
3043
3044 MAYBE_INIT_TCACHE ();
3045
3046 DIAG_PUSH_NEEDS_COMMENT;
3047 if (tc_idx < mp_.tcache_bins
3048 && tcache
3049 && tcache->counts[tc_idx] > 0)
3050 {
3051 return tcache_get (tc_idx);
3052 }
3053 DIAG_POP_NEEDS_COMMENT;
3054 #endif
3055
3056 if (SINGLE_THREAD_P)
3057 {
3058 victim = _int_malloc (&main_arena, bytes);
3059 assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
3060 &main_arena == arena_for_chunk (mem2chunk (victim)));
3061 return victim;
3062 }
3063
3064 arena_get (ar_ptr, bytes);
3065
3066 victim = _int_malloc (ar_ptr, bytes);
3067 /* Retry with another arena only if we were able to find a usable arena
3068 before. */
3069 if (!victim && ar_ptr != NULL)
3070 {
3071 LIBC_PROBE (memory_malloc_retry, 1, bytes);
3072 ar_ptr = arena_get_retry (ar_ptr, bytes);
3073 victim = _int_malloc (ar_ptr, bytes);
3074 }
3075
3076 if (ar_ptr != NULL)
3077 __libc_lock_unlock (ar_ptr->mutex);
3078
3079 assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
3080 ar_ptr == arena_for_chunk (mem2chunk (victim)));
3081 return victim;
3082 }
b)设置一些断点,并观察这些断点的执行情况
(gdb) b malloc.c:3033
Breakpoint 3 at 0x7ffff7e60dfb: malloc.c:3033. (2 locations)
(gdb) b malloc.c:3037
Breakpoint 4 at 0x7ffff7e60cc8: malloc.c:3037. (2 locations)
(gdb) b malloc.c:3051
Breakpoint 5 at 0x7ffff7e60dc0: malloc.c:3051. (2 locations)
(gdb) b malloc.c:3056
Breakpoint 6 at 0x7ffff7e60d03: malloc.c:3056. (3 locations)
(gdb) b malloc.c:3058
Breakpoint 7 at 0x7ffff7e60d13: malloc.c:3058. (2 locations)
(gdb) b malloc.c:3066
Breakpoint 8 at 0x7ffff7e60e6e: malloc.c:3066. (2 locations)
(gdb) b vm4_main.cpp:39
Breakpoint 9 at 0x5555555551dd: file vm4_main.cpp, line 39.
需要注意的是,我们在vm4_main.cpp:39
处也设置了断点,以便于区分接下来的malloc
的调用过程确实是由obj1
对象申请堆内存引起的。
c)继续执行,直到vm4_main.cpp:39
处停止
(gdb) c
Continuing.
Breakpoint 3, __GI___libc_malloc (bytes=8) at malloc.c:3033
3033 return (*hook)(bytes, RETURN_ADDRESS (0));
(gdb) c
Continuing.
Breakpoint 4, checked_request2size (sz=<synthetic pointer>, req=8) at malloc.c:3037
3037 if (!checked_request2size (bytes, &tbytes))
(gdb) c
Continuing.
Breakpoint 6, __GI___libc_malloc (bytes=8) at malloc.c:3056
3056 if (SINGLE_THREAD_P)
(gdb) c
Continuing.
Breakpoint 6, __GI___libc_malloc (bytes=8) at malloc.c:3056
3056 if (SINGLE_THREAD_P)
(gdb) c
Continuing.
Breakpoint 7, __GI___libc_malloc (bytes=8) at malloc.c:3058
3058 victim = _int_malloc (&main_arena, bytes);
(gdb) c
Continuing.
Breakpoint 9, main (argc=1, argv=0x7fffffffdda8) at vm4_main.cpp:39
39 HeapObject obj2(4);
从上面的输出结果中可以看出,在obj1
对象申请一块堆内存的过程中,malloc
函数中的以下代码部分依次被调用了,分别为:
malloc.c:3033 -> malloc.c:3037 -> malloc.c:3056 -> malloc.c:3056 -> malloc.c:3058
因此,可以得出结论:obj1
对象申请堆内存时,实际调用的malloc
函数的代码部分如下:
3021 void *
3022 __libc_malloc (size_t bytes)
3023 {
// 省略...
3025 void *victim;
3026
3027 _Static_assert (PTRDIFF_MAX <= SIZE_MAX / 2,
3028 "PTRDIFF_MAX is not more than half of SIZE_MAX");
3029
3030 void *(*hook) (size_t, const void *)
3031 = atomic_forced_read (__malloc_hook);
3032 if (__builtin_expect (hook != NULL, 0))
3033 return (*hook)(bytes, RETURN_ADDRESS (0));
3034 #if USE_TCACHE
3035 /* int_free also calls request2size, be careful to not pad twice. */
3036 size_t tbytes;
3037 if (!checked_request2size (bytes, &tbytes))
3038 {
3039 __set_errno (ENOMEM);
3040 return NULL;
3041 }
// 省略...
3054 #endif
3055
3056 if (SINGLE_THREAD_P)
3057 {
3058 victim = _int_malloc (&main_arena, bytes);
3059 assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
3060 &main_arena == arena_for_chunk (mem2chunk (victim)));
3061 return victim;
3062 }
// 省略...
3082 }
上述代码通过将没什么影响的部分去掉,从而简化了我们的分析过程。这里,有以下几个疑问:
局部变量victim
(数据类型为:void *
)的作用?
局部变量hook
(函数指针)的作用?
checked_request2size (bytes, &tbytes)
的作用?
main_arena
的作用?
_int_malloc (&main_arena, bytes);
的作用?
接下来,逐一研究这些问题。
3) malloc
函数中,局部变量victim
(数据类型为:void *
)的作用?
通过源码可以很容易地看出,局部变量victim
即为__libc_malloc
函数的返回值,意味着该变量指向用户所申请堆内存的起始位置。
接下来,通过调试直观地观察下。
a)执行完 malloc.c:3058 行后,查看局部变量victim
的值
(gdb) c
Continuing.
Breakpoint 7, __GI___libc_malloc (bytes=8) at malloc.c:3058
3058 victim = _int_malloc (&main_arena, bytes);
(gdb) n
3059 assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
(gdb) p victim
$2 = (void *) 0x5555555592a0
b)继续单步执行,查看数据成员data_
的值
(gdb) n
HeapObject::HeapObject (this=0x7fffffffdc40, size=8) at vm4_main.cpp:12
12 if (data_)
(gdb) p/x data_
$3 = 0x5555555592a0
(gdb) bt
#0 HeapObject::HeapObject (this=0x7fffffffdc40, size=8) at vm4_main.cpp:12
#1 0x00005555555551dd in main (argc=1, argv=0x7fffffffdda8) at vm4_main.cpp:38
从上面的结果中可以看出,局部变量victim
的值和数据成员data_
的值相等,都是 0x5555555592a0。
因此,malloc
函数中局部变量victim
的作用为:用于指向用户所申请堆内存的起始位置。
4) malloc 函数中,局部变量 hook(函数指针)的作用?
malloc
函数中局部变量hook
(函数指针)的作用:用于保存一个钩子函数的地址。这个钩子函数用于真正地分配堆内存。另外,我们可以在链接期替换钩子函数的默认实现,即将标准库中的malloc
函数实现替换成我们自己的。具体分析过程,见本文中的 如何在链接期拦截标准库的 malloc。
5)malloc
函数中,checked_request2size (bytes, &tbytes)
的作用?
checked_request2size (bytes, &tbytes)
的作用为:确定chunk
的大小。具体分析过程,见本文中的 chunk 的大小有哪些讲究。
6) malloc
函数中,main_arena
的作用?
a)查看main_arena
的定义(在 malloc.c 中)
/* There are several instances of this struct ("arenas") in this
malloc. If you are adapting this malloc in a way that does NOT use
a static or mmapped malloc_state, you MUST explicitly zero-fill it
before using. This malloc relies on the property that malloc_state
is initialized to all zeroes (as is true of C statics). */
static struct malloc_state main_arena =
{
.mutex = _LIBC_LOCK_INITIALIZER,
.next = &main_arena,
.attached_threads = 1
};
从上面的结果中可以看出,main_arena
是一个数据类型为struct malloc_state
的静态全局变量。
b)查看结构体malloc_state
的定义(在 malloc.c 中)
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;
/* 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;
};
从上面的结果中可以看出,结构体malloc_state
的数据成员很多,逐一研究的话很容易懵圈。那么我们可以优先研究那些在obj1
对象申请堆内存前后值发生变化的字段。
c)在obj1
对象申请堆内存前后,main_arena
对象中的哪些字段的值发生了变化?
执行到 malloc.c:3023 行时,查看静态全局变量main_arena
的值:
(gdb) c
Continuing.
Breakpoint 2, __GI___libc_malloc (bytes=8) at malloc.c:3023
3023 {
(gdb) bt
#0 __GI___libc_malloc (bytes=8) at malloc.c:3023
#1 0x00005555555552f3 in HeapObject::HeapObject (this=0x7fffffffdc40, size=8) at vm4_main.cpp:11
#2 0x00005555555551dd in main (argc=1, argv=0x7fffffffdda8) at vm4_main.cpp:38
(gdb) p p main_arena
No symbol "p" in current context.
(gdb) p main_arena
$1 = {mutex = 0, flags = 0, have_fastchunks = 0, fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, top = 0x0, last_remainder = 0x0, bins = {0x0 <repeats 254 times>},
binmap = {0, 0, 0, 0}, next = 0x7ffff7fafb80 <main_arena>, next_free = 0x0, attached_threads = 1, system_mem = 0, max_system_mem = 0}
(gdb) p/x &main_arena
$2 = 0x7ffff7fafb80
从上面的结果中可以看出,main_arena
对象的地址为 0x7ffff7fafb80。在obj1
对象申请堆内存前,main_arena
对象的值为:
{mutex = 0, flags = 0, have_fastchunks = 0, fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, top = 0x0, last_remainder = 0x0, bins = {0x0 <repeats 254 times>},
binmap = {0, 0, 0, 0}, next = 0x7ffff7fafb80 <main_arena>, next_free = 0x0, attached_threads = 1, system_mem = 0, max_system_mem = 0}
执行到 vm4_main.cpp:39 行时,再次查看静态全局变量main_arena
的值:
(gdb)
Continuing.
Breakpoint 9, main (argc=1, argv=0x7fffffffdda8) at vm4_main.cpp:39
39 HeapObject obj2(4);
(gdb) p *((struct malloc_state *)0x7ffff7fafb80)
$4 = {mutex = 0, flags = 0, have_fastchunks = 0, fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, top = 0x5555555592b0, last_remainder = 0x0, bins = {
0x7ffff7fafbe0 <main_arena+96>, 0x7ffff7fafbe0 <main_arena+96>, 0x7ffff7fafbf0 <main_arena+112>, 0x7ffff7fafbf0 <main_arena+112>, 0x7ffff7fafc00 <main_arena+128>, 0x7ffff7fafc00 <main_arena+128>...},
binmap = {0, 0, 0, 0}, next = 0x7ffff7fafb80 <main_arena>, next_free = 0x0, attached_threads = 1, system_mem = 135168, max_system_mem = 135168}
对比obj1
对象申请堆内存前后,main_arena
对象的值。我们可以发现,在obj1
对象申请堆内存后,main_arena
对象中值发生变化的数据成员有:top
、bins
、system_mem
、max_system_mem
。
相应地,这里有如下几个疑问:
结构体malloc_state
中的数据成员top
的作用?
结构体malloc_state
中的数据成员bins
的作用?
结构体malloc_state
中的数据成员system_mem
的作用?
结构体malloc_state
中的数据成员max_system_mem
的作用?
通过分析 struct malloc_state 中各字段的意义,我们可以推断,malloc
函数中main_arena
的作用为:用于管理堆。这里的堆特指狭义上的堆,即通过sbrk
函数进行扩展或伸缩的。
7) malloc
函数中,_int_malloc (&main_arena, bytes);
的作用?
查看_int_malloc (&main_arena, bytes);
被调用的地方:
3021 void *
3022 __libc_malloc (size_t bytes)
3023 {
// 省略...
3056 if (SINGLE_THREAD_P)
3057 {
3058 victim = _int_malloc (&main_arena, bytes);
3059 assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
3060 &main_arena == arena_for_chunk (mem2chunk (victim)));
3061 return victim;
3062 }
// 省略...
3082 }
这里有两个事实:1)main_arena
对象中的内容目前只被_int_malloc
函数修改过;2)_int_malloc
函数的返回值保存在局部变量victim
中。
因此,我们可以先简单地这样理解,malloc
函数中,_int_malloc (&main_arena, bytes);
的作用为:
堆内存不足时,扩展堆
从堆中分配内存给用户,并更新用于管理堆的对象(即arena
)
step 2: 研究 obj2 对象申请堆内存的过程
obj2
对象申请堆内存的过程与obj1
对象的基本相同,这里不再赘述。
我们重点观察下,在obj2
对象申请堆内存后,main_arena
对象的内容变化情况。
执行完 vm4_main.cpp:39 行后,查看main_arena
对象的值:
(gdb) c
Continuing.
Breakpoint 10, main (argc=1, argv=0x7fffffffdda8) at vm4_main.cpp:40
40 HeapObject obj3(64);
(gdb) p *((struct malloc_state *)0x7ffff7fafb80)
$5 = {mutex = 0, flags = 0, have_fastchunks = 0, fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, top = 0x5555555592d0, last_remainder = 0x0, bins = {
0x7ffff7fafbe0 <main_arena+96>, 0x7ffff7fafbe0 <main_arena+96>, 0x7ffff7fafbf0 <main_arena+112>, 0x7ffff7fafbf0 <main_arena+112>, 0x7ffff7fafc00 <main_arena+128>, 0x7ffff7fafc00 <main_arena+128>...},
binmap = {0, 0, 0, 0}, next = 0x7ffff7fafb80 <main_arena>, next_free = 0x0, attached_threads = 1, system_mem = 135168, max_system_mem = 135168}
从上面的结果中可以看出,在obj2
对象申请堆内存后,main_arena
对象中的数据成员top
的值从 0x5555555592b0 变成了 0x5555555592d0,其余字段的值未发生变化。
step 3: 研究 obj3 对象申请堆内存的过程
执行完 vm4_main.cpp:40 行后,查看main_arena
对象的值:
(gdb) c
Continuing.
Breakpoint 11, main (argc=1, argv=0x7fffffffdda8) at vm4_main.cpp:41
41 HeapObject obj4(4);
(gdb) p *((struct malloc_state *)0x7ffff7fafb80)
$6 = {mutex = 0, flags = 0, have_fastchunks = 0, fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, top = 0x555555559320, last_remainder = 0x0, bins = {
0x7ffff7fafbe0 <main_arena+96>, 0x7ffff7fafbe0 <main_arena+96>, 0x7ffff7fafbf0 <main_arena+112>, 0x7ffff7fafbf0 <main_arena+112>, 0x7ffff7fafc00 <main_arena+128>, 0x7ffff7fafc00 <main_arena+128>...},
binmap = {0, 0, 0, 0}, next = 0x7ffff7fafb80 <main_arena>, next_free = 0x0, attached_threads = 1, system_mem = 135168, max_system_mem = 135168}
从上面的结果中可以看出,在obj3
对象申请堆内存后,main_arena
对象中的数据成员top
的值从 0x5555555592d0 变成了 0x555555559320,其余字段的值未发生变化。
step 4: 研究 obj4 对象申请堆内存的过程
执行完 vm4_main.cpp:41 行后,查看main_arena
对象的值:
(gdb) c
Continuing.
Breakpoint 12, main (argc=1, argv=0x7fffffffdda8) at vm4_main.cpp:43
43 obj2.Free();
(gdb) p *((struct malloc_state *)0x7ffff7fafb80)
$7 = {mutex = 0, flags = 0, have_fastchunks = 0, fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, top = 0x555555559340, last_remainder = 0x0, bins = {
0x7ffff7fafbe0 <main_arena+96>, 0x7ffff7fafbe0 <main_arena+96>, 0x7ffff7fafbf0 <main_arena+112>, 0x7ffff7fafbf0 <main_arena+112>, 0x7ffff7fafc00 <main_arena+128>, 0x7ffff7fafc00 <main_arena+128>...},
binmap = {0, 0, 0, 0}, next = 0x7ffff7fafb80 <main_arena>, next_free = 0x0, attached_threads = 1, system_mem = 135168, max_system_mem = 135168}
从上面的结果中可以看出,在obj4
对象申请堆内存后,main_arena
对象中的数据成员top
的值从 0x555555559320 变成了 0x555555559340,其余字段的值未发生变化。
step 5: 研究 obj2 对象释放堆内存的过程
1) 进入free
函数
(gdb) b free
Breakpoint 6 at 0x7ffff7e61850: free. (2 locations)
(gdb) c
Continuing.
Breakpoint 6, __GI___libc_free (mem=0x5555555592c0) at malloc.c:3087
3087 {
2) 分析obj2
对象释放堆内存时,实际调用了free
函数的哪些代码
a)查看free
函数完整的实现源码
(gdb) l malloc.c:3085,malloc.c:3126
3085 void
3086 __libc_free (void *mem)
3087 {
3088 mstate ar_ptr;
3089 mchunkptr p; /* chunk corresponding to mem */
3090
3091 void (*hook) (void *, const void *)
3092 = atomic_forced_read (__free_hook);
3093 if (__builtin_expect (hook != NULL, 0))
3094 {
3095 (*hook)(mem, RETURN_ADDRESS (0));
3096 return;
3097 }
3098
3099 if (mem == 0) /* free(0) has no effect */
3100 return;
3101
3102 p = mem2chunk (mem);
3103
3104 if (chunk_is_mmapped (p)) /* release mmapped memory. */
3105 {
3106 /* See if the dynamic brk/mmap threshold needs adjusting.
3107 Dumped fake mmapped chunks do not affect the threshold. */
3108 if (!mp_.no_dyn_threshold
3109 && chunksize_nomask (p) > mp_.mmap_threshold
3110 && chunksize_nomask (p) <= DEFAULT_MMAP_THRESHOLD_MAX
3111 && !DUMPED_MAIN_ARENA_CHUNK (p))
3112 {
3113 mp_.mmap_threshold = chunksize (p);
3114 mp_.trim_threshold = 2 * mp_.mmap_threshold;
3115 LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,
3116 mp_.mmap_threshold, mp_.trim_threshold);
3117 }
3118 munmap_chunk (p);
3119 return;
3120 }
3121
3122 MAYBE_INIT_TCACHE ();
3123
3124 ar_ptr = arena_for_chunk (p);
3125 _int_free (ar_ptr, p, 0);
3126 }
b)为了简单,这里直接给出obj2
对象释放堆内存时,实际调用的free
函数的代码部分
3085 void
3086 __libc_free (void *mem)
3087 {
3088 mstate ar_ptr;
3089 mchunkptr p; /* chunk corresponding to mem */
// 省略...
3102 p = mem2chunk (mem);
// 省略...
3124 ar_ptr = arena_for_chunk (p);
3125 _int_free (ar_ptr, p, 0);
3126 }
其中,mem2chunk
的作用在上文已经提到过了。因此,局部变量p
的值就是要释放chunk
的起始地址。
这里,有以下几个疑问:
arena_for_chunk
的作用?
int_free`是如何将`obj2.data
所在的已分配块释放的?
c)arena_for_chunk
的作用?
查看arena_for_chunk
的定义(在 arena.c 中):
/* find the heap and corresponding arena for a given ptr */
#define heap_for_ptr(ptr) \
((heap_info *) ((unsigned long) (ptr) & ~(HEAP_MAX_SIZE - 1)))
#define arena_for_chunk(ptr) \
(chunk_main_arena (ptr) ? &main_arena : heap_for_ptr (ptr)->ar_ptr)
其中,chunk_main_arena
的定义(在 malloc.c 中):
/* size field is or'ed with NON_MAIN_ARENA if the chunk was obtained
from a non-main arena. This is only set immediately before handing
the chunk to the user, if necessary. */
#define NON_MAIN_ARENA 0x4
/* Check for chunk from main arena. */
#define chunk_main_arena(p) (((p)->mchunk_size & NON_MAIN_ARENA) == 0)
通过上面的宏定义,我们可以推断,arena_for_chunk
的作用:返回chunk
所属于的arena
(数据类型为struct malloc_state
的对象)的地址。这个arena
要么是main_arena
,要么是heap_for_ptr(ptr)
返回的。
同时,我们也可以看出,判断一个chunk
是否属于main_arena
的方法:如果chunk
中的mchunk_size
字段里面A
标志位的值为 0,那么该chunk
属于main_arena
。(此处解释了上一篇中的遗留问题——malloc_chunk
结构体中mchunk_size
字段里面的A
标志位是如何运用的?)
d)_int_free
是如何将obj2.data_
所在的已分配块释放的?
obj2.data_
所在的已分配块被释放后,会被维护在tcache
中。具体分析过程,见本文中的 为什么在调用 free 函数后,被释放的 chunk 从内存数据来看仍是一个已分配块?
step 6: 研究 obj4 对象释放堆内存的过程
(gdb) b malloc.c:4208
Breakpoint 12 at 0x7ffff7e5bc78: file malloc.c, line 4208.
(gdb) c
Continuing.
Breakpoint 12, tcache_put (tc_idx=0, chunk=0x555555559320) at malloc.c:4208
4208 tcache_put (p, tc_idx);
(gdb) bt
#0 tcache_put (tc_idx=0, chunk=0x555555559320) at malloc.c:4208
#1 _int_free (av=0x7ffff7fadb80 <main_arena>, p=0x555555559320, have_lock=0) at malloc.c:4208
#2 0x000055555555536f in HeapObject::Free (this=0x7fffffffdc60) at vm4_main.cpp:26
#3 0x0000555555555228 in main (argc=1, argv=0x7fffffffdd98) at vm4_main.cpp:44
(gdb) l vm4_main.cpp:44
// 省略...
44 obj4.Free();
// 省略...
从上面的结果中可以看出,obj4.data_
所在的已分配块被释放后,也会被维护在tcache
中。
step 7: 研究 obj3 对象释放堆内存的过程
(gdb) c
Continuing.
Breakpoint 12, tcache_put (tc_idx=3, chunk=0x5555555592d0) at malloc.c:4208
4208 tcache_put (p, tc_idx);
(gdb) bt
#0 tcache_put (tc_idx=3, chunk=0x5555555592d0) at malloc.c:4208
#1 _int_free (av=0x7ffff7fadb80 <main_arena>, p=0x5555555592d0, have_lock=0) at malloc.c:4208
#2 0x000055555555536f in HeapObject::Free (this=0x7fffffffdc50) at vm4_main.cpp:26
#3 0x0000555555555234 in main (argc=1, argv=0x7fffffffdd98) at vm4_main.cpp:45
(gdb) l vm4_main.cpp:45
// 省略...
45 obj3.Free();
// 省略...
从上面的结果中可以看出,obj3.data_
所在的已分配块被释放后,也会被维护在tcache
中。
step 8: 研究 obj1 对象释放堆内存的过程
(gdb) c
Continuing.
Breakpoint 12, tcache_put (tc_idx=0, chunk=0x555555559290) at malloc.c:4208
4208 tcache_put (p, tc_idx);
(gdb) bt
#0 tcache_put (tc_idx=0, chunk=0x555555559290) at malloc.c:4208
#1 _int_free (av=0x7ffff7fadb80 <main_arena>, p=0x555555559290, have_lock=0) at malloc.c:4208
#2 0x000055555555536f in HeapObject::Free (this=0x7fffffffdc30) at vm4_main.cpp:26
#3 0x0000555555555240 in main (argc=1, argv=0x7fffffffdd98) at vm4_main.cpp:46
(gdb) l vm4_main.cpp:46
// 省略...
46 obj1.Free();
// 省略...
从上面的结果中可以看出,obj1.data_
所在的已分配块被释放后,也会被维护在tcache
中。
step 9: 研究 obj5 对象申请堆内存的过程
结合上述分析过程,我们可以推断,由于obj5.data_
要申请的堆内存大小为 32 字节,所以malloc
函数会为分其分配一个大小为 48 字节的chunk
。目前tcache
中没有大小为 48 字节的空闲chunk
。所以,会从top chunk
中进行分配。
现在,我们通过 gdb 将obj5.data_
要申请的堆内存大小修改为 16 字节,观察其分配过程。
(gdb) c
Continuing.
Breakpoint 2, __GI___libc_malloc (bytes=32) at malloc.c:3023
3023 {
(gdb) p/d bytes=16
$1 = 16
(gdb) n
3031 = atomic_forced_read (__malloc_hook);
// 省略...
(gdb) n
3047 if (tc_idx < mp_.tcache_bins
(gdb)
Breakpoint 5, tcache_get (tc_idx=<optimized out>) at malloc.c:3051
3051 return tcache_get (tc_idx);
(gdb) n
2937 tcache->entries[tc_idx] = e->next;
(gdb)
2938 --(tcache->counts[tc_idx]);
(gdb)
2939 e->key = NULL;
(gdb)
__GI___libc_malloc (bytes=16) at malloc.c:2940
2940 return (void *) e;
// 省略...
(gdb) n
main (argc=1, argv=0x7fffffffdd98) at vm4_main.cpp:49
49 obj5.Free();
(gdb) p obj5.data_
$3 = (void *) 0x5555555592a0
从上面的结果中可以看出,当申请一个大小为 16 字节的堆内存时,在 64-bit 系统中所对应的chunk
的大小为 32 字节。如果tcache
中有相同大小的空闲chunk
,那么会从tcache
中分配并返回给用户。
研究过程:
step 1: 在链接期拦截标准库的malloc
函数的工作原理
1) 启动可执行目标文件vm4_main
,并设置一些断点等
$ gdb -q -x tvm4_1.gdb
Temporary breakpoint 1 at 0x11a9: file vm4_main.cpp, line 37.
Temporary breakpoint 1, main (argc=0, argv=0x0) at vm4_main.cpp:37
37 {
Breakpoint 2 at 0x7ffff7e5f260: malloc. (2 locations)
Breakpoint 3 at 0x7ffff7e5edfb: malloc.c:3033. (2 locations)
Breakpoint 4 at 0x5555555551dd: file vm4_main.cpp, line 39.
tvm4_1.gdb 文件的内容为:
file ./vm4_main
start
directory /home/workspace/git-projects/glibc-2.31/malloc
set listsize 20
b malloc
b malloc.c:3033
b vm4_main.cpp:39
2) 继续执行直到 malloc.c:3033 行时停止,并查看局部变量hook
的值
(gdb) c
Continuing.
Breakpoint 2, __GI___libc_malloc (bytes=8) at malloc.c:3023
3023 {
(gdb) c
Continuing.
Breakpoint 3, __GI___libc_malloc (bytes=8) at malloc.c:3033
3033 return (*hook)(bytes, RETURN_ADDRESS (0));
(gdb) p hook
$1 = (void *(*)(size_t, const void *)) 0x7ffff7e5ec90 <malloc_hook_ini>
从上面的结果中可以看出,局部变量hook
的值正是函数malloc_hook_ini
的地址。
也就是说,此处的return (*hook)(bytes, RETURN_ADDRESS (0));
语句等价于return malloc_hook_ini(bytes, RETURN_ADDRESS (0));
。
3) 查看函数malloc_hook_ini
的源码(在 hooks.c 中)
(gdb) l malloc_hook_ini
19
20 /* What to do if the standard debugging hooks are in place and a
21 corrupt pointer is detected: do nothing (0), print an error message
22 (1), or call abort() (2). */
23
24 /* Hooks for debugging versions. The initial hooks just call the
25 initialization routine, then do the normal work. */
26
27 static void *
28 malloc_hook_ini (size_t sz, const void *caller)
29 {
30 __malloc_hook = NULL;
31 ptmalloc_init ();
32 return __libc_malloc (sz);
33 }
从上面的结果中可以看出,函数malloc_hook_ini
里面依次调用的函数为ptmalloc_init ();
、__libc_malloc (sz);
。
ptmalloc_init ()
函数的作用,先作为遗留问题。
需要注意的是,函数malloc_hook_ini
在调用__libc_malloc (sz);
前先将__malloc_hook
变量置为NULL
。这样做是为了避免下一次进入__libc_malloc
函数后又进入malloc_hook_ini
函数,从而导致死循环。
4) 局部变量hook
是如何指向malloc_hook_ini
函数的?
查看局部变量hook
被赋值的地方(在 malloc.c 中):
3021 void *
3022 __libc_malloc (size_t bytes)
3023 {
// 省略...
3030 void *(*hook) (size_t, const void *)
3031 = atomic_forced_read (__malloc_hook);
// 省略...
3082 }
从上面的结果中可以看出,局部变量hook
被设置为变量__malloc_hook
的值。
5) 变量__malloc_hook
是从哪来的?
变量__malloc_hook
定义在malloc.c
源文件中,如下:
void *weak_variable (*__malloc_hook)
(size_t __size, const void *) = malloc_hook_ini;
从上面的结果中可以看出,变量__malloc_hook
的值即为malloc_hook_ini
函数的地址。从而,局部变量hook
的值就是malloc_hook_ini
函数的地址。
需要注意的是,变量__malloc_hook
是一个全局变量,并且是一个弱符号。
看到这的第一反应是,既然全局变量__malloc_hook
是一个弱符号,那么如果我们定义一个同名的全局变量(当然也是一个函数指针)且为强符号,局部变量hook
是不是就代表我们所指定的函数。如果猜想正确的话,这意味着我们“拦截了”标准库中的malloc
函数。
step 2: 如何在链接期拦截标准库的malloc
函数
1) 添加拦截代码
先将vm4_main.cpp
拷贝一份,并将拷贝文件重命名为vm4_main_2.cpp
。
在vm4_main_2.cpp
源文件的末尾添加如下代码:
static void *
my_malloc_hook (size_t sz, const void *caller);
void *(*__malloc_hook) (size_t, const void *) = my_malloc_hook;
static void *
my_malloc_hook (size_t sz, const void *caller)
{
__malloc_hook = NULL;
return NULL;
}
需要注意的是,my_malloc_hook
函数在返回前将全局变量__malloc_hook
设置为NULL
。从而,只会拦截对malloc
函数的第一次调用,接下来的调用依然使用标准库中的malloc
函数实现。
2) 编译,并运行可执行目标文件 vm4_main_2
$ g++ -o vm4_main_2 vm4_main_2.cpp -g
$ gdb -q -x tvm4_1_2.gdb
Temporary breakpoint 1 at 0x11a9: file vm4_main_2.cpp, line 37.
Temporary breakpoint 1, main (argc=0, argv=0x0) at vm4_main_2.cpp:37
37 {
Breakpoint 2 at 0x7ffff7e5f260: malloc. (2 locations)
Breakpoint 3 at 0x7ffff7e5edfb: malloc.c:3033. (2 locations)
Breakpoint 4 at 0x5555555551dd: file vm4_main_2.cpp, line 39.
tvm4_1_2.gdb 文件的内容为:
file ./vm4_main_2
start
directory /home/workspace/git-projects/glibc-2.31/malloc
set listsize 20
b malloc
b malloc.c:3033
b vm4_main_2.cpp:39
3) 继续执行直到 malloc.c:3033 行时停止,并查看局部变量hook
的值
(gdb) c
Continuing.
Breakpoint 2, __GI___libc_malloc (bytes=8) at malloc.c:3023
3023 {
(gdb) c
Continuing.
Breakpoint 3, __GI___libc_malloc (bytes=8) at malloc.c:3033
3033 return (*hook)(bytes, RETURN_ADDRESS (0));
(gdb) p hook
$1 = (void *(*)(size_t,
const void *)) 0x5555555552bb <my_malloc_hook(size_t, void const*)>
从上面的结果中可以看出,现在局部变量hook
的值变成了我们自定义函数my_malloc_hook
的地址。也就是说,我们成功地在链接期拦截了标准库中的malloc
函数。
研究结论:
在链接期拦截标准库malloc
函数的工作原理?
glibc 中的malloc
函数在第一次真正地为用户分配堆内存之前,会调用一个钩子函数。这个钩子函数的地址保存在全局变量__malloc_hook
中,并且 glibc 中所定义的__malloc_hook
默认是一个弱符号。因此,我们可以利用 Linux 系统中链接器的符号解析规则,即通过定义一个同名的全局变量__malloc_hook
(当然也是一个函数指针)且为强符号,从而达到拦截标准库的malloc
函数的效果。
同样地,我们可以通过__free_hook
在链接期拦截标准库的free
函数。
研究过程:
step 1: 问题引入
无论是否开启tcache
机制,glibc 中的malloc
函数所分配的chunk
的大小都可能经过内部调整。也就是说,用户数据所在的chunk
的大小不是简单地将用户数据大小与chunk
头部大小相加之和。用于确定chunk
大小的函数是checked_request2size
。
_int_malloc
函数中调用checked_request2size
的附近代码:
static void *
_int_malloc (mstate av, size_t bytes)
{
INTERNAL_SIZE_T nb; /* normalized request size */
// 省略...
/*
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 returns false for request sizes
that are so large that they wrap around zero when padded and
aligned.
*/
if (!checked_request2size (bytes, &nb))
{
__set_errno (ENOMEM);
return NULL;
}
// 省略...
}
step 2: 函数checked_request2size
的作用?
1) 查看函数checked_request2size
的定义(在 malloc.c 中)
/* Check if REQ overflows when padded and aligned and if the resulting value
is less than PTRDIFF_T. Returns TRUE and the requested size or MINSIZE in
case the value is less than MINSIZE on SZ or false if any of the previous
check fail. */
static inline bool
checked_request2size (size_t req, size_t *sz) __nonnull (1)
{
if (__glibc_unlikely (req > PTRDIFF_MAX))
return false;
*sz = request2size (req);
return true;
}
2) 查看PTRDIFF_MAX
的定义(在 stdint.h 中)
/* Limits of `ptrdiff_t' type. */
# if __WORDSIZE == 64
# define PTRDIFF_MIN (-9223372036854775807L-1)
# define PTRDIFF_MAX (9223372036854775807L)
# else
// 省略...
从上面的结果中可以看出,PTRDIFF_MAX
的值为 9223372036854775807L(在 64-bit 系统上),表示一个占用八字节(在 64-bit 系统上)的有符号整型的最大值(9223372036854775807 = 1024*1024*1024*1024*1024*1024*8-1
)。
3) 查看函数request2size
的定义(在 malloc.c 中)
#define request2size(req) \
(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) ? \
MINSIZE : \
((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
4) 查看SIZE_SZ
和MALLOC_ALIGN_MASK
的定义(在 malloc-internal.h 中)
#ifndef INTERNAL_SIZE_T
# define INTERNAL_SIZE_T size_t
#endif
/* The corresponding word size. */
#define SIZE_SZ (sizeof (INTERNAL_SIZE_T))
/* The corresponding bit mask value. */
#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)
注:在 64-bit 系统上,SIZE_SZ
的值为 8(字节)。
其中,MALLOC_ALIGNMENT
的定义(在 malloc-alignment.h 中)为:
/* MALLOC_ALIGNMENT is the minimum alignment for malloc'ed chunks. It
must be a power of two at least 2 * SIZE_SZ, even on machines for
which smaller alignments would suffice. It may be defined as larger
than this though. Note however that code and data structures are
optimized for the case of 8-byte alignment. */
#define MALLOC_ALIGNMENT (2 * SIZE_SZ < __alignof__ (long double) \
? __alignof__ (long double) : 2 * SIZE_SZ)
注:在 64-bit 系统上,long double
类型的对象占用 16 字节。
因此,在 64-bit 系统上,MALLOC_ALIGNMENT
的值为 16(字节)。相应地,MALLOC_ALIGN_MASK
的值为 0xf。
5) 查看MINSIZE
的定义(在 malloc.c 中)
/* The smallest possible chunk */
#define MIN_CHUNK_SIZE (offsetof(struct malloc_chunk, fd_nextsize))
/* The smallest size we can malloc is an aligned minimal chunk */
#define MINSIZE \
(unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK))
在 64-bit 系统上,字段fd_nextsize
相对于结构体struct malloc_chunk
起始位置的偏移量为 32(字节)。因此,在 64-bit 系统上,MIN_CHUNK_SIZE
的值为 32。
所以,在 64-bit 系统上,MINSIZE
的值为 ((32 + 0xf) & ~0xf),等于 0x20。
因此,我们可以推断,request2size(req)
的作用为(仅限于在 64-bit 系统上):
如果用户所申请的堆内存大小 <= 8 字节,那么malloc
函数实际分配的chunk
(包含了用户数据)的大小为 32 字节。这意味用户实际可以使用的内存大小是 16 字节,并且是完全合法的(不会破坏下一个chunk
的内存数据)。
如果用户所申请的堆内存大小 > 8 字节,那么malloc
函数实际分配的chunk
(包含了用户数据)的大小为 16 字节的整数倍,但不小于 32 字节。
所以,malloc
函数中,checked_request2size (bytes, &tbytes)
的作用为:
检查用户所申请的堆内存大小是否大于上限值。如果是,那么该函数返回false
。从而,导致errno
的值被设置为错误码ENOMEM
。
将用户所申请的堆内存大小(即参数bytes
的值)进行内存对齐处理,最终的chunk
大小保存在变量tbytes
中。在 64-bit 系统上,实际分配chunk
的大小同时满足:1)大小为 16 字节的整数倍;2)大小 >= 32 字节。
这就解释了上一篇中的遗留问题:obj1.data_
所在chunk
的内存布局中尾部八字节的作用,即用来凑够 64-bit 系统中最小chunk
的大小。
研究结论:
在 64-bit 系统上,glibc 中的malloc
函数所分配的chunk
的大小必须同时满足:1)chunk
的大小 >= 32 字节;2)chunk
的大小为 16 字节的整数倍。
研究过程:
step 0: 查看结构体malloc_state
的定义(在 malloc.c 中)
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;
/* 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;
};
step 1: 结构体malloc_state
中,数据成员top
的作用?
1) 查看数据成员top
的注释
/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;
从上面的注释中可以得出如下两个信息:
数据成员top
的值为最顶部chunk
的起始位置。
最顶部chunk
不属于任何一个bin
。
在obj1
对象申请堆内存后,数据成员top
的值从 0x0 变成了 0x5555555592b0。
2) 查看内存地址为 0x5555555592b0 附近的数据
(gdb) x/8gx 0x5555555592b0-8*4
0x555555559290: 0x0000000000000000 0x0000000000000021
0x5555555592a0: 0x0101010101010101 0x0000000000000000
0x5555555592b0: 0x0000000000000000 0x0000000000020d51
0x5555555592c0: 0x0000000000000000 0x0000000000000000
结合我们在上一篇中所分析的obj1.data_
所在chunk
的内存布局,我们可以推断,结构体malloc_state
中的数据成员top
的作用为:指向堆顶chunk
的起始位置。
step 2: 结构体malloc_state
中,数据成员system_mem
的作用?
1) 查看数据成员system_mem
的注释
/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
从上面的注释中可以得出,数据成员system_mem
的值表示系统为这个arena
分配的内存大小。
2) 查看数据成员system_mem
的值
在obj1
对象申请堆内存后,数据成员system_mem
的值从 0 变成了 135168。
十进制 135168 对应的十六进制为:
(gdb) p/x 135168
$6 = 0x21000
在上一篇中分析obj1.data_
所在chunk
的内存布局时,堆的初始大小也是 0x21000 字节。
3) 这里,再次查看堆的大小
(gdb) i proc mappings
process 354872
Mapped address spaces:
Start Addr End Addr Size Offset objfile
// 省略...
0x555555559000 0x55555557a000 0x21000 0x0 [heap]
// 省略...
从上面的结果中可以看出,这次堆的大小仍是 0x21000 字节。
因此,我们可以推断,结构体malloc_state
中的数据成员system_mem
的作用为:保存堆的大小(包括已分配的和空闲的)。
研究结论:
结构体malloc_state
中各字段的意义如下表:
字段名 | 类型 | 意义 |
---|---|---|
top | struct malloc_chunk* | 指向堆顶chunk 的起始位置 |
system_mem | INTERNAL_SIZE_T | 保存堆的大小(包括已分配的和空闲的) |
glibc 中关于tcache
(全称为per-thread cache
)特性的说明如下(在 NEWS 文件中):
Version 2.26
Major new features:
* A per-thread cache has been added to malloc. Access to the cache requires
no locks and therefore significantly accelerates the fast path to allocate
and free small amounts of memory. Refilling an empty cache requires locking
the underlying arena. Performance measurements show significant gains in a
wide variety of user workloads. Workloads were captured using a special
instrumented malloc and analyzed with a malloc simulator. Contributed by
DJ Delorie with the help of Florian Weimer, and Carlos O'Donell.
从上面的说明中,我们可以推断,引入tcache
是为了改善分配和释放较小内存块的性能。
编译期打开tcache
机制的方法:编译 glibc 时,添加编译选项-DUSE_TCACHE
。这样做是因为,tcache
相关的代码都定义在自定义的USE_TCACHE
预处理命令中。
研究过程:
step 0: 查看结构体tcache_perthread_struct
的定义
#if USE_TCACHE
// 省略...
/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
// 省略...
#endif /* !USE_TCACHE */
step 1: 结构体tcache_perthread_struct
中,数据成员counts
和entries
的作用?
1) 查看结构体tcache_entry
的定义
#if USE_TCACHE
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;
// 省略...
#endif /* !USE_TCACHE */
从上面的源码和注释,我们可以先这样理解(不一定正确):
结构体tcache_entry
中的next
字段的意义:指向链表的下一个元素。
结构体tcache_entry
中的key
字段的意义:用于检查是否重复释放。
2) 分析结构体tcache_entry
中next
和key
字段的意义
结构体tcache_entry
中的key
字段在 malloc.c 源文件中,由以下三个函数调用了,分别是:_int_free
、tcache_put
、tcache_get
。
a)结构体tcache_entry
中的key
字段在tcache_put
函数中被调用的代码部分
2915 /* Caller must ensure that we know tc_idx is valid and there's room
2916 for more chunks. */
2917 static __always_inline void
2918 tcache_put (mchunkptr chunk, size_t tc_idx)
2919 {
2920 tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
2921
2922 /* Mark this chunk as "in the tcache" so the test in _int_free will
2923 detect a double free. */
2924 e->key = tcache;
2925
2926 e->next = tcache->entries[tc_idx];
2927 tcache->entries[tc_idx] = e;
2928 ++(tcache->counts[tc_idx]);
2929 }
从上面的源码和注释,我们可以得出如下几个结论:
从第 2920 和 2927 行可以看出,结构体tcache_perthread_struct
中数据成员entries
的作用为:维护已释放的chunk
,并且其中元素的值为“用户数据”(已过时)的起始地址。
从第 2922~2924 行可以看出,结构体tcache_entry
中的key
字段的意义为:将该值设置为tcache
变量的值,表示该chunk
已经在tcache
中被维护了,用于检查一个chunk
是否重复释放。
从第 2926~2927 行可以看出,结构体tcache_perthread_struct
中数据成员entries
的每个元素的值指向各自单向链表的头节点,并且这些链表采用头插法添加新节点。
从第 2928 行可以看出,结构体tcache_perthread_struct
中数据成员counts
的作用为:tcache
中每个链表中的节点数量。
b)结构体tcache_entry
中的key
字段在tcache_get
函数中被调用的代码部分
2931 /* Caller must ensure that we know tc_idx is valid and there's
2932 available chunks to remove. */
2933 static __always_inline void *
2934 tcache_get (size_t tc_idx)
2935 {
2936 tcache_entry *e = tcache->entries[tc_idx];
2937 tcache->entries[tc_idx] = e->next;
2938 --(tcache->counts[tc_idx]);
2939 e->key = NULL;
2940 return (void *) e;
2941 }
tcache_get
函数的解释:从tcache
中的某个链表中取出头节点,并作为已分配块返回给用户。在返回之前做了三件事:更新链表的头节点;更新链表中的节点数量;将key
字段的值设置为NULL
,表示该chunk
不被tcache
维护了。
c)结构体tcache_entry
中的key
字段在_int_free
函数中被调用的代码部分
4153 static void
4154 _int_free (mstate av, mchunkptr p, int have_lock)
4155 {
4156 INTERNAL_SIZE_T size; /* its size */
// 省略...
4165 size = chunksize (p);
// 省略...
4181 #if USE_TCACHE
4182 {
4183 size_t tc_idx = csize2tidx (size);
4184 if (tcache != NULL && tc_idx < mp_.tcache_bins)
4185 {
4186 /* Check to see if it's already in the tcache. */
4187 tcache_entry *e = (tcache_entry *) chunk2mem (p);
4188
4189 /* This test succeeds on double free. However, we don't 100%
4190 trust it (it also matches random payload data at a 1 in
4191 2^<size_t> chance), so verify it's not an unlikely
4192 coincidence before aborting. */
4193 if (__glibc_unlikely (e->key == tcache))
4194 {
4195 tcache_entry *tmp;
4196 LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
4197 for (tmp = tcache->entries[tc_idx];
4198 tmp;
4199 tmp = tmp->next)
4200 if (tmp == e)
4201 malloc_printerr ("free(): double free detected in tcache 2");
4202 /* If we get here, it was a coincidence. We've wasted a
4203 few cycles, but don't abort. */
4204 }
// 省略...
4211 }
4212 }
4213 #endif
// 省略...
4428 }
从第 4189~4192 行可以看出,第 4193~4204 行的作用为:检查该chunk
是否重复释放。
第 4193 行,当该要被释放的chunk
中的key
字段的值等于tcache
变量的值时,条件表达式的求值结果为true
。此时,该chunk
可能已经被释放了。由于存在“用户数据正好是 tcache 变量的值”的可能性。于是,检查该chunk
在tcache
中对应的链表(从头节点开始遍历)中的元素是否已经有该chunk
了。如果有,表示该chunk
已经被tcache
维护了,即本次释放操作是重复释放,那么进行错误处理(内部调用abort
函数结束进程)。
step 2: chunk 大小与 tc_idx 之间的映射关系
tcache
中所维护的已释放的chunk
,根据chunk
的不同大小,会被维护到不同的链表中。链表的索引(即entries
数组的下标)就是tc_idx
。将chunk
大小映射为tc_idx
的宏定义为csize2tidx
。
1) 查看宏定义csize2tidx
的定义(在 malloc.c 中)
/* When "x" is from chunksize(). */
# define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
从上面的注释可以看出,宏定义csize2tidx
(名称可以理解为:chunk-size-to-tcache-index)的参数x
表示chunk
的大小。
从上文中的分析中我们知道,在 64-bit 系统上,MINSIZE
、MALLOC_ALIGNMENT
的值分别为 0x20、0x10。
因此,在 64-bit 系统上,宏定义csize2tidx
等价于:
# define csize2tidx(x) (((x) - 17) / 16)
于是,可以很容易地得出chunk
大小与tc_idx
之间的映射关系,详见下文。
研究结论:
结构体tcache_perthread_struct
中各字段的意义:
字段名 | 类型 | 意义 |
---|---|---|
counts | 大小为 TCACHE_MAX_BINS 的数组,其中元素的数据类型为 uint16_t |
用于存储 tcache 所维护的各个链表中的节点数量,即 counts[i] 的值表示 entries[i] 链表中的节点数量。 |
entries | 大小为 TCACHE_MAX_BINS 的数组,其中元素的数据类型为 tcache_entry * |
用于存储 tcache 所维护的各个链表,即 entries[i] 表示由大小为 32+i*16 的 chunk 所组成的单向链表中的头节点 |
结构体tcache_entry
中各字段的意义:
字段名 | 类型 | 意义 |
---|---|---|
next | struct tcache_entry * | 指向链表中的下一个已释放的 chunk,即 next 的值为下一个已释放 chunk 中“用户数据”的起始地址 |
key | struct tcache_perthread_struct * | 用于检查一个 chunk 是否重复释放 |
在 64-bit 系统上,chunk
大小与tc_idx
之间的映射关系:
tc_idx 的值 | 对应的 chunk 大小 |
---|---|
0 | 32 |
1 | 48 |
2 | 64 |
3 | 80 |
i | 32+i*16 |
62 | 1024 |
63 | 1040 |
注意: chunk
大小 > 1040 字节的,不会被维护在tcache
中。这是因为,结构体tcache_perthread_struct
中,数据成员entries
的数组大小为TCACHE_MAX_BINS
(值被定义为 64)。
研究过程:
step 1: 准备
1) 启动可执行目标文件vm4_main
,并设置一些断点等
$ gdb -q -x tvm4_4.gdb
Temporary breakpoint 1 at 0x11a9: file vm4_main.cpp, line 37.
Temporary breakpoint 1, main (argc=0, argv=0x0) at vm4_main.cpp:37
37 {
Breakpoint 2 at 0x7ffff7e5f260: malloc. (2 locations)
Breakpoint 3 at 0x7ffff7e5cbf0: file malloc.c, line 3545.
Breakpoint 4 at 0x5555555551dd: file vm4_main.cpp, line 39.
tvm4_4.gdb 文件的内容为:
file ./vm4_main
start
directory /home/workspace/git-projects/glibc-2.31/malloc
set listsize 20
b malloc
b _int_malloc
b vm4_main.cpp:39
2) 继续执行
(gdb) c
Continuing.
Breakpoint 2, __GI___libc_malloc (bytes=8) at malloc.c:3023
3023 {
(gdb) c
Continuing.
Breakpoint 3, _int_malloc (av=av@entry=0x7ffff7fadb80 <main_arena>, bytes=bytes@entry=640) at malloc.c:3545
3545 if (!checked_request2size (bytes, &nb))
(gdb) p/x bytes
$1 = 0x280
需要注意的是,obj1
对象申请的堆内存大小为 8 字节,而这里_int_malloc
函数中的参数bytes
的值为 640(十六进制为 0x280)字节。
另外,从上一篇中我们知道,第一个chunk
的大小为 0x290 字节。那么,目前正在分配的是否就是第一个chunk
呢?继续往下看。
3) 查看当前线程的调用堆栈信息
(gdb) bt
#0 _int_malloc (av=av@entry=0x7ffff7fadb80 <main_arena>, bytes=bytes@entry=640) at malloc.c:3545
#1 0x00007ffff7e5dafb in tcache_init () at malloc.c:2982
#2 0x00007ffff7e5ed8e in tcache_init () at malloc.c:3044
#3 __GI___libc_malloc (bytes=8) at malloc.c:3044
#4 malloc_hook_ini (sz=8, caller=<optimized out>) at hooks.c:32
#5 0x00005555555552f3 in HeapObject::HeapObject (this=0x7fffffffdc30, size=8) at vm4_main.cpp:11
#6 0x00005555555551dd in main (argc=1, argv=0x7fffffffdd98) at vm4_main.cpp:38
从上面的调用堆栈信息可以看出,目前正在分配的chunk
的根源是由“obj1 对象申请堆内存”导致的,并且该chunk
的分配发生在obj1.data_
所在chunk
被分配之前。另外,目前正在分配的chunk
的上一级为tcache_init
函数。那么,是不是tcache_init
函数中有申请大小为 0x280 字节的堆内存的操作呢。继续往下看。
4) 单步执行直到运行到 malloc.c:4142 行时停止,并观察局部变量p
的值和进程的内存映射情况
(gdb) n
1210 *sz = request2size (req);
# 省略...
(gdb) n
4141 void *p = sysmalloc (nb, av);
(gdb) n
4142 if (p != NULL)
(gdb) p/x p
$2 = 0x555555559010
(gdb) i proc mappings
process 673635
Mapped address spaces:
Start Addr End Addr Size Offset objfile
# 省略...
0x555555559000 0x55555557a000 0x21000 0x0 [heap]
从上面的结果中可以看出,局部变量p
的值为 0x555555559010,堆的起始地址为 0x555555559000。
查看 malloc.c:4142 行附近的代码:
4141 void *p = sysmalloc (nb, av);
4142 if (p != NULL)
4143 alloc_perturb (p, bytes);
4144 return p;
结合上述源码,我们可以发现,局部变量p
就是_int_malloc
函数的返回值。我们知道,_int_malloc
函数的返回值表示用户数据的起始地址。从而,目前正在分配的chunk
的起始地址为 0x555555559000,也就是本进程中堆的起始地址。因此,目前正在分配的chunk
正是上一篇中我们所提到的“第一个 chunk ”。
step 2: 第一个 chunk 是如何被创建的?
通过全局搜索 glibc 2.31 版本的源码可以发现,tcache_init
函数仅在宏定义MAYBE_INIT_TCACHE
中被调用。
1) 查看MAYBE_INIT_TCACHE
的定义(在 malloc.c 中)
#if USE_TCACHE
// 省略..
# define MAYBE_INIT_TCACHE() \
if (__glibc_unlikely (tcache == NULL)) \
tcache_init();
#else /* !USE_TCACHE */
# define MAYBE_INIT_TCACHE()
// 省略..
#endif /* !USE_TCACHE */
从上面的源码可以看出,tcache_init
函数要被调用,需要同时满足两个条件:1)tcache
机制已开启;2)变量tcache
的值等于NULL
。
2) 查看变量tcache
的定义(在 malloc.c 中)
static __thread tcache_perthread_struct *tcache = NULL;
从上面的源码可以看出,变量tcache
的初始值等于NULL
。由于该变量被关键字static
和__thread
修饰。所以,变量tcache
是一个静态变量,并且每个线程各自持有一份该变量的不同实体。
3) 查看宏定义MAYBE_INIT_TCACHE
在函数__libc_malloc
中被调用的地方
3021 void *
3022 __libc_malloc (size_t bytes)
3023 {
// 省略...
3034 #if USE_TCACHE
// 省略...
3043
3044 MAYBE_INIT_TCACHE ();
3045
// 省略...
3054 #endif
// 省略...
3082 }
结合上述源码以及本节中步骤“step 1:3)”中的调用堆栈信息,我们可以发现,第一个chunk
是在第一次为用户分配堆内存前创建的。
step 3: 第一个 chunk 的作用?
1) 查看tcache_init
函数的定义(在 malloc.c 中)
(gdb) l malloc.c:2971,malloc.c:3004
2971 static void
2972 tcache_init(void)
2973 {
2974 mstate ar_ptr;
2975 void *victim = 0;
2976 const size_t bytes = sizeof (tcache_perthread_struct);
2977
2978 if (tcache_shutting_down)
2979 return;
2980
2981 arena_get (ar_ptr, bytes);
2982 victim = _int_malloc (ar_ptr, bytes);
2983 if (!victim && ar_ptr != NULL)
2984 {
2985 ar_ptr = arena_get_retry (ar_ptr, bytes);
2986 victim = _int_malloc (ar_ptr, bytes);
2987 }
2988
2989
2990 if (ar_ptr != NULL)
2991 __libc_lock_unlock (ar_ptr->mutex);
2992
2993 /* In a low memory situation, we may not be able to allocate memory
2994 - in which case, we just keep trying later. However, we
2995 typically do this very early, so either there is sufficient
2996 memory, or there isn't enough memory to do non-trivial
2997 allocations anyway. */
2998 if (victim)
2999 {
3000 tcache = (tcache_perthread_struct *) victim;
3001 memset (tcache, 0, sizeof (tcache_perthread_struct));
3002 }
3003
3004 }
从上面的源码可以发现,第 2892 行的语句victim = _int_malloc (ar_ptr, bytes);
中的实参bytes
是一个局部常量,值为tcache_perthread_struct
所占用的字节数(等于 0x280 字节,如下)。
(gdb) p/x sizeof (tcache_perthread_struct)
$3 = 0x280
结合第 3000 行的语句tcache = (tcache_perthread_struct *) victim;
,表示将第一个chunk
的用户数据的起始地址保存到变量tcache
中。因此,我们可以推断,第一个chunk
的作用为:用于存储一个类型为tcache_perthread_struct
的对象。
关于tcache_perthread_struct
的更多内容,可参考本文中的 理解 struct tcache_perthread_struct 中各字段的意义。
研究结论:
第一个 chunk 是何时分配的?
第一个chunk
是在第一次为用户分配堆内存前创建的。
第一个 chunk 被创建需要满足的条件?
要创建第一个chunk
,需要同时满足两个条件:1)tcache
机制已开启;2)静态变量tcache
(线程局部存储)的值等于NULL
。
第一个 chunk 的作用?
用于存储一个类型为tcache_perthread_struct
的对象。
研究过程:
以释放obj2.data_
所在的已分配块为例。
step 1: 准备
1) 启动可执行目标文件vm4_main
,并设置一些断点等
$ gdb -q -x tvm4_2.gdb
Temporary breakpoint 1 at 0x11a9: file vm4_main.cpp, line 37.
Temporary breakpoint 1, main (argc=0, argv=0x0) at vm4_main.cpp:37
37 {
Breakpoint 2 at 0x7ffff7e5f850: free. (2 locations)
Breakpoint 3 at 0x7ffff7e5b9c0: file malloc.c, line 4155.
Breakpoint 4 at 0x55555555521c: file vm4_main.cpp, line 44.
tvm4_2.gdb 文件的内容为:
file ./vm4_main
start
directory /home/workspace/git-projects/glibc-2.31/malloc
set listsize 20
b free
b _int_free
b vm4_main.cpp:44
2) 继续执行,并查看当前线程的调用堆栈信息
(gdb) c
Continuing.
Breakpoint 2, __GI___libc_free (mem=0x5555555592c0) at malloc.c:3087
3087 {
(gdb) bt
#0 __GI___libc_free (mem=0x5555555592c0) at malloc.c:3087
#1 0x000055555555536f in HeapObject::Free (this=0x7fffffffdc40) at vm4_main.cpp:26
#2 0x000055555555521c in main (argc=1, argv=0x7fffffffdd98) at vm4_main.cpp:43
(gdb) l vm4_main.cpp:43
33 std::size_t size_;
34 };
35
36 int main(int argc, char *argv[])
37 {
38 HeapObject obj1(8);
39 HeapObject obj2(4);
40 HeapObject obj3(64);
41 HeapObject obj4(4);
42
43 obj2.Free();
44 obj4.Free();
45 obj3.Free();
46 obj1.Free();
47
48 HeapObject obj5(32);
49 obj5.Free();
50 return 0;
51 }
52
从上面的调用堆栈信息可以看出,目前正在分析的是释放obj2.data_
所在已分配块的过程。
在上文中,我们已经分析了obj2
对象释放堆内存时,实际调用了free
函数的哪些代码。这里不再赘述。接下来,我们直接研究_int_free
函数是如何释放obj2.data_
所在chunk
的。
step 2: 在obj2
对象释放堆内存时,实际调用了_int_free
函数中的哪部分代码?
1) 继续执行
(gdb) c
Continuing.
Breakpoint 3, _int_free (av=0x7ffff7fadb80 <main_arena>, p=0x5555555592b0,
have_lock=0) at malloc.c:4155
4155 {
(gdb) bt
#0 _int_free (av=0x7ffff7fadb80 <main_arena>, p=0x5555555592b0, have_lock=0) at malloc.c:4155
#1 0x000055555555536f in HeapObject::Free (this=0x7fffffffdc40) at vm4_main.cpp:26
#2 0x000055555555521c in main (argc=1, argv=0x7fffffffdd98) at vm4_main.cpp:43
(gdb) f 2
#2 0x000055555555521c in main (argc=1, argv=0x7fffffffdd98) at vm4_main.cpp:43
43 obj2.Free();
(gdb) p obj2.data_
$1 = (void *) 0x5555555592c0
从上面的结果中可以看出,obj2.data_
的值为 0x5555555592c0,而_int_free
函数的参数p
的值为 0x5555555592b0(即obj2.data_
所在chunk
的起始地址)。
也就是说,free
函数的参数表示用户数据的起始地址,_int_free
函数的参数p
表示用户数据所在chunk
的起始地址。这一点需要注意。
2) 在obj2
对象释放堆内存时,实际调用的_int_free
函数的代码部分如下(这里除了未被调用的以外,还省略了一些合法性检查):
4153 static void
4154 _int_free (mstate av, mchunkptr p, int have_lock)
4155 {
4156 INTERNAL_SIZE_T size; /* its size */
// 省略...
4165 size = chunksize (p);
// 省略...
4181 #if USE_TCACHE
4182 {
4183 size_t tc_idx = csize2tidx (size);
4184 if (tcache != NULL && tc_idx < mp_.tcache_bins)
4185 {
// 省略...
4206 if (tcache->counts[tc_idx] < mp_.tcache_count)
4207 {
4208 tcache_put (p, tc_idx);
4209 return;
4210 }
4211 }
4212 }
4213 #endif
// 省略...
4428 }
关于csize2tidx
、tc_idx
、tcache->counts[tc_idx]
和tcache_put
的更多内容,见本文中的 理解 tcache_perthread_struct 结构体中各字段的意义。
第 4183 行语句的作用:根据要释放的chunk
的大小,确定该chunk
应该添加到tcache
中的哪个链表里面。链表索引保存在tc_idx
变量中。
第 4184 行语句的作用:检查是否开启了tcache
机制和所计算出来的链表索引是否越界。
第 4206 行语句的作用:检查要存放的链表中的节点数量是否已经达到最大值。
第 4208 行语句的作用:将要释放的chunk
维护在tcache
中。
正因为obj2.data_
所在的chunk
在释放后被维护在了tcache
中。并且,该过程未更新该chunk
和下一个chunk
的头部。因此,在调用free
函数后,被释放的chunk
从内存数据来看仍是一个已分配块。
研究结论:
调用free
函数后,如果被释放的chunk
被维护在tcache
中,那么该chunk
和下一个chunk
的头部不会被更新。也就是说,这些被释放的chunk
从内存数据来看仍是一个已分配块,但实际上是作为空闲块由tcache
维护了。
下一篇:计算机系统之目录