计算机系统篇之虚拟内存(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 机制。

再探 malloc 中已分配块和空闲块的内部布局

本文我们结合 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 0x11a9file vm4_main.cpp, line 37.
Starting program: /home/test/vm/vm4_main 

Temporary breakpoint 1main (argc=0, argv=0x0at 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/mallocmalloc.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:3021malloc.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_tconst void *)
3031        = atomic_forced_read (__malloc_hook);
3032      if (__builtin_expect (hook != NULL0))
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)设置一些断点,并观察这些断点的执行情况

(gdbb malloc.c:3033
Breakpoint 3 at 0x7ffff7e60dfbmalloc.c:3033. (2 locations)
(gdbb malloc.c:3037
Breakpoint 4 at 0x7ffff7e60cc8malloc.c:3037. (2 locations)
(gdbb malloc.c:3051
Breakpoint 5 at 0x7ffff7e60dc0malloc.c:3051. (2 locations)
(gdbb malloc.c:3056
Breakpoint 6 at 0x7ffff7e60d03malloc.c:3056. (3 locations)
(gdbb malloc.c:3058
Breakpoint 7 at 0x7ffff7e60d13malloc.c:3058. (2 locations)
(gdbb malloc.c:3066
Breakpoint 8 at 0x7ffff7e60e6emalloc.c:3066. (2 locations)
(gdbb vm4_main.cpp:39
Breakpoint 9 at 0x5555555551ddfile vm4_main.cppline 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_tconst void *)
3031        = atomic_forced_read (__malloc_hook);
3032      if (__builtin_expect (hook != NULL0))
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    }

上述代码通过将没什么影响的部分去掉,从而简化了我们的分析过程。这里,有以下几个疑问:

接下来,逐一研究这些问题。

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 = {0x00x00x00x00x00x00x00x00x00x0}, 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 = {0000}, next = 0x7ffff7fafb80 <main_arena>, next_free = 0x0, attached_threads = 1, system_mem = 135168, max_system_mem = 135168}

对比obj1对象申请堆内存前后,main_arena对象的值。我们可以发现,在obj1对象申请堆内存后,main_arena对象中值发生变化的数据成员有:topbinssystem_memmax_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);的作用为

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 = {0x00x00x00x00x00x00x00x00x00x0}, 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 = {0000}, 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 = {0x00x00x00x00x00x00x00x00x00x0}, 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 = {0000}, 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 = {0x00x00x00x00x00x00x00x00x00x0}, 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 = {0000}, 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 0x7ffff7e61850free. (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 != NULL0))
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的起始地址。

这里,有以下几个疑问:

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中分配并返回给用户。


如何在链接期拦截标准库的 malloc

研究过程:

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

malloc
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_tconst 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_tconst 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_tconst 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

malloc
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_tvoid const*)>

从上面的结果中可以看出,现在局部变量hook的值变成了我们自定义函数my_malloc_hook的地址。也就是说,我们成功地在链接期拦截了标准库中的malloc函数。

研究结论:


chunk 的大小有哪些讲究

研究过程:

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_SZMALLOC_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 系统上):

所以,malloc函数中,checked_request2size (bytes, &tbytes)的作用为:

这就解释了上一篇中的遗留问题:obj1.data_所在chunk的内存布局中尾部八字节的作用,即用来凑够 64-bit 系统中最小chunk的大小。

研究结论:

在 64-bit 系统上,glibc 中的malloc函数所分配的chunk的大小必须同时满足:1)chunk的大小 >= 32 字节;2)chunk的大小为 16 字节的整数倍。


理解 malloc_state 结构体中各字段的意义

研究过程:

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;

从上面的注释中可以得出如下两个信息:

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 保存堆的大小(包括已分配的和空闲的)

理解 tcache 机制


引入 tcache 机制的动机

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 机制

编译期打开tcache机制的方法:编译 glibc 时,添加编译选项-DUSE_TCACHE。这样做是因为,tcache相关的代码都定义在自定义的USE_TCACHE预处理命令中。

返回上一级


理解 tcache_perthread_struct 结构体中各字段的意义

研究过程:

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中,数据成员countsentries的作用?

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

从上面的源码和注释,我们可以先这样理解(不一定正确):

2) 分析结构体tcache_entrynextkey字段的意义

结构体tcache_entry中的key字段在 malloc.c 源文件中,由以下三个函数调用了,分别是:_int_freetcache_puttcache_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    }

从上面的源码和注释,我们可以得出如下几个结论:

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 变量的值”的可能性。于是,检查该chunktcache中对应的链表(从头节点开始遍历)中的元素是否已经有该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 系统上,MINSIZEMALLOC_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)。

返回上一级


第一个 chunk 的来龙去脉

研究过程:

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

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, 0sizeof (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 中各字段的意义

研究结论:


为什么在调用 free 函数后,被释放的 chunk 从内存数据来看仍是一个已分配块?

研究过程:

以释放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 0x7ffff7e5f850free. (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

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    }

关于csize2tidxtc_idxtcache->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维护了。


References


下一篇:计算机系统之目录

上一篇:计算机系统篇之虚拟内存(8):理解 glibc malloc 的工作原理(上)

首页