ptmalloc2涉及的基础知识与基本数据结构

发布时间 2023-11-07 13:50:38作者: 无敌超人强

随笔来源:ctfwiki CSDN

本随笔只为记录分析总结的自己学习的结论,方便未来回顾,以及为他人提供一个理解的思路,不保证正确。如有谬误,请大家指出。

1.堆相关的操作

malloc:返回对应大小内存块的指针,当描述大小的参数为0时,返回最小大小的内存块,即4*size_sz,在32位中size_sz=4,在64位中size_t=8。另外需要注意的是,描述大小的参数为无符号整数。

free:释放指针对应的内存块,当指针为NULL时不执行任何操作。在多数情况下,释放一个极大的内存空间时(如mmap申请的空间、较大的top_chunk),释放的空间不会交还给堆管理器,而是直接返还给系统。double free是未定义的行为,会出现各种奇怪的情况。

realloc:可以粗略理解为对malloc分配内存的调整,在原分配的块相邻地址有大小合适的空闲块时,会进行块的合并,将合并后的块返回,否则会另取一块内存,将原有内存的内容拷贝过去,然后释放掉原内存。

calloc:与malloc类似,但是calloc会将分配的内存初始化为全0。

new、delete:二者是c++中常用的分配内存关键字,内部也是由malloc和free实现,但是增加了构造函数和析构函数。

2.malloc和free涉及的系统调用

在ptmalloc2中,向操作系统申请内存的系统调用有两种方式,通过这两种方式从系统处申请得到的内存,只是系统分配的虚存,并没有对应的物理页,在使用时才会通过缺页中断(小错误)获取物理页。在内存释放后,一般不会返还系统,而是交由堆管理器管理。

image-20231104200422508

​ 进程空间的内存分布图

1)brk

操作系统提供了brk系统调用,通过向高地址抬升brk结束地址来申请内存。brk申请的内存位于BSS段结尾附近(没有PIE开启时,brk开始地址等同于BSS段结尾地址;如果开启PIE,为BSS段结尾地址加一个随机偏移),向高地址生长。main_arena优先使用brk获取内存,其余的线程arena不使用此方法申请内存。

2)mmap

mmap系统调用可以创建独立的匿名映射段,用于堆空间的分配。申请的内存位于内存映射区域(栈区的起始地址之前),向低地址生长。内存映射区通常还有动态库对应的映射文件与匿名映射段交错分布。大多数时候(各arena使用mmap扩容的情况不在此列),直接调用mmap获取用户需要的内存时要求请求的内存大于一个阈值(通常是128kb),且mmap的数量小于一个值。

3.堆中的数据结构

从大到小依次包括malloc_state、heap_info、bin、top_chunk、last_reminder和chunk、tcache等主要结构。

1)malloc_state结构

struct malloc_state {
    /* Serialize access.  */
    __libc_lock_define(, mutex);//访问该arena临界区的互斥锁

    /* Flags (formerly in max_fast).  */
    int flags;//用作标志功能,例如,低3位由高到低分别表示检测arena是否损坏、检测arena是否连续、检测fast bin中是否纯在chunk。

    /* Fastbins */
    mfastbinptr fastbinsY[ NFASTBINS ];//放置fastbin的表头的数组,管理一些空闲chunk。数组大小默认为8,最大可以修改为10,会在后面详细描述。

    /* Base of the topmost chunk -- not otherwise kept in a bin */
    mchunkptr top;//用于描述此arena区域的最高地址的一个chunk,该chunk性质与其他chunk不同,不会被各个bin收纳,且其标志位pre_inuse总置1,防止其意外合并其低地址前的chunk。通常来说,它是arena中最大的chunk。在arena第一次建立时,系统分配的内存通常大于需要的内存,以防止频繁的内存申请,这就会发生内存的切分,分割后低地址的chunk分配给用户使用,高地址chunk作为最初的top_chunk,而不是返还给系统。此后top_chunk的每次切分与第一次相似。

    /* The remainder from the most recent split of a small request */
    mchunkptr last_remainder;//当用户需要一块内存时,堆管理器并不总是能找到一块大小完全相同的内存,如果找到的内存比申请的内存更大,就需要切分,切分剩下的空闲chunk就是一个reminder,最后一个产生的reminder就是last_reminder,top_chunk的切分不纳入此列。last_remainder也不会放入任何bin中。

    /* Normal bins packed as described above */
    mchunkptr bins[ NBINS * 2 - 2 ];//放置包括unsorted bin、small bin和large bin在内的多个bin的表头指针的数组。一个bin是一个链表,在bins中的bin都是双向链表,记录大小相同或相近的空闲chunk,在后面会详细描述。

    /* Bitmap of bins, help to speed up the process of determinating if a given bin is definitely empty.*/
    unsigned int binmap[ BINMAPSIZE ];//用于记录bins中对应不同大小的bin是否空闲,bins中一共127个bin,在此处对应4个32位(4字节)的map,即BINMAPSIZE为4。每个bin对应一个map中的一位,当bin空闲时,对应bin置为0,否则置为1。对不同位的置0并不是及时更新,而是在malloc寻找chunk时,发现bin中没有chunk再更新。

    /* Linked list, points to the next arena */
    struct malloc_state *next;//指向下一个arena,最终形成一个环形单链表。

    /* Linked list for free arenas.  Access to this field is serialized
       by free_list_lock in arena.c.  */
    struct malloc_state *next_free;//指向下一个空闲的arena,因为线程arena一旦产生,不会减少。最终形成一个单链表。

    /* 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;//与该arena相关的线程数,为0时表明该arena空闲。

    /* Memory allocated from the system in this arena.  */
    INTERNAL_SIZE_T system_mem;//该arena中系统分配的内存大小。
    INTERNAL_SIZE_T max_system_mem;
};

该结构用于描述一个arena区域,arena区域指针对某个线程的堆分配区。一个进程中的arena区域数量是有上限的,因为当arena超过一个值后,更多的arena没有意义,不能提高并发执行的效率。在32位系统中,该值为处理器核数*2,在64位系统中,该值为处理器核数*8。当线程数超过这个限制后,会多个线程共用一个arena。需要注意的是,在所有用于描述arena的malloc_state结构体中,main_arena是libc.so动态库映射中的一个全局变量,而其余的线程arena对应的malloc_state结构体位于第一次调用new_heap函数申请的堆空间中。

2)heap_info结构

    typedef struct _heap_info {
    	mstate ar_ptr; //此heap对应的arena
    	struct _heap_info *prev; //此heap对应的同一个arena中的前一个heap
    	size_t size; //heap的大小(往往不等同于new_heap时申请到的实际大小,即HEAP_MAX_SIZE
    	size_t mprotect_size; //heap中含有读写权限的内存大小,小于等于size
    	/*  Make sure the following data is properly aligned, particularly
    	 that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of
    	 MALLOC_ALIGNMENT. */
    	char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK];//用于对齐,将6*SIZE_SZ,即heap_info+2*SIZE_SZ按MALLOC_ALIGN_MASK对齐,SIZE_SZ的大小与指针类型的大小相同,MALLOC_ALIGN_MASK=2*SIZE_SZ-1,MALLOC_ALIGNMENT=MALLOC_ALIGN_MASK+1。在32位下解释表达式的含义:计算机中负数是补码表示,-6*SIZE_SZ=2^32(2的32次方)-6*SIZE_SZ。-6 * SIZE_SZ&MALLOC_ALIGN_MASK为保留掩码对应的低位,相当于对MALLOC_ALIGNMENT取余。6*SIZE_SZ对MALLOC_ALIGNMENT取余,表示在对齐后heap_info结构多出的字节数。显然2的32次方必定整除MALLOC_ALIGNMENT,用r1、r2表示两个余数,即2^32-6*SIZE_SZ=n1*MALLOC_ALIGNMENT+r1,6*SIZE_SZ=n2*MALLOC_ALIGNMENT+r2,2^32=n3*MALLOC_ALIGNMENT=(n1+n2)*MALLOC_ALIGNMENT+r1+r2。显然有r1+r2=n4*MALLOC_ALIGNMENT。由于0<r1<MALLOC_ALIGNMENT,0<r2<MALLOC_ALIGNMENT,即0<r1+r2<2*MALLOC_ALIGNMENT,因此r1+r2=MALLOC_ALIGNMENT。即-6 * SIZE_SZ & MALLOC_ALIGN_MASK表示为按照MALLOC_ALIGNMENT对齐还需要填充的字节数。
    } heap_info;

heap_info用于描述向系统中每次申请的堆的信息,显然一个线程不可能都只向系统申请一次内存,申请的内存总有用完的时候,就可能需要多次向系统申请堆,因此使用heap_info对每次从系统处申请的内存进行管理。main_arena没有heap_info结构,因为main_arena是唯一能够使用brk方法申请内存的堆分配区,这使得它不会产生太多不连续的堆区。

3)chunk结构

struct malloc_chunk {

  INTERNAL_SIZE_T      prev_size;  //用于描述该chunk物理相邻的前一个chunk的大小,当且仅当前一个chunk空闲时有效,否则该值没有意义。因此,在物理相邻的前一个块非空闲时,此字段可以作为用户申请的mem的一部分进行复用,以提高空间利用率。
  INTERNAL_SIZE_T      size;  //用于描述该chunk的大小,注意不是用户分配的mem的大小。由于size按照MALLOC_ALIGNMENT对齐,低三位必定为0,因此作为标志位使用。标志位从高到低分别为:NON_MAIN_ARENA,记录当前 chunk 是否不属于主线程,1 表示不属于,0 表示属于;IS_MAPPED,记录当前 chunk 是否是直接由 mmap 分配的;PREV_INUSE,记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。top_chunk的P位也会置1,防止意外的合并。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并。
//后四个字段仅在chunk空闲时有效,因此这些字段与用户申请使用的空间进行复用,不需要占用额外的空间。
  struct malloc_chunk* fd; //当此chunk空闲时,指向在bin中相邻的下一个chunk
  struct malloc_chunk* bk; //当此chunk空闲时,指向在bin中相邻的上一个chunk

  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; //当此chunk空闲且在large bin中时,指向链表中下一个大小不同且更小的块。因为large bin与其他bin不同,放入其中的chunk大小不一,按fd顺序为从大到小排列。
  struct malloc_chunk* bk_nextsize;//当此chunk空闲且在large bin中时,指向链表中上一个大小不同且更大的块。
};

malloc_chunk是用于管理用户申请的块的数据结构,用户申请的可使用的空间在malloc_chunk偏移两个字段后,fd、bk、fd_nextsize、bk_nextsize在此chunk使用时没有意义,只有当chunk空闲后,才被堆管理器用作管理的字段,ctfwiki中的这幅图很形象。

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)                      .
next    .                                                               |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             (size of chunk, but used for application data)    |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of next chunk, in bytes                |A|0|1|
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

为了提高访问效率,chunk的大小需要按照MALLOC_ALIGNMENT对齐,即必定为MALLOC_ALIGNMENT的整数倍。chunk在堆分配器中要正常循环,至少需要pre_size、size、fd、bk四个字段,因此chunk的最小大小为4*SIZE_SZ。

下面为一些计算chunk相关的宏

#define aligned_OK(m) (((unsigned long) (m) & MALLOC_ALIGN_MASK) == 0)//检测用户使用的mem地址是否对齐

#define misaligned_chunk(p)                                                    \
    ((uintptr_t)(MALLOC_ALIGNMENT == 2 * SIZE_SZ ? (p) : chunk2mem(p)) &       \
     MALLOC_ALIGN_MASK)//返回用户的mem地址未对齐的字节数,当MALLOC_ALIGNMENT == 2 * SIZE_SZ,要求的对齐大小恰好等于chunk头(chunk结构的前2个字段),不需要特意计算mem地址来比较对齐。但是当不等时,需要chunk2mem(p)计算出chunk地址对应的用户mem地址进行比较。

#define MINSIZE                                                                \
    (unsigned long) (((MIN_CHUNK_SIZE + MALLOC_ALIGN_MASK) &                   \
                      ~MALLOC_ALIGN_MASK))//MINSIZE为MIN_CHUNK_SIZE按MALLOC_ALIGNMENT向上对齐后的大小,目前二者是相等的

#define REQUEST_OUT_OF_RANGE(req)                                              \
    ((unsigned long) (req) >= (unsigned long) (INTERNAL_SIZE_T)(-2 * MINSIZE))//判断请求的字节数是否超出了能表示的范围。此处的用于比较的参考大小是一个极大的无符号数(32位机中为2^32-2 * MINSIZE),用户请求的mem大小req不能大于该无符号数。这样比较是为了防止分配块的大小在经过多次对齐等操作后发生溢出,导致大小变为一个极小的值。此处是为了给其他代码的操作留足空间,因此预留了2 * MINSIZE的阈值。

#define request2size(req)                                                      \
    (((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE)                           \
         ? MINSIZE                                                             \
         : ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)//将用户请求的mem大小转为实际chunk要求的大小。比较的语句(req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE在此处的作用等同于(req) + SIZE_SZ < MINSIZE,当比较的条件满足时,取chunk对齐后的最小值MINSIZE。否则将(req) + SIZE_SZ按MALLOC_ALIGNMENT向上对齐。之所以用(req) + SIZE_SZ进行比较是因为下一个物理相邻的chunk的pre_size字段可以复用,因此算上chunk头也只需要一个字段的大小。

/*  Same, except also perform argument check */

#define checked_request2size(req, sz)                                          \
    if (REQUEST_OUT_OF_RANGE(req)) {                                           \
        __set_errno(ENOMEM);                                                   \
        return 0;                                                              \
    }                                                                          \
    (sz) = request2size(req);//检查用户请求的mem大小并返回实际需要的chunk大小

下面为一些与chunk的字段相关的宏,都比较简单,只放在这里作为参考,方便分析后续代码时查询对应宏的细节,不进行分析。

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

/* 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)
    
    /* Set size at head, without disturbing its use bit */
// SIZE_BITS = 7
#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))
 
 /* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s) ((mchunkptr)(((char *) (p)) + (s)))

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

4)bin结构

bin是用于管理空闲chunk的链表,在ptmalloc2中,bin大致分为4类,fast bin、unsorted bin、small bin和large bin。其中,除fast bin外的另外三个bin被维护在同一数组bins中,bins中的每个bin都是一个双向循环链表。fast bin有一个单独的数组fastbinsY。所有的bin都有一个特征,即物理相邻的chunk在链表中不能相邻。需要注意的是,为了简化在双链表中的使用,表头类型与链表中其他节点的类型相同,统一为chunk类型,但是为了节约空间,表头中实际只使用了fd与bk两个字段,另外的空间被数组中其他表头复用。此外,在bins数组的声明中,这并不是一个chunk类型的数组,仅仅只是这样使用,数组的实际类型为chunk的指针类型,表头的一个chunk实际上对应数组中的两个chunk指针。以 32 位系统为例,bins 前 3项的含义如下:

含义 bin1 的 fd/bin2 的 prev_size bin1 的 bk/bin2 的 size bin2 的 fd/bin3 的 prev_size
bin 下标 0 1 2

可以看到,虽然在此处每个元素看似都有两种含义,但是实际上每个下标元素的含义都取fd或bk(注意,此处的数组下标与bin中的index是两回事)。

下面是一些bin中通用的宏:

/* 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))//获取arena对应的bins中索引为i的bin的地址。需要注意,bins中bin的索引是1开始,因此对应到数组中的下标是2*(i-1),要减去fd字段到chunk头的偏移的原因是,如果将表头看作chunk类型,其开始地址并不是bins,而是bins-offsetof(struct malloc_chunk, fd)。

/* analog of ++bin */
//获取下一个bin的地址
#define next_bin(b) ((mbinptr)((char *) (b) + (sizeof(mchunkptr) << 1)))

/* Reminders about list directionality within bins */
// 这两个宏可以用来遍历bin
// 获取 bin 的位于链表头的 chunk
#define first(b) ((b)->fd)
// 获取 bin 的位于链表尾的 chunk
#define last(b) ((b)->bk)

bins中的数组分布如下:

1.索引在1处的为unsorted bin,放在其中的chunk没有排序,比较杂乱,可以认为是临时存放空闲chunk的缓冲区,寻找chunk时bin内部的遍历顺序为FIFO,放入其中的chunk来源为释放一个不在fast bin范围的chunk、较大的chunk分割剩下的reminder、fast bin中的chunk在执行malloc_consolidate后产生的chunk。

2.索引在2到63的为small bin,共62个bin,每个bin与其存储的chunk的大小为chunk_size = 2 * SIZE_SZ *index,下面为具体数字。

下标 SIZE_SZ=4(32 位) SIZE_SZ=8(64 位)
2 16 32
3 24 48
4 32 64
5 40 80
x 24x 28x
63 504 1008

small bin对应的宏如下:

#define NSMALLBINS 64//NSMALLBINS为small bins数量,当然这里根据数目来看应该是第一个large bin的索引
#define SMALLBIN_WIDTH MALLOC_ALIGNMENT
// 是否需要对small bin的下标进行纠正
#define SMALLBIN_CORRECTION (MALLOC_ALIGNMENT > 2 * SIZE_SZ)//有SIZE_SZ=4,MALLOC_ALIGNMENT=16的情况,这个用于对这种情况进行修正,符合情况时占用一个index的修正值

#define MIN_LARGE_SIZE ((NSMALLBINS - SMALLBIN_CORRECTION) * SMALLBIN_WIDTH)//用于small bin的index总数是固定的,最大为63,且修正的index不能使用,因此此处要减去修正值来计算large bin的最小大小。
//判断chunk的大小是否在small bin范围内
#define in_smallbin_range(sz)                                                  \
    ((unsigned long) (sz) < (unsigned long) MIN_LARGE_SIZE)
// 根据chunk的大小得到small bin对应的索引。
#define smallbin_index(sz)                                                     \
    ((SMALLBIN_WIDTH == 16 ? (((unsigned) (sz)) >> 4)                          \
                           : (((unsigned) (sz)) >> 3)) +                       \
     SMALLBIN_CORRECTION)//根据大小计算index,SMALLBIN_WIDTH可能为16或8,计算后加上修正值

small bin寻找chunk时内部的遍历顺序为FIFO,其中chunk的来源为malloc时,在unsorted bin寻找合适的chunk的过程中,如果unsorted bin中遍历到的chunk不为合适的chunk,且该chunk大小在small bin的范围内,则该chunk会被移入small bin中。

3.索引在64到127的为large bin,共64个bin

large bin与small bin不同,放入其中的一个bin的chunk大小不一定相同,而是在一定范围内,这个范围称为公差,公差与32位还是64位无关。此外large bin根据公差不同,分为6组,每组中的bin数目不同,公差最小的组bin数目最多,具体如下(此外,large bin还有一个起始的bin,用在开头,由于small bin中的限制,它相距最后一个small bin的大小差仍为2*SIZE_SZ):

数量 公差
1 32 64B
2 16 512B
3 8 4096B
4 4 32768B
5 2 262144B
6 1 不限制

根据chunk大小决定对应的bin的index的宏如下:

#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)//这三个宏都很类似,原理也很简单,仅以此为例分析。对于第一层,每个相邻索引的公差统一为2^6,因此在除以2^6后,size对应的公差间隔才被映射到索引间隔,即令公差间隔为i,索引间隔为i,有i=f(y)=y/(2^6)。在映射完成后,只需要找到一个固定的x值,满足x+i为规定的索引值,这个是很容易的,完全可以从规定的索引值和i去倒推x。举例,large bin的最小大小为512字节,512>>6=8,其对应索引为64,可倒推出x为64-8=56,与代码中一致。当然,这个算法只适用于公差为64的层,因此有一个范围限制,必须小于512+(2^5)*(2^6),即(size>>6)<40。这个范围大小似乎与代码中(size>>6)<=38有些不同,实际上代码中只是把这个范围中少去的一部分交由下一层计算,整体逻辑是一样的。

#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))//这里选择应该用哪种方式计算index。需要思考的是究竟什么时候会出现MALLOC_ALIGNMENT==16&&SIZE_SZ==4的情况,显然large bin和small bin的索引计算都考虑了这种情况。

large bin中,每个bin的chunk都按照fd从大到小的顺序排列,在连续排列的多个大小相同的chunk中,第一个chunk会记录下一个更小的chunk和上一个更大的chunk的位置(fd_nextsize和bk_nextsize),这也会构成一个循环链表。

除bins外,还有一个存储fast bin的数组,为fastbinsY,在32位中,其默认支持的最大mem空间为64(注意,是实际的mem空间大小,即除开chunk头的大小,不是chunk大小,也不是考虑pre_size字段复用后的mem大小),但是最大可以支持到80。也就是说fast bin最多可以支持的bin有10个,从8到80。fast bin 产生的初衷是为了在寻找较小的chunk时,更加快速的获取合适的chunk,而不是将时间浪费在chunk的分割和合并上。为了达成这个目的,fast bin仅使用fd链接为单链表,且fast bin中的chunk的使用位都被置1(即物理上的下一个chunk的PREV_INUSE位,这是为了防止意外合并),fast bin中取chunk遵守局部性原则(取chunk按照FILO进行)。fastbin中的chunk来源主要为释放的大小在此范围内的chunk,fast bin中的chunk主要去向包括作为大小刚好的chunk提供给用户和执行malloc_consolidate后放入unsorted bin。

fast bin相关的宏如下:

#define NFASTBINS (fastbin_index(request2size(MAX_FAST_SIZE)) + 1)

#ifndef DEFAULT_MXFAST
#define DEFAULT_MXFAST (64 * SIZE_SZ / 4)//默认的max fastbin请求大小,32位下为64
#endif

/* The maximum fastbin request size we support */
#define MAX_FAST_SIZE (80 * SIZE_SZ / 4)//真正支持的max fastbin请求大小

/*
   Since the lowest 2 bits in max_fast don't matter in size comparisons,
   they are used as flags.
 */

/*
   FASTCHUNKS_BIT held in max_fast indicates that there are probably
   some fastbin chunks. It is set true on entering a chunk into any
   fastbin, and cleared only in malloc_consolidate.

   The truth value is inverted so that have_fastchunks will be true
   upon startup (since statics are zero-filled), simplifying
   initialization checks.
 */
//判断分配区是否有 fast bin chunk,1表示没有
#define FASTCHUNKS_BIT (1U)
//分别为检测和设置fastchunk标志位的宏,设置时都使用的原子性的操作。
#define have_fastchunks(M) (((M)->flags & FASTCHUNKS_BIT) == 0)
#define clear_fastchunks(M) catomic_or(&(M)->flags, FASTCHUNKS_BIT)
#define set_fastchunks(M) catomic_and(&(M)->flags, ~FASTCHUNKS_BIT)

/*
   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.
 */
// MORECORE是否返回连续的内存区域。
// 主分配区中的MORECORE其实为sbr(),默认返回连续虚拟地址空间
// 非主分配区使用mmap()分配大块虚拟内存,然后进行切分来模拟主分配区的行为
// 而默认情况下mmap映射区域是不保证虚拟地址空间连续的,所以非主分配区默认分配非连续虚拟地址空间。
#define NONCONTIGUOUS_BIT (2U)
//检测和设置NONCONTIGUOUS标志位
#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)

/* ARENA_CORRUPTION_BIT is set if a memory corruption was detected on the
   arena.  Such an arena is no longer used to allocate chunks.  Chunks
   allocated in that arena before detecting corruption are not freed.  */
//标志arena是否正常,在此标志位被置位后此arena将不再用于分配chunk,此外,分配到此arena中的chunk在检测到损坏前不会被释放。
#define ARENA_CORRUPTION_BIT (4U)
//检测和设置 ARENA_CORRUPTION标志位
#define arena_is_corrupt(A) (((A)->flags & ARENA_CORRUPTION_BIT))
#define set_arena_corrupt(A) ((A)->flags |= ARENA_CORRUPTION_BIT)

/*
   Set value of max_fast.
   Use impossibly small value if 0.
   Precondition: there are no existing fastbin chunks.
   Setting the value clears fastchunk bit but preserves noncontiguous bit.
 */
//设置全局变量global_max_fast的值,决定fast bin真正使用时允许的最大值
//这个操作的前提是没有任何fast chunk。在操作后,fastchunk标志位会被清除。
//ptmalloc 默认情况下会调用 set_max_fast(s) 将全局变量 global_max_fast 设置为 DEFAULT_MXFAST
#define set_max_fast(s)                                                        \
    global_max_fast =                                                          \
        (((s) == 0) ? SMALLBIN_WIDTH : ((s + SIZE_SZ) & ~MALLOC_ALIGN_MASK))//用于计算max_fast的逻辑
#define get_max_fast() global_max_fast

fast bin计算索引的宏:

#define fastbin(ar_ptr, idx) ((ar_ptr)->fastbinsY[ idx ])//获取对应索引的bin,可以注意到这里的下标与index的对应关系与small bin不同。

/* offset 2 to use otherwise unindexable first 2 bins */
// chunk size=2*size_sz*(2+idx)
// 这里要减2,否则的话,前两个bin没有办法索引到。
#define fastbin_index(sz)                                                      \
    ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)//根据size计算对应的索引。由于这里的size包括了chunk头,因此要减去2个SIZE_SZ大小,即移位后要减一(这里的index对应的mem大小是没有复用下一条pre_size域的实际mem大小。但是不影响内存块的选取,因为用户需要的chunk也是经过request2size后从这里取走的,用户需要的大小对这里来说是透明的。比如说,用户需要内存为20字节,经过request2size后得出需要chunk大小为24字节,在此处的index对应的是实际mem大小为16字节的块),又因为index这样是从1开始,为了与数组下标对应,又要减去1,共减去2。

5)tcache结构

tcache结构的产生是为了提升堆管理的性能,它引入了两个结构体:tcache_entry 和 tcache_perthread_struct。

/* 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;//指向下一个tcache_entry,即下一个空闲chunk的mem首地址
} tcache_entry;//需要注意的是,这个结构复用的是空闲chunk的mem空间。

/* 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
{
  char counts[TCACHE_MAX_BINS];//记录了每一条tcache链表上的chunk数
  tcache_entry *entries[TCACHE_MAX_BINS];//链表表头构成的数组,类似bins
} tcache_perthread_struct;//每个线程维护一个此结构体

# define TCACHE_MAX_BINS                64

static __thread tcache_perthread_struct *tcache = NULL;//每个线程拥有一个