計算機系統篇之虛擬記憶體(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/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 元)