堆深入学习记录2 | 青训营

286 阅读8分钟

此篇文章是我参与 #青训营笔记创作活动 的第8篇笔记

主要是简单记录一下自己在深入学习堆时遇到的一些小要点,

只是个人的一个学习记录,水平不高还望大佬们包容。


堆的宏观结构

复习一下, 从总体上看可以把堆的数据结构分为微观结构和宏观结构:

  • 微观结构主要是malloc_chunk的数据与结构:

    • prev_size
    • size
    • fd,bk
    • fd_nextsize, bk_nextsize
  • 宏观结构包括堆块的宏观信息,可以使我们更好地了解glibc的堆管理实现,宏观结构分为以下几个部分:

    • arena & main_arena:可以看作是堆块的管理器,但其实就是堆内存本身

    • bin:用链表结构管理被free的malloc_chunk(堆块)

    • top_chunk:处于一个arena的最顶部(即最高内存地址处)、不属于任何bin的一片内存

    • malloc_state:管理 arena 的核心结构,包含堆的状态信息、bins 链表等

      • main arena 对应的 malloc state 结构存储在 glibc 全局变量中
      • 其他线程 arena 对应的 malloc_state 存储在 arena 本身中

arena & main_arena

  • 区分二者

    • arena 指的是堆内存区域本身,并不是结构;
    • 主线程的 main arena 通过 sbrk 创建,管理所有堆块的结构体
    • 其他线程的 arena 通过 mmap 创建,有时可能或会被不恰当地称为arena,存在于线程的控制块plt
    • 不是每个线程都会有对应的arena 因为每个系统的核数有限,当线程数大于核数的二倍时,就必然有线程处于等待状态,所以没有必要为每个线程分配一个arena 32bit --> arena_num = 2 * core 64bit --> arena_num = 8 * core

  • 主线程的 main arena和其他线程的 arena 可以看作是主分配区和非主分配区

    • ps:ptmalloc为什么要增加非主分配区?(问题与回答复制自malloc内存管理总结_
    • 答:如果没有非主分配区,所有的线程在主分配区上操作,互相竞争锁的过程十分影响分配效率。ptmalloc中增加了非主分配区支持,主分配区和非主分配区用环形链表进行管理,提高malloc的分配效率。 申请小块内存时会产生很多内存碎片,ptmalloc在整理时也需要对分配区做加锁操作。每个加锁操作大概需要5~10个cpu指令,而且程序线程很多的情况下,锁等待的时间就会延长,导致malloc性能下降。一次加锁操作需要消耗100ns左右,正是锁的缘故,导致ptmalloc在多线程竞争情况下性能远远落后于tcmalloc。最新版的ptmalloc对锁进行了优化,加入了PER_THREAD和ATOMIC_FASTBINS优化,但默认编译不会启用该优化,这两个对锁的优化应该能够提升多线程内存的分配的效率。
  • 如何标志

    • 在malloc_chunk 中的 size 的倒数第三个标志位 A,多线程时为1,主线程为0
    • NON_MAIN_ARENA,A:size的倒数第三位表示当前chunk属于主分配区(0)还是非主分配区(1)
  • 子线程的堆和主线程的堆是不一样的

    • 每个线程都会预分配一个堆空间
    • 线程会从这个对空间创建top_chunk和堆块
    • 当malloc的空间超过预分配的大小,会回到main_arena之前再次分配一个空间
    • 如果线程的堆存在溢出,可以用之前的chunk越界写堆的arena结构

一些 tips

  • 定位子线程的chunk的技巧
  1. 向子线程的堆块输入特殊值:"0xdeadbeef"
  2. 在gdb使用 search -4 0xdeadbeef
  3. 搜索出来的地址即堆的地址
  • 多线程利用思路
  1. 在子线程中找到堆空间的地址空间A
  2. 在A中找到恢复线程的arena的结构
  3. 通过arena的结构尝试堆利用

malloc_state

管理 arena 的核心结构,包含堆的状态信息、bins 链表等

  • main arena 对应的 malloc state 结构存储在 glibc 全局变量中
  • 其他线程 arena 对应的 malloc_state 存储在 arena 本身中

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

主要关心这么几个:

  • mfastbinptr fastbinsY[NFASTBINS] ,保存了 fastbins 各个链表的数组的头,大小为10 记录的是fast bin链
  • mchunkptr top,指向了 top chunk
  • mchunkptr bins[NBINS * 2 - 2] ,大小为129。记录的是unsorted bin(1)、small bin(263)、large bin链(64126)

bin

bin负责管理free的malloc_chunk,按照free的chunk大小划分为以下几种:

  • fast bins,用于管理较小的 chunk

  • small bins,用于管理中等大小的 chunk

  • large bins,用于管理较大的 chunk

  • unsorted bin,用于存放未整理的 chunk

  • tcache

    • 是 glibc 2.26 (ubuntu 17.10) 之后引入的一种技术(see commit),目的是提升堆管理的性能。但提升性能的同时舍弃了很多安全检查,也因此有了很多新的利用方式。
  • 管理流程
  1. malloc/free --> glibc --> arena --> fastbin/bins -->smallbin/largebin/unsortedbin
  2. 从glibc找到main_arena
  3. 在main_arena的管理结构体malloc_state通过固定偏移中找到fastbinsY[NFASTBINS],用以管理fastbin。
  4. 找到bins[NBINS * 2 - 2],用以管理unsortedbin。
  • bin的放置顺序

索引为1的是unsortedbin,这里面的chunk没有进行排序,比较杂乱。 索引从2到63的bin称为small bin,同一个small bin链表中的chunk的大小相同。两个相邻索引的small bin链表中的chunk大小为2个机器字节,即32-->4字节,64-->8字节。 索引从64到126的bin被称为large bin。large bins中的每一个bin都包含一定范围内的chunk,其中的chunk按fd指针的顺序从大到小排列,最靠近bin头的越大,相同大小的chunk按照最近使用顺序排列。

  • 任意两个物理相邻的空闲chunk不能在一起,否则会合并。
  • free之后的chunk,与top_chunk相邻的,会与top_chunk合并,不与之相邻的,会根据其大小进入到不同的bin

小的进入fastbin,大的进入unsortedbin 此时,释放掉的chunk不会马上归还系统,ptmalloc会统一管理heap和mmap映射区域的空闲的chunk。 当用户再一次请求分配内存时,ptmalloc分配器会试图在空闲的chunk中挑选一块合适的给用户,这样可以避免频繁的系统调用,减少内存分配的开销。

  • 需要注意的是,并不是所有的chunk被释放之后立即放到bin中。ptmalloc为了提高分配的速度,会把一些小的堆块先放到fast bin的容器内。而且fast bin容器中的chunk的使用标记总是被置为1的,所以不会自动合并。

fastbin

单向链表后进先出,同时 p 位被保留(设置值为1)防止合并,同一大小的 chunk 会在同一条链上,不同大小的 chunk 在不同的链上


不同平台大小不同,列一个索引,当 malloc 的大小在这个范围内的时候会首先去 fastbin 中找

fastbinsY[](下标)x86(size_t=4)x64(size_t=8)
00x100x20
10x180x30
20x200x40
30x280x50
40x300x60
50x380x70
60x400x80
  • 注意fastbin不属于bins,不是bins管理的
  • fashbin是ptmalloc单独用来管理0x20-0x80(64位平台)的堆块的数据结构,如果free的chunk大小在0x20-0x80之间,会优先进入fashbin,

smallbin

  • 大小在0x20-0x400(64位)
下标x86(size_t=4)x64(size_t=8)
20x100x20
30x180x30
40x200x40
50x28(40)0x50(80)
x2 * 4 * x2 * 8 * x
630x1F8(504)0x3F0(1008)
  • 双向链表,先进先出
  • 释放的时候会检查相邻的是不是 free 的,如果是进行合并然后放到 unsortedbin

unsortedbin

堆块中转站

  • 双向循环链表,先进先出

  • 存放所有不满足 fastbin,未被整理的 chunk

    • 除了fastbin管理的堆块,free掉的chunk都是先进入到unsortedbin里再进行整理
  • malloc 的时候在其他 bin 没找到合适的就会遍历 unsortedbin 同时根据大小放到对应的 bin 里

  • 在整理过程中,先将所有放在unsortedbin链上的堆块按照大小整理到其它链上

  • 将fastbin上的碎片整理到unsorted,再由unsorted整理到其他bin链


largebin

  • 管理大于0x400的堆块(64位),大于0x200的堆块(32位)
  • 双向链表,先进先出
  • 需要根据 fd_nextsize 和 bk_nextsize 指针从大到小排序

附录

学习与参考链接:

堆相关数据结构 - CTF Wiki (ctf-wiki.org)

二进制安全之堆溢出(系列)——堆基础 & 结构(一)

二进制安全之堆溢出(系列)——堆基础 & 结构(二)

二进制安全之堆溢出(系列)——堆基础 & 结构(三)

二进制安全之堆溢出(系列)——堆基础 & 结构(四)

堆相关知识 (yuque.com)

malloc内存管理总结_g_malloc是怎么进行内存管理的