bin是什么?
在 Glibc 的 ptmalloc 分配器中,Bin 是用于管理空闲内存块(Free Chunk)的核心数据结构。
从实现角度来看,Bin 并非物理容器,而是维护在 malloc_state(也称为 Arena)结构体中的指针数组。这些指针指向由空闲 Chunk 组成的链表(Linked List)。
当用户通过 free() 释放内存时,分配器会根据 Chunk 的大小将其插入(Link)到对应的 Bin 链表中;当用户 malloc() 时,分配器从 Bin 中取出(Unlink)合适的 Chunk 并返回给用户。
Malloc_state
在 glibc 的内存管理器(ptmalloc)中,malloc_state 是一个至关重要的结构体,它被通俗地称为 Arena(分配区)。
简单来说,malloc_state 记录了一个分配区内所有内存管理的核心信息,包括空闲块的链表、Top Chunk 的位置以及统计数据。
glic-2.35的malloc_state结构体源代码[1]如下:
1 | struct malloc_state |
Arena的分类:主和从
在一个多线程程序中,为了减少锁竞争,glibc 会创建多个 Arena:
(1)Main Arena (主分配区): 进程中只有一个。它直接管理通过 sbrk(堆区扩展)和 mmap 分配的内存。它是一个全局变量。
(2)Thread Arena (从分配区): 当多个线程竞争内存分配时,glibc 会为新线程创建额外的 Arena。这些从分配区主要通过 mmap 分配的一块被称为 heap_info 的大内存中切分。
malloc_state核心结构解析
以下是 malloc.c 中该结构体的主要成员及其功能:
| 成员名称 | 功能描述 |
|---|---|
mutex |
互斥锁。每个 Arena 都有自己的锁,保证在多线程环境下对该分配区的操作是原子性的。 |
fastbinsY |
Fast Bins 数组。存储小块内存(16B-80B)的单向链表头指针。 |
top |
Top Chunk 指针。指向当前 Arena 顶部的空闲空间,如果所有 Bin 都找不到合适的块,就从这里切割。 |
last_remainder |
剩余块指针。当一个小的申请从一个大的空闲块中切出后,剩余部分如果不能进 Bin,就存放在这里以备下次快速分配。 |
bins |
常规 Bins 数组。存储 Unsorted Bin、Small Bins 和 Large Bins 的双向链表。 |
binmap |
位图。用于快速查找哪个 Bin 索引下存在空闲块,提高搜索效率。 |
next |
链表指针。将进程中所有的 malloc_state 实例连接成一个循环链表,方便分配器寻找空闲的 Arena。 |
system_mem |
统计量。记录该 Arena 从系统中获取的总内存量。 |
工作流程概览
当你调用 malloc(size) 时,分配器会经历以下关于 malloc_state 的查找过程:
(1)寻找 Arena: 线程首先尝试获取它最后一次使用的那个 Arena 的锁。如果被占用,它会遍历 Arena 链表(通过 next 指针)寻找一个未上锁的。
(2)检查 Fastbins: 如果申请的大小属于 Fast Bin 范围,直接查看 fastbinsY 数组。
(3)检查 Bins: 如果 Fast Bin 没命中,则查找 bins 数组(Unsorted -> Small -> Large)。
(4)使用 Top Chunk: 如果所有 Bin 都空了,则计算 top 指针指向的剩余空间。
(5)系统调用: 如果连 Top Chunk 都不够了,则通过 sbrk (Main Arena) 或 mmap (Thread Arena) 向内核申请更多物理内存。
安全意义
在二进制漏洞利用中,malloc_state 对于攻击者来说非常重要。
- libc 地址泄露: 由于
main_arena是 libc.so 中的一个全局变量,泄露了malloc_state的地址(比如泄露了 Unsorted Bin 的指针)就能绕过 ASLR,计算出 libc 的基地址。 - 控制流篡改: 攻击者通过堆溢出伪造
malloc_state中的指针,可以诱导malloc返回任意地址(如栈地址或 GOT 表地址),实现任意代码执行。
核心单元:malloc_chunk
Bin管理的对象是malloc_chunk。理解其字段是理解 Bin 运作机制的前提。
1 | struct malloc_chunk { |
复用机制: 这里的 fd、bk 等指针仅在 Chunk 处于 Free(空闲) 状态并存在于 Bin 中时才有意义。当 Chunk 被分配给用户时,这些内存空间被用作 User Data。
Bin的分类与数据结构特性
ptmalloc 根据 chunk 的大小(Size)和操作策略(Policy),将 Bin 划分为四类。
Fast Bins、Unsorted Bin、Small Bins、Large Bins。
Fast Bins
这是为了优化小内存分配性能而设计的结构。
- 数据结构: 单向链表 (Singly Linked List)。
- 存储位置:
malloc_state.fastbinsY数组。 - 操作策略: LIFO (Last In, First Out)。最近释放的 chunk 会被插入链表头部,下次申请时最先被取出。
- 合并策略 (Coalescing): 不执行合并。即使两个 Fast Bin chunk 物理相邻,它们也不会合并成一个更大的 chunk。这通过保持 chunk 的
P(PREV_INUSE) 标志位为 1 来实现。 - 范围: 默认覆盖 16 到 80 字节(32位系统)或 16 到 160 字节(64位系统),具体取决于
SIZE_SZ。 - 技术细节: 由于是单向链表,Fast Bin chunk 只有
fd指针指向下一个节点,bk指针无定义。

Unsorted Bin
这是 chunk 被释放后的第一个缓冲区,也是唯一的非分类容器。
- 数据结构: 双向环形链表 (Doubly Linked Circular List)。
- 存储位置:
malloc_state.bins[1]。 - 功能逻辑: 当一个 chunk 被释放(且大小超出 Fast Bin 范围)时,它首先被插入 Unsorted Bin 的头部。
- 遍历逻辑: 在
malloc过程中,如果 Fast Bin 无法满足请求,分配器会遍历 Unsorted Bin。在遍历过程中,分配器会将每个遇到的 chunk 重新分类(Resort) 并放入对应的 Small Bin 或 Large Bin 中。 - 利用价值: 利用了时间局部性原理。如果程序释放了一个 chunk 随后立即申请相同大小的 chunk,可以直接从这里复用,避免了复杂的查找逻辑。

Small Bins
用于管理固定大小的中小块内存,采用精确匹配策略。
- 数据结构: 双向环形链表。
- 存储位置:
malloc_state.bins[2]到malloc_state.bins[63]。 - 大小映射: 每个 Small Bin 对应一个精确的固定大小,步长为 16 字节。例如,Bin 2 存储 32 字节,Bin 3 存储 48 字节,依此类推。
- 操作策略: FIFO (First In, First Out)。插入到头部,从尾部摘取。
- 合并策略: 不执行合并。Small Bins 中的 chunk 都是固定大小的,相邻的空闲 chunk 不会被合并,它们保持各自独立的状态。这样可以确保分配时的效率,避免额外的合并开销。
Small Bins 结构示意图:
1 | malloc_state.bins[] 数组: |
Large Bins
用于管理大块内存,支持范围分配和最佳匹配策略。
- 数据结构: 双向环形链表 + 跳表逻辑 (Skip-list logic)。
- 存储位置:
malloc_state.bins[64]到malloc_state.bins[126]。 - 大小映射: 范围映射 (Range Mapping)。每个 Large Bin 存储一定范围大小的 chunks。例如,Bin 64 存储 1024 字节及以上的 chunk(具体范围取决于复杂的计算公式)。
- 排序机制: 链表中的 chunk 按照大小(Size)从大到小排序。
- 性能优化: 为了避免在长链表中线性查找合适的大小,Large Bin chunk 使用了
fd_nextsize和bk_nextsize字段。这两个指针构成了一个子链表,仅连接大小不同的 chunk。这允许分配器快速跳过相同大小的 chunk,直接定位到满足大小需求的 chunk。
Large Bins 结构示意图:
1 | malloc_state.bins[] 数组 (Large Bins 部分): |
现代架构补充:Tcache(Thread Local Cache)
自 Glibc 2.26 版本引入,Tcache 位于 malloc_state 之外,是线程私有的堆缓存结构。
- 并发控制: 无锁 (Lock-free)。每个线程拥有独立的
tcache_perthread_struct,通过线程局部存储 (TLS) 实现,完全避免锁竞争。 - 数据结构: 单向链表,LIFO 策略(结构上类似 Fast Bin,但独立于 Fast Bin)。
- 优先级: 最高。
malloc时最先检查 Tcache,free时最先放入 Tcache。 - 限制: 有容量限制(默认每个 size 链表最多存 7 个 chunk,可通过
tcache_count宏调整)。 - 尺寸范围: 覆盖小块内存(24-1032字节,64位系统),与 Fast Bin 部分重叠但优先级更高。
Tcache 结构示意图:
1 | 线程私有 Tcache (tcache_perthread_struct): |
技术参数对比表
| 特性 | Tcache | Fast Bin | Small Bin | Large Bin | Unsorted Bin |
|---|---|---|---|---|---|
| 链表拓扑 | 单向 (fd) | 单向 (fd) | 双向环形 (fd/bk) | 双向环形 (fd/bk) + NextSize | 双向环形 (fd/bk) |
| 分配顺序 | LIFO | LIFO | FIFO | Sorted Best-Fit | 遍历过程中的 Exact Fit |
| 尺寸精度 | 精确大小 | 精确大小 | 精确大小 | 范围大小 | 混合大小 |
| 合并逻辑 | 不合并 (P=1) | 不合并 (P=1) | 不合并 | 自动合并 | 自动合并 |
| 索引计算 | 直接尺寸映射 (size/16) | 显式索引转换 | $$Index = Size / 16$$ | 复杂的范围分段函数 | 固定 Index 1 |
| 并发控制 | 无锁 (Thread-Local) | 有锁 | 有锁 | 有锁 | 有锁 |
| 优先级 | 最高 (先检查/后释放) | 中等 | 低 | 低 | 中等 |
| 容量限制 | 默认7个/chunk (可配置) | 无限制 | 无限制 | 无限制 | 无限制 |
| 存储位置 | 线程私有 (TLS) | malloc_state.fastbinsY | malloc_state.bins[2-63] | malloc_state.bins[64-126] | malloc_state.bins[1] |
| 安全特性 | 双重释放检测 (Double Free) | 无 | 无 | 无 | 无 |
内存布局视角的安全影响
在二进制安全领域,Bin 的链表结构特性直接导致了特定的漏洞利用原语(Primitives):
- 单向链表攻击 (Fastbin/Tcache): 由于只校验
fd,攻击者可以通过篡改fd指针,将其指向任意地址(如 Stack 或 GOT 表),下次malloc时即可获得该地址的控制权。 - 双向链表解链 (Unlink Exploit): 在 Small/Large Bin 中,当一个 chunk 被取出时,会执行
FD->bk = BK和BK->fd = FD。如果攻击者控制了fd和bk,这实际上是一个任意地址写(Arbitrary Write) 操作。 - Unsorted Bin 泄露: 由于 Unsorted Bin 是双向循环链表,链表头部的
bk指针指向main_arena结构体内部。通过读取未被清空的bk指针,攻击者可以计算出 libc 的基地址(ASLR Bypass)。
参考文献
Glibc 2.35 malloc_state 结构体定义:https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L1830 ↩︎