Heap - Part II

  • Arena

    • 指的是堆内存区域本身,并非结构

    • 主线程的main arena通过sbrk创建

    • 其他线程arena通过mmap创建

    • malloc_state

      • 管理arena的核心结构,包含堆的状态信息、bins链表等
      • main arena对应的malloc_state结构存储在glibc的全局变量
      • 其他线程arena对应的malloc_state存储在arena本身当中
  • bins

    • bins用来管理空闲内存块,通常使用链表结构来进行组织
  • chunks

    • 内存块的结构

⚠️ 这里的堆管理环境位glibc2.26以下(不包含2.26),即出现tcache之前的堆管理方式

环境:64位

1. 多线程支持

​ ptmalloc 实现了 malloc(),free()以及一组其它的函数, 以提供动态内存管理的支持。分配器处在用户程序和内核之间,它响应用户的分配请求,向操作系统申请内存,然后将其返回给用户程序,为了保持高效的分配,分配器一般都会预先分配一块大于用户请求的内存,并通过某种算法管理这块内存,来满足用户的内存分配要求。用户释放掉的内存也并不是立即就返回给操作系统,相反,分配器会管理这些被释放掉的空闲空间,以应对用户以后的内存分配要求。也就是说,分配器不但要管理已分配的内存块,还需要管理空闲的内存块,当响应用户分配要求时,分配器会首先在空闲空间中寻找一块合适的内存给用户,在空闲空间中找不到的情况下才分配一块新的内存。

​ 在原来的 dlmalloc 实现中,当两个线程同时要申请内存时,只有一个线程可以进入临界区申请内存,而另外一个线程则必须等待直到临界区中不再有线程。这是因为所有的线程共享一个堆。在 glibc 的 ptmalloc 实现中,比较好的一点就是支持了多线程的快速访问。在新的实现中,所有的线程共享多个堆。

1.1 多线程实验

  • example.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
void* threadFunc(void* arg) {
        printf("Before malloc in thread 1\n");
        getchar();
        char* addr = (char*) malloc(1000);
        printf("After malloc and before free in thread 1\n");
        getchar();
        free(addr);
        printf("After free in thread 1\n");
        getchar();
}
int main() {
        pthread_t t1;
        void* s;
        int ret;
        char* addr;
        printf("Welcome to per thread arena example::%d\n",getpid());
        printf("Before malloc in main thread\n");
        getchar();
        addr = (char*) malloc(1000);
        printf("After malloc and before free in main thread\n");
        getchar();
        free(addr);
        printf("After free in main thread\n");
        getchar();
        ret = pthread_create(&t1, NULL, threadFunc, NULL);
        if(ret)
        {
                printf("Thread creation error\n");
                return -1;
        }
        ret = pthread_join(t1, &s);
        if(ret)
        {
                printf("Thread join error\n");
                return -1;
        }
        return 0;
}

编译命令:gcc example1.c -o example -l pthread

a. Before malloc in main thread

在程序调用malloc之前程序进程中是没有heap segment的,并且在创建在创建线程前,也是没有线程堆栈的。

b. After malloc in main thread

在主线程中调用malloc之后,就会发现系统给程序分配了堆栈,且这个堆栈刚好在数据段之上。说明它是通过brk系统调用实现的。并且,还可以看出虽然我们只申请了1000字节的数据,但是系统却分配了132KB大小的堆,这132KB的堆空间叫做Arena,此时因为是主线程分配的,所以叫做main arena(每个arena中含有多个chunk,这些chunk以链表的形式加以组织)。

由于132KB比1000字节大很多,所以主线程后续再申请堆空间的话,就会先从这132KB的剩余部分中申请,直到用完或不够用的时候,再通过增加program break location的方式来增加main arena的大小。同理,当main arena中有过多空闲内存的时候,也会通过减小program break location的方式来缩小main arena的大小。

image.png

c. After free in main thread

​ 在主线程调用free之后:从内存布局可以看出程序的堆空间并没有被释放掉。调用free函数释放已经分配了的空间并非直接“返还”给系统,而是由glibc 的malloc库函数加以管理。它会将释放的chunk添加到main arenas的bin(这是一种用于存储同类型free chunk的双链表数据结构)中。在这里,记录空闲空间的freelist数据结构称之为bins。之后当用户再次调用malloc申请堆空间的时候,glibc malloc会先尝试从bins中找到一个满足要求的chunk,如果没有才会向操作系统申请新的堆空间。

image.png

d. Before malloc in thread 1

​ 在thread1调用malloc之前:从输出结果可以看出thread1中并没有heap segment,但是此时thread1自己的栈空间已经分配完毕了。

image.png

e. After malloc in thread 1

​ 在thread1调用malloc之后:从输出结果可以看出thread1的heap segment已经分配完毕了,同时从这个区域的起始地址可以看出,它并不是通过brk分配的,而是通过mmap分配,因为它的区域为0x7f9cf4000000-0x7f9cf8000000共64MB,并不是同程序的data segment相邻。同时,我们还能看出在这64MB中,根据内存属性分为了2部分:0x7f9cf4000000-0x7f9cf4021000共132KB大小的空间是可读可写属性;后面的是不可读写属性。这里只有可读写的132KB空间才是thread1的堆空间。

image.png

f. After free in thread 1

在thread1调用free之后:同main thread

2. Arenas

  • Arena :内存分配区,可以理解为堆管理器所持有的内存池。
    • 操作系统 --> 堆管理器 --> 用户
    • 物理内存 --> arena --> 可用内存
  • 堆管理器与用户的内存交易发生于arena中,可以理解为堆管理器向操作系统批发来的有冗余的内存库存 。

​ 在多线程应用程序上,堆管理器需要保护内部堆数据结构免受可能导致程序崩溃的竞争条件的影响。 在ptmalloc2之前,堆管理器只是通过在每次堆操作之前简单地使用全局互斥锁以确保在任何给定时间只有一个线程可以与堆进行交互。

​ 尽管这个策略有效,但堆管理器对使用率和性能非常敏感,这导致使用大量线程的应用程序会出现严重的性能问题。 对此,ptmalloc2堆分配器引入了“ arenas”的概念。每个arena本质上是一个完全不同的堆,它完全独立地管理自己的chunk分配和 free bin。 虽然每个 arena 仍然使用互斥锁来序列化对其内部数据结构的访问,但是与不同的arena进行交互时,线程可以安全地执行堆操作而不会彼此停顿。main arena和 thread arena 用环形链表进行管理。

​ 对于加入该进程的每个新线程,堆管理器将尝试查找没有其他线程在使用的arena,并将该arena附加到该线程。 一旦所有可用的arena都被其他线程使用,堆管理器将创建一个新的arena,最大arena数在32位进程中为2 * cpu核数,在64位进程中为8 * cpu核数。 一旦达到该限制,堆管理器就会放弃创建新的arena,多个线程将须共享一个arena,并面临执行堆操作时需要其中一个线程等待另一个线程的风险。

​ 这些secondary arena(thread arena)如何工作? 之前,我们看到main heap 就位于将程序加载到内存之后的内存,调用brk对其进行扩展,但是对于secondary arena(thread arena) 并不是如此!

​ 每个进程只有一个main arena,但可能存在多个thread main,ptmalloc 根据系统对arena的争用情况动态增加thread arena的数量,arena的数量一旦增加,就不会再减少了。main arena可以访问进程的 heap 区域和 mmap 映射区域,也就是说main arena可以使用 sbrk 和 mmap向操作系统申请虚拟内存。而thread arena 只能访问进程的 mmap 映射区域,thread arena 每次使用 mmap()向操作系统“批发”HEAP_MAX_SIZE(32 位系统上默认为 1MB,64 位系统默 认为 64MB)大小的虚拟内存,当用户向thread arena 请求分配内存时再切割成小块“零售”出去,毕竟系统调用是相对低效的,直接从用户空间分配内存快多了。所以ptmalloc 在必要的情况下才会调用 mmap()函数向操作系统申请虚拟内存。

2.1 subheaps

​ Sub-heaps的工作方式与main heap 基本相同,但有两个主要区别。 回想一下,initial heap位于程序加载到内存之后的内存,并由sbrk动态扩展。 相比之下,每个subheap 都使用 mmap 定位到内存中,并且堆管理器使用mprotect手动模拟增大subheap。

​ 当堆管理器想要创建subheap时,它首先请求内核通过调用 mmap 预留该subheap可以增长到的内存区域。 预留这个区域不会将内存直接分配到subheap中;它只是请求内核不要在该区域内分配诸如线程栈,mmap区域和其他分配。

​ 默认情况下,在32位进程中,子堆的最大大小(即预留供子堆使用的内存区域)为1MB,在64位系统上为64MB。

​ 这是通过向mmap询问标记为PROT_NONE的页面来完成的,这表明内核仅需要为该区域预留地址范围;它不需要内核来attach内存。

image.png

​ 在使用sbrk增长initial heap的情况下,堆管理器通过手动调用mprotect将区域中的页面从PROT_NONE更改为PROT_READ/PROT_WRITE 来将subheap“增长”到此保留地址范围 。这让内核将物理内存attach到那些地址,使得subheap缓慢地增长,直到整个mmap区域已满。 一旦整个subheap都用完,arena便会分配另一个subheap。 这使secondary arenas (thread arena)几乎无限期地增长,直到内核耗尽内存或进程耗尽地址空间时才最终失败。

​ 回顾一下:initial(main)arena仅包含main heap,该main heap位于将程序二进制文件加载到内存之后的位置,并使用sbrk进行扩展。 这是用于单线程应用程序的唯一场所。 在多线程应用程序上,为新线程提供了供分配的secondary area(thread arena)。 使用arenas可以降低线程在执行堆操作之前需要等待互斥量的可能性,从而提高了程序的速度。与main arena不同,这些secondary arena(thread arena)从一个或多个subheap分配chunk,这些subheap在内存中的位置首先使用mmap进行确定,并通过使用mprotect进行扩展。

image.png

2.2 malloc_state

  • Arena的头部结构:malloc_state。它存储了 arena 的状态,其中的 bins[] 用于管理空闲块的 bins
  • 作用:管理整个堆
struct malloc_state
{
  /* Serialize access.  */
  mutex_t mutex;
  /* Flags (formerly in max_fast).  */
  int flags;
  /* 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.  */
  struct malloc_state *next_free;
  /* Memory allocated from the system in this arena.  */
  INTERNAL_SIZE_T system_mem;
  INTERNAL_SIZE_T max_system_mem;
};


/*主线程的malloc_state结构存储在glibc的全局变量中,变量名为main_arena。*/
static struct malloc_state main_arena; /* global variable in libc.so */

2.3 Main Arena

​ main arena 可以访问 heap 区域,如果用户不调用 brk()或是 sbrk()函数,分配程序就可以保证分配到连续的虚拟地址空间,因为每个进程只有一个main arena 使用 sbrk()分配 heap 区域的虚拟内存。内核对 brk 的实现可以看着是 mmap 的一个精简版,相对高效一些。如果main arena 的内存是通过 mmap()向系统分配的,当 free 该内存时,arena 会直接调用 munmap()将该内存归还给系统。

image

2.4 Main Arena VS. Thread Arena

image.png

2.5 Thread Arena

​ 当某一线程需要调用 malloc()分配内存空间时,该线程先查看线程私有变量中是否已经存在一个arena,如果存在,尝试对该arena加锁,如果加锁成功,使用该arena分配内存,如果失败,该线程搜索循环链表试图获得一个没有加锁的arena。如果所有的分配区都已经加锁,那么 malloc()会开辟一个新的分配区,把该分配区加入到全局分配区循环链表并加锁,然后使用该分配区进行分配内存操作。在释放操作中,线程同样试图获得待释放内存块所在分配区的锁,如果该分配区正在被别的线程使用,则需要等待直到其他线程释放该分配区的互斥锁之后才可以进行释放操作。

2.6 heap_info

​ 程序刚开始执行时,每个线程是没有 heap 区域的。当其申请内存时,就需要一个结构来记录对应的信息,而 heap_info 的作用就是这个。而且当该 heap 的资源被使用完后,就必须得再次申请内存了。此外,一般申请的 heap 是不连续的,因此需要记录不同 heap 之间的链接结构。

注意:heap_info 不是存储堆块的数据,而是来解释说明这个堆段的。

​ 该数据结构是专门为从 Memory Mapping Segment 处申请的内存准备的,即为非主线程准备的。

​ 主线程可以通过 sbrk() 函数扩展 program break location 获得(直到触及 Memory Mapping Segment),只有一个 heap,没有 heap_info 数据结构。

#define HEAP_MIN_SIZE (32 * 1024)
#ifndef HEAP_MAX_SIZE
# ifdef DEFAULT_MMAP_THRESHOLD_MAX
#  define HEAP_MAX_SIZE (2 * DEFAULT_MMAP_THRESHOLD_MAX)
# else
#  define HEAP_MAX_SIZE (1024 * 1024) /* must be a power of two */
# endif
#endif

/* HEAP_MIN_SIZE and HEAP_MAX_SIZE limit the size of mmap()ed heaps
   that are dynamically created for multi-threaded programs.  The
   maximum size must be a power of two, for fast determination of
   which heap belongs to a chunk.  It should be much larger than the
   mmap threshold, so that requests with a size just below that
   threshold can be fulfilled without creating too many heaps.  */

/***************************************************************************/

/* A heap is a single contiguous memory region holding (coalesceable)
   malloc_chunks.  It is allocated with mmap() and always starts at an
   address aligned to HEAP_MAX_SIZE.  */

typedef struct _heap_info
{
  mstate ar_ptr; /* Arena for this heap. */
  struct _heap_info *prev; /* Previous heap. */
  size_t size;   /* Current size in bytes. */
  size_t mprotect_size; /* Size in bytes that has been mprotected
                           PROT_READ|PROT_WRITE.  */
  /* 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];
} heap_info;

该结构主要是描述堆的基本信息,包括:

  • at_ptr:堆对应的arena的地址(此堆段归属于哪个arena管理),mstate 的定义为:typedef struct malloc_state *mstate。
  • prev:前一个堆段。由于一个线程申请一个堆之后,可能会使用完,之后就必须再次申请。因此,一个线程可能会有多个堆(sub_heap)。prev用于将同一个分配区(arena)中的 sub_heap 用单向链表链接起来。指向链表中的前一个 sub_heap。这里可以看到每个堆的heap_info是通过单向链表进行链接的。
  • size:表示当前堆的大小,以 page 对齐。
  • mprotect_size: 表示当前 sub_heap 中被读写保护的内存大小,也就是说还没有被分配的内存大小。
  • pad:用于保证 sizeof (heap_info) + 2 SIZE_SZ 是按 MALLOC_ALIGNMENT 对齐的。MALLOC_ALIGNMENT_MASK 为 2*SIZE_SZ - 1,无论 SIZE_SZ 为 4 或 8,-6 * SIZE_SZ & MALLOC_ALIGN_MASK 的 值 为 0 , 如 果 sizeof (heap_info) + 2 * SIZE_SZ 不 是 按MALLOC_ALIGNMENT 对齐,编译的时候就会报错。编译时会执行下面的宏。
/* Get a compile-time error if the heap_info padding is not correct to make alignment work as expected in sYSMALLOc. */
extern int sanity_check_heap_info_alignment[(sizeof (heap_info) + 2 * SIZE_SZ) % MALLOC_ALIGNMENT 
? -1 : 1];

Q:为什么一定要保证对齐呢?

A:作为非主分配区(thread arena)的第一个 sub_heap,heap_info 存放在sub_heap 的头部,紧跟 heap_info 之后是该非主分配区的 malloc_state 实例,紧跟 malloc_state实例后,是 sub_heap 中的第一个 chunk,但 chunk 的首地址必须按照 MALLOC_ALIGNMENT 对齐,所以在 malloc_state 实例和第一个 chunk 之间可能有几个字节的 pad,但如果 sub_heap 不是非主分配区的第一个 sub_heap,则紧跟 heap_info 后是第一个 chunk,但 sysmalloc()函数默认 heap_info 是按照 MALLOC_ALIGNMENT 对齐的,没有再做对齐的工作,直接将 heap_info 后的内存强制转换成一个 chunk。所以这里在编译时保证 sizeof (heap_info) + 2 * SIZE_SZ 是按 MALLOC_ALIGNMENT 对齐的,在运行时就不用再做检查了,也不必再做对齐。

3. Chunk

​ 无论一个 chunk 的大小如何,处于分配状态还是释放状态,它们都使用一个统一的结构。虽然它们使用了同一个数据结构,但是根据是否被释放,它们的表现形式会有所不同。

struct malloc_chunk {
    INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
    INTERNAL_SIZE_T 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;
};

3.1 Allocated Chunk

image

  • prev_size:前一个chunk的大小。仅当前一个chunk为free chunk时生效。

  • size

    • chunk size:当前chunk大小
    • A:用于告诉堆管理器该块是否属于 thread arena 而不是main arena。在释放期间,需要向堆管理器提供指向程序员要释放的区域的指针,并且堆管理器需要确定该指针属于哪个arena。 如果在chunk的metadata中设置了A标志,则堆管理器必须搜索每个arena,并查看指针是否位于该arena的任何子堆中。 如果未设置该标志,则堆管理器可以减小搜索,因为它知道该块来自mian arena。
    • M:用于指示当前chunk是否是通过mmap进行堆外分配的巨大分配。当此分配最终被free时,堆管理器将立即通过munmap将整个chunnk返回给操作系统,而不是尝试对其进行回收。 因此,释放的chunk永远不会设置该标志。
    • P:它指示先前的chunk是否空闲chunk。 这意味着当释放该chunk时,可以将其安全地连接到先前的chunk上,以创建更大的空闲chunk。如下图所示,当free chunk4时,由于前面的chunk3是空闲的,将把chunk3、4、5合并。
  • user data:存储的都是用户数据。甚至下一个chunk的第一个字段prev_size,也可被用来存放数据,原因是这个prev_size字段只有当“前一个“chunk是free的时候才有意义,如果”前一个“chunk是已经分配的,堆管理器并不关心。

heap-chunks-coalescing-gif-color.gif

由于对齐的原因(堆的大小是0x8(b1000)/0x10(b10000)的倍数,不可能出现0x18这样大小的堆),因此,chunk size 的低三位始终为零,这就可以用作其他含义,用来存储A、M、P三个标志。

image.png

  • chunk 指针:指向一个 chunk 的开始,一个 chunk 中包含了用户请求的内存区域和相关的metadata
  • mem指针:才是malloc真正返回给用户的内存指针。

3.2malloc参数与chunk大小的关系

image

  • malloc参数为用户申请的内存大小
  • chunk包含数据和metadata
  • 返回的chunk只要保证其中可用数据大小大于等于用户申请即可
  • 在x86平台下,chunk的大小一定是8字节的整数倍;在x64平台下,chunk的大小一定是16字节的整数倍。

3.3 chunk的空间复用

​ 为了使得 chunk 所占用的空间最小,ptmalloc 使用了空间复用,一个 chunk 或者正在被使用,或者已经被 free 掉,所以 chunk 的中的一些域可以在使用状态和空闲状态表示不同的意义,来达到空间复用的效果。

​ 以 32 位系统为例,当空闲时,一个 chunk 中至少需要 4 个 size_t(1size_t = 4Byte)大小的空间,用来存储 prev_size,size,fd 和 bk ,也就是 16Byte,chunk 的大小要对齐到 8Byte。当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size域肯定是无效的。所以实际上,这个空间也可以被当前 chunk 使用。故而实际上,一个使用中的 chunk 的大小的计算公式应该是:

in_use_size = (用户请求大小+ 8 - 4 ) align to 8Byte

这里加 8 是因为需要存储 prev_size 和 size,但又因为向下一个 chunk“借”了 4B,所以要减去 4。最后,因为空闲的 chunk 和使用中的chunk 使用的是同一块空间。所以肯定要取其中最大者作为实际的分配空间。即最终的分配空间

chunk_size = max(in_use_size, 16)

这就是当用户请求内存分配时,ptmalloc 实际需要分配的内存大小,在后面的介绍中。如果不是特别指明的地方,指的都是这个经过转换的实际需要分配的内存大小,而不是用户请求的内存分配大小。

3.4 Free Chunk

image

  • prev_size:前一个chunk的大小,仅当前一个chunk是free chunk时有效。

  • size:

    • chunk size:记录了当前chunk的大小
    • P:代表前一个chunk是否被使用
    • M:代表当前chunk是否是mmap出来的
    • A:代表该chunk是否属于thread arena
    • 当 chunk 空闲时,其 M 状态不存在,只有 AP 状态
  • fd:前向指针

  • bk:后向指针

这两个字段用于bin链表当中,用来链接大小相同或者相近的free chunk,便于后续分配时查找。

  • fd_nextsize:前面的chunk的大小。只有当chunk为large chunk使用。
  • bk_nextsiz:后面的chunk的大小。只有当chunk为large chunk时使用。
  • unused space:未使用的空间。

4. Bins

​ bins是管理 arena 中空闲 chunk 的结构,以数组的形式存在,数组元素为相应大小的 chunk 链表的链表头,存在于 arena 的 malloc_state 中 。

根据chunk的大小和状态,有许多种不同的Bins结构。因此一个线程中会有很多的bin链,这些bin链都由arena所表示的struct malloc_state结构体的以下成员保存:

  • 数组fastbinsY[ NFASTBINS ]:大小为10。记录的是fast bin链。

    • Fast Bins:用于管理小的chunk
  • 数组bins[ NBINS 2 - 2 ]:大小为126。

    • 索引0:未使用
    • unsorted bin(1):用于存放未整理的chunk;
    • small bin(2-64):用于管理中等大小的chunk;
    • large bin(65-126):用于管理较大的chunk。

image.png

4.1 堆管理器的基本回收策略

在看这些bin之前,我们先来看看堆管理器的基本回收策略。

  1. 如果chunk在metadata中设置了M位,则分配是在堆外分配的,应进行munmaped,直接返回给操作系统。
  2. 否则,如果“前面”的chunk是空闲的,则将该chunk向后合并以创建更大的空闲chunk。
  3. 同样,如果“后面”的chunk是空闲的,则该chunk将向前合并以创建更大的空闲chunk。
  4. 如果此chunk与堆的top接壤,则整个chunk将被吸收到堆的末端,而不是存储在“bin”中。
  5. 否则,该chunk将被标记为空闲并将其放置在适当的容器中。

4.2 Small Bins

​ Small bins是最容易理解的基础bin。它有62个,每个small bin都存储相同大小的固定chunk。 在32/64位系统上小于512字节(0x200)/1024字节(0x400)的每个chunk都有一个对应的小bin。 由于每个small bin仅存储一个大小的chunk,因此它们会自动排序,在这些列表中插入和删除条目的速度非常快。

struct malloc_state
{
  ...
  /* Normal bins packed as described above */
  mchunkptr bins[NBINS * 2 - 2];  //重点关注
  ...
};

image.png

  • 在这62个small bin链中

​ 第一个small bin链中chunk的大小为32字节,后续每个small bin中chunk的大小依次增加两个机器字长(32位相差8字节,64位相差16字节),以此类推,跟fastbinsY数组存储fastbin链的原理是相同的。

bin链存储的大小与数组下标的关系:

chun_size=2 * times SIZE_SZ * times index

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

    • chunk大小

      • x86:小于512 byte(0x200)
      • x64:小于1024 byte(0x400)
    • 相同大小的chunk放在一个bin中

    • 双向循环链表

    • 先进先出

    • 当有空闲块相邻时,chunk会被合并成一个更大的chunk

    • bins[2], bin[3], ..., bin[124], bin[125]共62组smallbin

4.3 Large Bins

​ small bin的策略非常适合小型分配,但我们无法为每个可能的 chunk 大小都配备一个 bin 。 对于超过512字节(64位为1024字节)的 chunk ,堆管理器将改为使用 large bins。

​ 63个“large bins”中的每一个与 small bin 的操作方式基本相同,但是它们不存储固定大小的块,而是存储大小范围内的块。 每个large bin的大小范围都设计为与 small bin 的 chunk 大小或其他 large bin 的范围都不重叠。 换句话说,在给定 chunk 的大小的情况下,恰好有一个对应于此大小的small bin 或 large bin。

​ 由于 large bin 存储一定范围的大小,因此必须手动对bin中的插入进行排序,并且分配需要遍历该链表。 这使得large bin 本质上比small bin 慢。 但是,在大多数程序中,large bin 的使用频率较低。 这是因为程序倾向于分配(释放)速度比“大分配”更高的“小分配”。 出于相同的原因,large bin 范围会聚集到较小的chunk大小; 最小的“large bin ”仅覆盖从512字节到576字节的64字节范围,而第二大large bin则覆盖256KB的大小范围。 最大的large bin 覆盖了1MB以上的所有已释放块。

struct malloc_state
{
  ...
  /* Normal bins packed as described above */
  mchunkptr bins[NBINS * 2 - 2];  //重点关注
  ...
};

image.png

  • 在这63个large bin中:

    • 第一组的32个largebin链依次以64字节步长为间隔,即第一个large bin链中chunk size为1024-1087字节,第二个large bin中chunk size为1088~1151字节;
    • 第二组的16个large bin链依次以512字节步长为间隔;
    • 第三组的8个largebin链以步长4096为间隔;
    • 第四组的4个largebin链以32768字节为间隔;
    • 第五组的2个largebin链以262144字节为间隔;
    • 最后一组的largebin链中的chunk大小无限制。
数量 公差
1 32 64B
2 16 512B
3 8 4096B
4 4 32768B
5 2 262144B
6 1 不限制
  • 总结

    • chunk大小

      • x86:大于512Byte
      • x64:大于1025Byte
    • 每组bin表示一组size范围而不是具体的size。

    • 双向循环链表

    • 先进先出

    • 当有空闲块相邻,chunk会被合并

    • 共63组largebin,大小范围[0x400,X] (64bit)

    • 每组bins中的chunk大小不一定相同,按由大到小的顺序在链表中排列

4.4 Unsorted Bin

​ 堆管理器使用称为“unsorted bin”的优化缓存层进一步改善了free的基本算法。此优化基于以下观察:空闲空间经常会聚集在一起,释放后通常会立即分配大小相似的chunk。

​ 在这些情况下,在将结果较大的chunk放入正确的bin中之前合并这些释放的块将避免一些开销,并且如果能够快速分配最近释放的分配也会加快过程。

​ 当空闲的 chunk 被链接到 bin 中的时候,ptmalloc 会把表示该 chunk 是否处于使用中的标志 P 设为 0(注意,这个标志实际上处在下一个 chunk 中),同时 ptmalloc 还会检查它前后的 chunk 是否也是空闲的,如果是的话,ptmalloc 会首先把它们合并为一个大的 chunk,然后将合并后的 chunk 放到 unstored bin 中。

使用时机:当释放较小或较大的chunk的时候,堆管理器没有立即将新释放的chunk放入正确的bin中,而是与邻居合并,然后将其放到的unsorted bin中。 在malloc期间,将检查 unsorted bin上的每个chunk,以查看其是否“适合”请求。 如果符合,malloc可以立即使用它。 如果不是,则malloc然后将块放入其相应的small bin或large bin中。

​ ⚠️ 并不是所有的 chunk 被释放后就立即被放到 bins 中。ptmalloc 为了提高分配的速度,会把一些小的 chunk 先放到fast bins 内。

目的:让“glibc malloc机制”能够有第二次机会重新利用最近释放的chunk(第一次机会就是fast bin机制)。利用unsorted bin,可以加快内存的分配和释放操作,因为整个操作都不再需要花费额外的时间去查找合适的bin了。

struct malloc_state
{
  ...
  /* Normal bins packed as described above */
  mchunkptr bins[NBINS * 2 - 2];  
  ...
};

image.png

  • 小结

    • chunk大小

      • 在unsorted bin中,对chunk的大小并没有限制,任何大小的chunk都可以归属到unsorted bin中
      • 64位平台中:chunk大小>128byte
    • 只存在唯一一个unsorted bin

    • 双向循环链表

    • 先进先出

    • 当一个chunk(非fast bin)被free,它首先被放入unsorted bin,等后续整理时才会放入对应的small bin/fast bin

4.5 Fast Bins

​ 一般情况下,程序在运行时会经常需要申请和释放一些较小的内存空间。当分配器合并了相邻的几个小的 chunk 之后,也许马上就会有另一个小块内存的请求,这样分配器又需要从大的空闲内存中切分出一块,这样无疑是比较低效的,故而,ptmalloc 在前面三种bin的基础上又引入了 fast bins 来进一步优化。

  • 概念:大小在16字节-128字节(0x10~0x80)的chunk称为“fast chunk”(大小不是malloc时的大小,而是在内存中struct malloc_chunk的大小,包含前2个成员)
  • 不会对free chunk进行合并:鉴于设计fast bin的初衷就是进行快速的小内存分配和释放,因此系统将属于fast bin的chunk的P(PREV_INUSE)位总是设置为1,这样即使当fast bin中有某个chunk同一个free chunk相邻的时候,系统也不会进行自动合并操作,而是保留两者。虽然这样做可能会造成额外的碎片化问题,但瑕不掩瑜。
struct malloc_state
{
  ...
  /* Fastbins */
  mfastbinptr fastbinsY[NFASTBINS];
  ...
};

image.png

  • 小结

    • chunk的大小

      • x86:16-64(0x10-0x40)字节
      • x64:32-128(0x20-0x80)字节
    • 相同大小的chunk放在一个bin中

    • 单向链表

    • 后进先出

    • 相邻的空闲fast bin chunk 不会被合并

    • 当chunk被free时,不会清理P标志。

4.6 小结

  • 大小

    • fast bin

      • x86:16-64(0x10-0x40)
      • x64:32-128(0x20-0x80)
    • small bin

      • x86:小于512 byte(0x200)
      • x64:小于1024 byte(0x400)
    • large bin

      • x86:大于512Byte
      • x64:大于1025Byte
    • unsorted bin

      • x86:大于64Byte
      • x64:大于128Byte

5.其他chunk

​ 并不是所有的 chunk 都按照上面的方式来组织,实际上,有三种例外情况。Top chunk,mmaped chunk 和 last remainder,下面会分别介绍这三类特殊的 chunk。

5.1 Top chunk

  • 概念:当一个chunk处于一个arena的最顶部(即最高内存地址处)时,就称之为top chunk

  • 作用:该chunk并不属于任何bin,而是在系统当前的所有free chunk(无论哪一种bin)都无法满足用户请求的内存大小的时候,将此chunk当做一个应急消防员,分配给用户使用。

  • 分配的规则:如果top chunk的大小比用户请求的大小要大的话,就将该top chunk分作两部分:1)用户请求的chunk; 2)剩余的部分成为新的top chunk。否则,就需要扩展heap或分配新的heap了——在main arena中通过sbrk扩展heap,而在thread arena中通过mmap分配新的heap。实际上,top chunk 在分配时总是在 fast bins 和 bins 之后被考虑,所以,不论 top chunk 有多大,它都不会被放到 fast bins 或者是 bins 中。

  • 小结

    • 不属于任何bin

    • 在arena中处于最高地址

    • 当没有其他空闲块时,top chunk就会被用于分配

    • 分裂时

      • 一块时请求大小的chunk
      • 另一块余下chunk将成为新的top chunk

5.2 last remainder chunk

​ 在用户使用 malloc 请求分配 small chunk时,且该请求无法被small bin、unsorted bin满足的时候,即ptmalloc2 找到的 chunk 和申请的内存大小不一致,这时候就将分割之后的剩余部分称之为 last remainder chunk

​ unsort bin 也会存这一块,top chunk 分割剩下的部分不会作为 last remainer。

5.3 mmaped chunk

​ 当需要分配的 chunk 足够大,而且 fast bins 和 bins 都不能满足要求,甚至 top chunk 本身也不能满足分配需求时,ptmalloc 会使用 mmap 来直接使用内存映射来将页映射到进程空间。这样分配的 chunk 在被 free 时将直接解除映射,于是就将内存归还给了操作系统,再次对这样的内存区的引用将导致 segmentation fault 错误。这样的 chunk 也不会包含在任何bin 中。

参考资料

https://www.bilibili.com/video/BV14i4y1V7rw
https://azeria-labs.com/heap-exploitation-part-1-understanding-the-glibc-heap-implementation/
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/heap_overview-zh/
• ...

由于有些资料是以前查的,不可考了🤦‍♂️,如果有没列上的,敬请谅解。

同样发布在我的语雀上:https://www.yuque.com/u1499710/fgcf17/ungqp3

posted @ 2020-10-27 09:43  直木  阅读(280)  评论(0编辑  收藏  举报