各类不同的堆分配器

不同也应用堆内存需求各异,促使了多重堆分配器的诞生

  • dlmalloc:通用分配器,满足多种场景
  • ptmalloc2:用于glibc,在GNU系统中广泛使用
  • jemalloc:用于FreeBSD和Fierfox,针对特定系统和软件优化
  • tcamalloc:是Google开发,适用于Google的软件生态
  • libumem:用于Solaris系统,为其提供内存管理支持

堆管理器

堆管理器处于用户程序与内核中间

  • 响应用户的申请内存请求,向操作系统申请内存,然后将其返回给用户
    程序。同时,为了保持内存管理的高效性,内核一般都会预先分配很大
    的一块连续的内存,然后让堆管理器通过某种算法管理这块内存。只有
    当出现了堆空间不足的情况,堆管理器才会再次与操作系统进行交互。
  • 管理用户所释放的内存。一般来说,用户释放的内存并不是直接返还给
    操作系统的,而是由堆管理器进行管理。这些释放的内存可以来响应用
    户新申请的内存的请求。

堆的基本操作

malloc函数

  • malloc(size_t n):malloc函数返回8对应大小字节的内存块的指针
    • 当 n=0 时,返回当前系统允许的堆的最小内存块。
    • 当 n 为负数时,由于在大多数系统上,size_t 是无符号数(这一点非常重要),所以程序就会申请很大的内存空间,但通常来说都会失败,因为系统没有那么多的内存可以分配。

free函数

  • free(void*p):free函数会释放p所指向的堆块
    • 当 p 为空指针时,函数不执行任何操作。
    • 当 p 已经被释放之后,再次释放会出现乱七八糟的效果,这其实就是double free。
    • 除了被禁用 (mallopt) 的情况下,当释放很大的内存空间时,程序会将这些内存空间还给系统,以便于减小程序所使用的内存空间。

堆的微观结构

称有malloc申请的内存为chunk,在ptmalloc内部用malloc_chunk结构体来表示,当程序申请的chunk被free后,会被加入到相应的空闲管理列表中
无论一个chunk的大小如何,处于分配状态还是释放状态,他们都使用一个统一的结构

chunk

struct malloc_chunk{
      INTERNAL_SIZE_T   prev_size;
      INTERNAL_SIZE_T   size;
      
      struct malloc_chunk*fd;
      struct malloc_chunk*bk;

      /* Only used for large blocks:point*/
      struct malloc_chunk* fd_nextsize;
      struct malloc_chunk* bk_nextsize;
};

prev_size

如果该 chunk 的物理相邻的前一地址 chunk(两
个指针的地址差值为前一chunk 大小)是空闲的话,那该字段记录的是前一个 chunk 的大小 (包括 chunk 头)。否则,该字段可以用来存储物理相邻的前一个 chunk 的数据。这里的前一 chunk 指的是较低地址的 chunk 。

size

该 chunk 的大小,大小必须是 2 * SIZE_SZ 的整数倍。如果申请的内存大小不是 2 * SIZE_SZ 的整数倍,会被转换满足大小的最小的 2 *SIZE_SZ 的倍数。32 位系统中,SIZE_SZ 是 4;64 位系统中,SIZE_SZ 是 8。 该字段的低三个比特位对chunk 的大小没有影响,它们从高到低分别表示
NON_MAIN_ARENA,记录当前 chunk 是否不属于主线程,1 表示不属于,0 表示属于。

  • IS_MAPPED,记录当前 chunk 是否是由
    mmap 分配的。
  • PREV_INUSE,记录前一个 chunk 块是否被分
    配。一般来说,堆中第一个被分配的内存块
    的 size 字段的 P 位都会被设置为 1,以便于
    防止访问前面的非法内存。当一个 chunk 的
    size 的 P 位为 0 时,我们能通过 prev_size 字
    段来获取上一个 chunk 的大小以及地址。这
    也方便进行空闲 chunk 之间的合并。

fd,bk

chunk 处于分配状态时,从 fd 字段开始是用户的数据。chunk 空闲时,会被添加到对应的空闲管理链表中,其字段的含义如下:

  • fd 指向下一个(非物理相邻)空闲的chunk
  • bk 指向上一个(非物理相邻)空闲的 chunk
  • 通过 fd 和 bk 可以将空闲的chunk 块加入到空闲的 chunk 块链表进行统一管理

fd_nextsize bk_nextsize

也是只有 chunk 空闲的时候才使用,不过其用于较大的 chunk(large chunk)。

  • fd_nextsize 指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
  • bk_nextsize 指向后一个与当前chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
  • 一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适chunk 时挨个遍历。

整体结构

被释放的chunk被记录在链表中


size

  • 如果一个chunk处于free状态,那么会有两个位置记录其相应的大小

    • 本身的size字段会记录
    • 他后面的chunk会记录
  • 一般情况下,物理相邻的两个空闲chunk‘会在适合的时间被合并为一个chunk,堆管理器会通过prev_size字段以及size字段合并为两个物理相邻的空闲chunk块,以提高内存利用率

bin-空闲chunk管理结构

  • 用户释放掉的 chunk 不会马上归还给系统,ptmalloc 会统一管理heap 和 mmap 映射区域中的空闲的 chunk
  • 当用户再一次请求分配内存时,ptmalloc 分配器会试图在空闲的chunk 中挑选一块合适的给用户。这样可以避免频繁的系统调用,降低内存分配的开销
  • 这结构就是bins – 一种分箱式的chunk管理方法
  • 根据空闲的 chunk 的大小以及使用状态将chunk 初步分为 4 类
    • fast bins
    • small bins
    • large bins
    • unsorted bin

bin的实现

对于 small bins,large bins,unsorted bin 来说,ptmalloc 将它们维护在同一个数组中

  • 第一个为 unsorted bin,字如其面,这里面的 chunk 没有进行排序,存储的chunk 比较杂。
  • 索引从 2 到 63 的 bin 称为 small bin,同一个 small bin 链表中的 chunk 的大小相同。两个相邻索引的 small bin 链表中的 chunk 大小相差的字节数为 2 个机器字长,即 32 位相差 8 字节,64 位相差 16 字节。
  • small bins 后面的 bin 被称作 large bins。large bins 中的每一个 bin 都包含一定范围内的 chunk,其中的 chunk 按 fd 指针的顺序从大到小排列。相同大小的 chunk 同样按照最近使用顺序排列。

fastbin

  • fastbin 中默认支持最大的 chunk 的数据空间大小为 64 字节。但是其可以支持的 chunk 的数据空间最大为 80 字节。除此之外,fastbin 最多可以支持的 bin 的个数为 10 个,从数据空间为 8 字节开始一直到 80 字节
  • fastbin 范围的 chunk 的 inuse 始终被置为 1。因此它们不会和其它被释放的 chunk 合并。

堆溢出

  • 堆溢出漏洞发生的基本前提是
    • 程序向堆上写入数据。
    • 写入的数据大小没有被良好地控制。
  • 堆溢出漏洞没有办法直接去覆盖返回地址控制程序eip
  • 堆溢出漏洞的利用方法通常分为两种:
    • 去想办法覆盖堆结构里的虚函数指针
    • 通过bins结构的一些特点去实现任意地址读写,进而控制程序流

简单例子

对课件的简单整理,如有不足,还请指教