计算机系统篇之虚拟内存(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维护了。
下一篇:计算机系统之目录