glibc malloc 源码分析

glibc malloc 源码分析

linux给了我们两种类型的系统调用来申请动态内存,分别是brk()mmap()malloc()仅仅是在这二者之上做了一些其他的事情而已,这里从源代码来剖析一下glibc malloc都做了什么。源代码是glibc v2.32版本。

chunk

‘chunk’指的就是malloc分配内存的最小单元,我们来看下它的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
This struct declaration is misleading (but accurate and necessary).
It declares a "view" into memory allowing access to necessary
fields at known offsets from a given base. See explanation below.
*/

struct malloc_chunk {

INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};

首先INTERNAL_SIZE_T其实就是无符号整数size_t:x86-64 linux下,32位操作系统为4字节32位,64位操作系统为64位8字节,用下面的这个宏定义:

1
2
3
#ifndef INTERNAL_SIZE_T
# define INTERNAL_SIZE_T size_t
#endif

这里面的4根指针注释上都强调了只有在free了后才使用,因此使用中的chunk是长这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

这里注意,mchunk_size最后面的3个bit是用来表示这个chunk的一些信息的,意义如下:

  • A表示NON_MAIN_ARENA,记录当前 chunk 是否不属于主线程,1 表示不属于,0 表示属于。
  • M表示IS_MAPPED,记录当前chunk是否由mmap分配的,1表示是,0表示不是。
  • P表示PREV_INUSE,记录物理上相邻的前一个chunk是否正在使用,1表示正在使用,0表示没有。

接下来就是一些chunk的宏函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef MALLOC_ALIGNMENT
#define MALLOC_ALIGNMENT (2 * SIZE_SZ)
#endif

/* conversion from malloc headers to user pointers, and back */

#define chunk2mem(p) ((void*)((char*)(p) + 2*SIZE_SZ))
#define mem2chunk(mem) ((mchunkptr)((char*)(mem) - 2*SIZE_SZ))

//#define offsetof(s,m) ((size_t)&(((s*)0)->m))
/* 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))

其中:

  • chunk2mem(p):偏移2*SIZE_SZ到用户真正使用的数据区

  • MIN_CHUNK_SIZE:chunk的最小size。在CTF wiki的引用里面MIN_CHUNK_SIZE的定义是:

    1
    #define MIN_CHUNK_SIZE (offsetof(struct malloc_chunk, fd_nextsize))

    这里offsetof()函数返回的是一个size_t,大小为结构成员相对于结构开头的偏移量,在64位操作系统下,MIN_CHUNK_SIZE是32,32位操作系统下是16。

  • MINSIZE:申请最小的堆内存大小,展开后和MIN_CHUNK_SIZE一样。(虽然是个无关紧要的细节,但我没看懂为什么要定义相同的MIN_CHUNK_SIZEMINSIZE

检查对齐的宏:

1
2
3
4
5
6
7
8
/* Check if m has acceptable alignment */

#define aligned_OK(m) (((unsigned long)(m) & MALLOC_ALIGN_MASK) == 0)

//SIZE_SZ = sizeof(size_t)
#define misaligned_chunk(p) \
((uintptr_t)(MALLOC_ALIGNMENT == 2 * SIZE_SZ ? (p) : chunk2mem (p)) \
& MALLOC_ALIGN_MASK)

这里可以看出,申请内存大小必须是2*SIZE_SZ的整数倍,否则也会给你对齐到整数倍。

然后是把malloc请求的size转换为对应chunk的size宏和对request size做检查的宏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* pad request bytes into a usable size -- internal version */

#define request2size(req) \
(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) ? \
MINSIZE : \
((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)

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

接下来是对chunk做一些操作的宏,从命名和定义就可以看出具体用途:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/* size field is or'ed with PREV_INUSE when previous adjacent chunk in use */
#define PREV_INUSE 0x1
/* extract inuse bit of previous chunk */
#define prev_inuse(p) ((p)->mchunk_size & PREV_INUSE)
/* size field is or'ed with IS_MMAPPED if the chunk was obtained with mmap() */
#define IS_MMAPPED 0x2
/* check for mmap()'ed chunk */
#define chunk_is_mmapped(p) ((p)->mchunk_size & IS_MMAPPED)
/* 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)
/* Mark a chunk as not being on the main arena. */
#define set_non_main_arena(p) ((p)->mchunk_size |= NON_MAIN_ARENA)
/*
Bits to mask off when extracting size

Note: IS_MMAPPED is intentionally not masked off from size field in
macros for which mmapped chunks should never be seen. This should
cause helpful core dumps to occur if it is tried by accident by
people extending or adapting this malloc.
*/
#define SIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
/* Get size, ignoring use bits */
#define chunksize(p) (chunksize_nomask (p) & ~(SIZE_BITS))
/* Like chunksize, but do not mask SIZE_BITS. */
#define chunksize_nomask(p) ((p)->mchunk_size)
/* Ptr to next physical malloc_chunk. */
#define next_chunk(p) ((mchunkptr) (((char *) (p)) + chunksize (p)))
/* Size of the chunk below P. Only valid if !prev_inuse (P). */
#define prev_size(p) ((p)->mchunk_prev_size)
/* Set the size of the chunk below P. Only valid if !prev_inuse (P). */
#define set_prev_size(p, sz) ((p)->mchunk_prev_size = (sz))
/* Ptr to previous physical malloc_chunk. Only valid if !prev_inuse (P). */
#define prev_chunk(p) ((mchunkptr) (((char *) (p)) - prev_size (p)))
/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s)))
/* extract p's inuse bit */
#define inuse(p) \
((((mchunkptr) (((char *) (p)) + chunksize (p)))->mchunk_size) & PREV_INUSE)
/* set/clear chunk as being inuse without otherwise disturbing */
#define set_inuse(p) \
((mchunkptr) (((char *) (p)) + chunksize (p)))->mchunk_size |= PREV_INUSE
#define clear_inuse(p) \
((mchunkptr) (((char *) (p)) + chunksize (p)))->mchunk_size &= ~(PREV_INUSE)
/* check/set/clear inuse bits in known places */
#define inuse_bit_at_offset(p, s) \
(((mchunkptr) (((char *) (p)) + (s)))->mchunk_size & PREV_INUSE)
#define set_inuse_bit_at_offset(p, s) \
(((mchunkptr) (((char *) (p)) + (s)))->mchunk_size |= PREV_INUSE)
#define clear_inuse_bit_at_offset(p, s) \
(((mchunkptr) (((char *) (p)) + (s)))->mchunk_size &= ~(PREV_INUSE))
/* Set size at head, without disturbing its use bit */
#define set_head_size(p, s) ((p)->mchunk_size = (((p)->mchunk_size & SIZE_BITS) | (s)))
/* Set size/use field */
#define set_head(p, s) ((p)->mchunk_size = (s))
/* Set size at footer (only when chunk is not in use) */
#define set_foot(p, s) (((mchunkptr) ((char *) (p) + (s)))->mchunk_prev_size = (s))

Bin

前面也说了,chunk是用户通过malloc申请内存的最小单元,glibc malloc不会在一个chunk free了以后马上将内存还给操作系统,而是创建了一些数据结构来接管他们,以节省再次申请时的时间,由于时间空间局部性的存在,这种设计一般来说是能起作用的(当然对于生命周期很长的程序这样的设计可能导致一大堆内存释放不掉)。在内部为了管理这些free chunk,glibc使用了一种叫bins的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define NBINS 128
typedef struct malloc_chunk *mbinptr;

/* addressing -- note that bin_at(0) does not exist */
#define bin_at(m, i) \
(mbinptr) (((char *) &((m)->bins[((i) - 1) * 2])) \
- offsetof (struct malloc_chunk, fd))

/* analog of ++bin */
#define next_bin(b) ((mbinptr) ((char *) (b) + (sizeof (mchunkptr) << 1)))

/* Reminders about list directionality within bins */
#define first(b) ((b)->fd)
#define last(b) ((b)->bk)
/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];

这里就能看出来,bins是一个chunk指针组成的数组,而前面chunk的数据结构定义中也有指向前一个free chunk和后一个free chunk的指针(如果这个chunk也是free的话),也就是说,bins实际上就是一个管理free chunk的链表数组。在bins中,如果两个free chunk是物理相邻的,两个chunk就会合并以减少内存碎片,相似地,如果在bins里找不到malloc要的size大小的chunk,那么就会从大chunk中分割出一个符合size要求的chunk来,这个步骤后面的代码会看到。

bins又细分为fastbinsmallbinlargebinunsortedbin,free chunk会根据一些规则被分到这4个组里。

Fast Bin

首先,小size的chunk在free后如果物理相邻就会被合并,但很多程序常常会申请和释放小内存块,要是每次malloc或者free小块都会进行合并和分割就会导致程序变慢,为了照顾着一些很常用的小块内存,fast bins就出现了:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct malloc_chunk *mfastbinptr;
//indexing
#define fastbin(ar_ptr, idx) ((ar_ptr)->fastbinsY[idx])

/* offset 2 to use otherwise unindexable first 2 bins */
#define fastbin_index(sz) \
((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
/* The maximum fastbin request size we support */
#define MAX_FAST_SIZE (80 * SIZE_SZ / 4)

#define NFASTBINS (fastbin_index (request2size (MAX_FAST_SIZE)) + 1)
/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];

NFASTBINS展开以后是10,而根据fastbin_index的定义(这个定义中的-2显然受到了MIN_CHUNK_SIZE的约束),0和1的index不存在,因此fastbin最多cache 8个chunk,根据这个宏可以推出每个index对应的chunk size大小:

index 32位系统(SIZE_SZ=4) 64位系统(SIZE_SZ=8)
0 不存在 不存在
1 不存在 不存在
2 16 32
3 24 48
4 32 64
5 40 80
6 48 96
7 56 112
8 64 128
9 72 144

注意,在fast bins中的free chunk是LIFO的,使用单向链表实现,fast bins能fast也是基于时间空间局部性。在malloc申请一个chunk时,首先就会在fast bins中查找有没有适合的size,如果没用才会进行后面的操作:

1
2
3
/* 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;

BTW,fast bins中的chunk会被标记为使用中,即链表中chunk的PREV_INUSE都会被设置为1,为了防止被合并。

Small bin

顾名思义,small bins就是包含着小size chunk的bins:

1
2
3
4
5
6
7
8
9
10
11
12
#define NBINS             128
#define NSMALLBINS 64
#define SMALLBIN_WIDTH MALLOC_ALIGNMENT
#define SMALLBIN_CORRECTION (MALLOC_ALIGNMENT > 2 * SIZE_SZ)
#define MIN_LARGE_SIZE ((NSMALLBINS - SMALLBIN_CORRECTION) * SMALLBIN_WIDTH)

#define in_smallbin_range(sz) \
((unsigned long) (sz) < (unsigned long) MIN_LARGE_SIZE)

#define smallbin_index(sz) \
((SMALLBIN_WIDTH == 16 ? (((unsigned) (sz)) >> 4) : (((unsigned) (sz)) >> 3))\
+ SMALLBIN_CORRECTION)

从源码中可以看出,一个chunk是small还是large,是由宏MIN_LARGE_SIZE决定的,这个size在64位操作系统上是1024,在32位系统上是512,小于它的被定义为small chunk,大于等于的是large chunk。

而根据smallbin_index(sz)的indexing规则,32位系统下(SMALLBIN_WIDTH != 16)为sz/8,64位下为sz/16,在已知sz必须和2 * SIZE_SZ(64位操作系统为16,32位操作系统为8)对齐的情况下,那么我们就能反推出small chunk总共的index数量为1024/16=64,正好和NSMALLBINS对上了!

然后我们就可以得到不同small bin的index下,每个size的chunk的一一对应关系,注意MIN_CHUNK_SIZE规定了最小的size,因此index是1和0的情况是不存在的,所以实际情况上,small bins有62个index

index 32位系统(SIZE_SZ=4) 64位系统(SIZE_SZ=8)
2 16 32
3 24 48
4 32 64
x 24x 28x
63 504 1008

fast bin是和small bin的范围有重合的,实际上,fast bins就是small bins的cache

Large Bin

搞懂了small bins,large bins就很简单了,前面说过,比MIN_LARGE_SIZE大的chunk都称为large bins,它们是如何indexing的就看源代码怎么定义的了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#define largebin_index_32(sz)                                                \
(((((unsigned long) (sz)) >> 6) <= 38) ? 56 + (((unsigned long) (sz)) >> 6) :\
((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) :\
((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :\
((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :\
((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :\
126)

#define largebin_index_32_big(sz) \
(((((unsigned long) (sz)) >> 6) <= 45) ? 49 + (((unsigned long) (sz)) >> 6) :\
((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) :\
((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :\
((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :\
((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :\
126)

// XXX It remains to be seen whether it is good to keep the widths of
// XXX the buckets the same or whether it should be scaled by a factor
// XXX of two as well.
#define largebin_index_64(sz) \
(((((unsigned long) (sz)) >> 6) <= 48) ? 48 + (((unsigned long) (sz)) >> 6) :\
((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) :\
((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :\
((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :\
((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :\
126)

#define largebin_index(sz) \
(SIZE_SZ == 8 ? largebin_index_64 (sz) \
: MALLOC_ALIGNMENT == 16 ? largebin_index_32_big (sz) \
: largebin_index_32 (sz))

#define bin_index(sz) \
((in_smallbin_range (sz)) ? smallbin_index (sz) : largebin_index (sz))

看一下这个嵌套三元表达式的宏就知道,large bins一共分为了6组,每组的index个数可以从移位算符得出,算一算就可以知道它们一一对应到表里,glibc内存管理ptmalloc源码分析里有完整的数据,这里给出CTF wiki的比较简短的总结:

组号 index个数 公差
1 6 64
2 16 512
3 8 4096
4 4 32768
5 2 262144
6 1 不限制

Unsorted Bin

源码定义如下:

1
2
/* The otherwise unindexable 1-bin is used to hold unsorted chunks. */
#define unsorted_chunks(M) (bin_at (M, 1))

可见unsorted bin定义在了bins的第一个index下,因此unsorted bin只是一个链表。

Top Chunk

glibc对top chunk定义和描述如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
Top

The top-most available chunk (i.e., the one bordering the end of
available memory) is treated specially. It is never included in
any bin, is used only if no other chunk is available, and is
released back to the system if it is very large (see
M_TRIM_THRESHOLD). Because top initially
points to its own bin with initial zero size, thus forcing
extension on the first malloc request, we avoid having any special
code in malloc to check whether it even exists yet. But we still
need to do so when getting memory from system, so we make
initial_top treat the bin as a legal but unusable chunk during the
interval between initialization and the first call to
sysmalloc. (This is somewhat delicate, since it relies on
the 2 preceding words to be zero during this interval as well.)
*/

/* Conveniently, the unsorted bin can be used as dummy top on first call */
#define initial_top(M) (unsorted_chunks (M))

根据注释描述,所谓的top chunk就是位于可用堆内存地址最高位的chunk,它不属于任何bin,只有当没有chunk可用时它才会向系统去申请扩展heap的可用区域。为防止被合并,top chunk的prev_inuse始终为1。初始情况时,unsorted chunks用作top chunk。

现在总结一下,宏NBINS告诉我们bins一共有108个入口,small bins有62个,large bins一共有63个,加起来125个bin,根据bin的indexing宏bin_at的定义,bin[0]bin[127]是不存在的,因此bin[1]就是top chunk,也是unsorted bin,加起来总共126个。

BTW,bins的定义是:

1
mchunkptr bins[NBINS * 2 - 2];

也就是实际size有NBINS * 2 - 2一共254个mchunkptr,而chunk实例的是6个mchunkptr的大小,这怎么存的下呢?但我们注意到,bins中的chunk是头节点,那么chunk中的mchunk_prev_sizemchunk_size是没有意义的!而fd_nextsizebk_nextsize只有large chunk才会用到,那么出于节省内存的想法,我们只需要2个mchunkptr,总共需要的就是126*2=254个mchunkptr的大小,正好对上了!

bins

总之,glibc的malloc使用的内存管理方法就是链表数组的内存池,和gnu C++远古版本std allocator是相同的思想,因此在后面版本的gnu C++里面默认allocator都是封装malloc,如果看了侯捷的STL源码剖析,别被书里推崇无比的内存池allocator误导了。

其他核心结构

malloc_state

定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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;
};

这里就可以看到,前面所说的bins是malloc_state的一部分,因此一个malloc_state的实例就是一个分配区域,下面一一说明这些变量:

flags是一些标志位,它的用途从后面的宏定义就可以看出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
NONCONTIGUOUS_BIT indicates that MORECORE does not return contiguous
regions. Otherwise, contiguity is exploited in merging together,
when possible, results from consecutive MORECORE calls.

The initial value comes from MORECORE_CONTIGUOUS, but is
changed dynamically if mmap is ever used as an sbrk substitute.
*/

#define NONCONTIGUOUS_BIT (2U)

#define contiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) == 0)
#define noncontiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) != 0)
#define set_noncontiguous(M) ((M)->flags |= NONCONTIGUOUS_BIT)
#define set_contiguous(M) ((M)->flags &= ~NONCONTIGUOUS_BIT)

这里就是用flags的第2位来判断MORECORE是否返回了连续的虚拟地址空间,0为是,1为否。实际上MORECORE就是系统调用sbrk(),只是经过了重重包装:

1
2
3
4
5
6
7
8
9
10
11
12
#define MORECORE         (*__morecore) 
void * __default_morecore (ptrdiff_t);
void *(*__morecore)(ptrdiff_t) = __default_morecore;
void *
__default_morecore (ptrdiff_t increment)
{
void *result = (void *) __sbrk (increment);
if (result == (void *) -1)
return NULL;

return result;
}

have_fastchunk用来表示这个分配区域是否有fast chunk。以前版本的glibc malloc是用flags的第1位来判断是否有fast chunk的,定义宏的方法和contiguous是一样的,但我看的这个版本是在malloc_state定义了一个have_fastchunk来判断,感觉新的版本有点浪费内存资源了。

fastbinsY,前面已经说过了,存储fast chunk链表指针的数组。

top,指向bins top chunk的指针。

last_remainder,指向chunk的指针, 分配区上次分配 small chunk 时,从一个 chunk 中分 裂出一个 small chunk 返回给用户, 分裂后的剩余部分形成一个 chunk,last_remainder 就是 指向的这个 chunk 。

bins:前面说过了,存储chunk的链表指针数组,分为unsorted bin,fast bin,small bin, large bin,bin[0]不存在,bin[1]是unsorted bin。

binmap:一个int数组,关于它的用途,可以看下面部分的代码:

1
2
3
4
5
6
7
8
9
10
11
/* Conservatively use 32 bits per map word, even if on 64bit system */
#define BINMAPSHIFT 5
#define BITSPERMAP (1U << BINMAPSHIFT)
#define BINMAPSIZE (NBINS / BITSPERMAP)

#define idx2block(i) ((i) >> BINMAPSHIFT)
#define idx2bit(i) ((1U << ((i) & ((1U << BINMAPSHIFT) - 1))))

#define mark_bin(m, i) ((m)->binmap[idx2block (i)] |= idx2bit (i))
#define unmark_bin(m, i) ((m)->binmap[idx2block (i)] &= ~(idx2bit (i)))
#define get_binmap(m, i) ((m)->binmap[idx2block (i)] & idx2bit (i))

这里可以看出BINMAPSIZE的值是128/32=4,我们知道int是32位,那么binmap实际上就是一个128位的buffer,这些宏就是在定义每一位对应每一个bins的映射关系,ptmalloc就使用这些位来标记对应bin中是否有free chunk。

next:一根指向下一个分配区的指针。

next_free:一根指向下一个free分配区的指针。

system_mem:记录当前分配去已经分配的内存大小。

max_system_mem:记录当前分配去最大能分配的内存大小。

malloc_par

定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
struct malloc_par
{
/* Tunable parameters */
unsigned long trim_threshold;
INTERNAL_SIZE_T top_pad;
INTERNAL_SIZE_T mmap_threshold;
INTERNAL_SIZE_T arena_test;
INTERNAL_SIZE_T arena_max;

/* Memory map support */
int n_mmaps;
int n_mmaps_max;
int max_n_mmaps;
/* the mmap_threshold is dynamic, until the user sets
it manually, at which point we need to disable any
dynamic behavior. */
int no_dyn_threshold;

/* Statistics */
INTERNAL_SIZE_T mmapped_mem;
INTERNAL_SIZE_T max_mmapped_mem;

/* First address handed out by MORECORE/sbrk. */
char *sbrk_base;

#if USE_TCACHE
/* Maximum number of buckets to use. */
size_t tcache_bins;
size_t tcache_max_bytes;
/* Maximum number of chunks in each bucket. */
size_t tcache_count;
/* Maximum number of chunks to remove from the unsorted list, which
aren't used to prefill the cache. */
size_t tcache_unsorted_limit;
#endif
};

各个变量的意义如下(摘自ptmalloc源码剖析):

trim_threshold字段表示收缩阈值,默认为 128KB,当每个分配区的 top chunk 大小大于 这个阈值时,在一定的条件下,调用 free 时会收缩内存,减小 top chunk 的大小。由于 mmap 分配阈值的动态调整,在 free 时可能将收缩阈值修改为 mmap 分配阈值的 2 倍,在 64 位系 统上, mmap 分配阈值最大值为 32MB,所以收缩阈值的最大值为 64MB,在 32 位系统上, mmap 分配阈值最大值为 512KB,所以收缩阈值的最大值为 1MB。 收缩阈值可以通过函数 mallopt()进行设置。

top_pad 字段表示在分配内存时是否添加额外的 pad,默认该字段为 0。 mmap_threshold 字段表示 mmap 分配阈值,默认值为 128KB,在 32 位系统上最大值为 512KB, 64 位系统上的最大值为 32MB,由于默认开启 mmap 分配阈值动态调整,该字段的 值会动态修改,但不会超过最大值。

arena_testarena_max 用于 PER_THREAD 优化,在 32 位系统上 arena_test默认值为 2, 64 位系统上的默认值为 8, 当每个进程的分配区数量小于等于 arena_test 时,不会重用已有 的分配区。为了限制分配区的总数,用 arena_max 来保存分配区的最大数量,当系统中的分 配区数量达到 arena_max,就不会再创建新的分配区,只会重用已有的分配区。 这两个字段 都可以使用 mallopt()函数设置。

n_mmaps 字段表示当前进程使用 mmap()函数分配的内存块的个数。 n_mmaps_max 字段表示进程使用 mmap()函数分配的内存块的最大数量,默认值为 65536,可以使用 mallopt()函数修改。 max_n_mmaps字段表示当前进程使用 mmap()函数分配的内存块的数量的最大值,有关 系 n_mmaps <= max_n_mmaps 成立。 这个字段是由于 mstats()函数输出统计需要这个字段。 no_dyn_threshold 字段表示是否开启 mmap 分配阈值动态调整机制,默认值为 0,也就 是默认开启mmap 分配阈值动态调整机制。 pagesize 字段表示系统的页大小,默认为 4KB。 mmapped_memmax_mmapped_mem 都用于统计mmap 分配的内存大小,一般情况 下两个字段的值相等, max_mmapped_mem用于mstats()函数。 max_total_mem 字段在单线程情况下用于统计进程分配的内存总数。 sbrk_base字段表示堆的起始地址。

References:

[1]. https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/heap_structure-zh

[2].glibc内存管理ptmalloc源码分析

 wechat
scan and following my wechat official account.