1. 程式人生 > 其它 >計算機系統篇之虛擬記憶體(9):理解 glibc malloc 的工作原理(中)

計算機系統篇之虛擬記憶體(9):理解 glibc malloc 的工作原理(中)

技術標籤:# 虛擬記憶體glibc malloctcache

計算機系統篇之虛擬記憶體(9):理解 glibc malloc 的工作原理(中)

Author: stormQ

Created: Wednesday, 25. November 2020 10:52PM

Last Modified: Sunday, 13. December 2020 04:38PM



再探 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 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/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: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物件中值發生變化的資料成員有:topbinssystem_memmax_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 元)

在這裡插入圖片描述