主要以 glibc 2.23 为例。
基础
进程内存布局

malloc_chunk 结构
malloc_chunk 是 glibc 用于管理堆内存的基本单元结构。每一块通过 malloc 分配出来的堆内存区域,其开头都带了一个 malloc_chunk 结构体。
malloc_chunk 结构体定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
#define INTERNAL_SIZE_T size_t // 内部大小类型,通常为 size_t
struct malloc_chunk {
// ----- 以下两个字段总是存在(无论是空闲还是占用) -----
INTERNAL_SIZE_T prev_size; // 前一个 chunk 的大小(如果前一个 chunk 是空闲的)
// 如果当前 chunk 的 size 字段中 PREV_INUSE 位为 0,
// 则该字段有效,用于向前合并。
INTERNAL_SIZE_T size; // 当前 chunk 的大小(包括元数据头部),低 3 位为标志位:
// - PREV_INUSE (0x1): 前一个 chunk 是否在使用
// - IS_MMAPPED (0x2): 是否由 mmap 分配
// - NON_MAIN_ARENA (0x4): 是否不属于主分配区
// ----- 以下字段仅当 chunk 空闲时才用于管理 -----
// (被占用时,这部分空间完全属于用户数据,可被覆盖)
struct malloc_chunk* fd; // 前向指针,指向空闲链表(bin)中的下一个空闲 chunk
struct malloc_chunk* bk; // 后向指针,指向空闲链表中的上一个空闲 chunk
// ----- 以下字段仅用于 large bins(大小 >= 512 字节的空闲块)-----
// 用于在同一个 bin 中按大小排序,方便快速查找
struct malloc_chunk* fd_nextsize; // 指向下一个不同大小的空闲 chunk
struct malloc_chunk* bk_nextsize; // 指向上一个不同大小的空闲 chunk
};
typedef struct malloc_chunk* mchunkptr; // 指向 malloc_chunk 的指针类型
|
当内存被使用时,元数据依然存在(但 fd/bk 部分被数据覆盖, prev_size 和 size 始终保留)
堆上的每一块小区域(无论是空闲还是占用),它的开头必然存在一个逻辑上的 malloc_chunk 头部,用来描述这块区域的大小、状态和邻居信息。

size 的低 3 位是标志位(A、M、P)。因为 chunk 的大小永远是 8 字节或 16 字节对齐的,所以低 3 位在表示真实大小时永远为 0,刚好可以用来存放标志。
- P(PREV_INUSE, bit 0):标记前一个 chunk 是否正在使用。如果 P=0,说明前一个 chunk 是空闲的,那么当前 chunk 的 prev_size 字段就有效(存着前一个 chunk 的大小),用来合并。
- M (IS_MMAPPED, bit 1):标记这块内存是不是用 mmap 分配的。如果是,free 时会用 munmap 释放,不会走常规的堆管理逻辑。
- A (NON_MAIN_ARENA, bit 2):标记这个 chunk 属于非主分配区(即多线程下其他的 arena)。主要用于区分不同 arena 的锁和行为。
每次读取 chunk 的实际大小时,会用掩码 ~(0x7) (清除低 3 位) 来得到真正的长度。
1
2
|
size & 0x7 // 取标志位
size & ~0x7 // 取大小
|
在 64 位下 malloc 分配的最小 chunk 是 0x20(32) 字节,即 8 字节的 userdata 和 24 字节的 matadata。(这里的 24 字节还有 8 字节是侵占它下一个可用 chunk 的 prev_size)
pwndbg 查看 chunk
- 使用
heap 查看 chunk:
可以看到 chunk 的信息,包括 allocated chunk, free chunk。

- 用
vis-heap-chunks 将堆的二进制数据可视化。

除此之外,用原生 gdb 命令也可以比较好的看原始二进制数据。
比如:
1
|
char* b = malloc(0x256);
|
可以查看 b 的地址为 0x555555559530 ,但是实际上前面 malloc_chunk 结构体(prev_size、size)还占据了前面 8 字节。
使用 x/20gx [addr] 查看数据,如:

arena
arena 表示单个线程维护的内存池。
对于每个线程都会有着一个 arena 实例用以管理属于该线程的堆内存区域,包括 bins、fastbin 都是放置在 arena 结构体中统一进行管理的。
在 glibc 中,用 struct malloc_state 结构体来描述 arena:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
struct malloc_state
{
/* 线程锁,用于多线程环境互斥访问该 arena */
mutex_t mutex;
/* 该 arena 的标志位,记录一些状态(如是否使用了 mmap 等) */
int flags;
/* Fast bins 数组,每个槽是一个单链表头,存放固定大小的小块空闲内存。
* NFASTBINS 通常为 10,对应 16 字节到 80 字节(64位系统)的块。
* Fast bin 中的块不会合并,以提升性能。
*/
mfastbinptr fastbinsY[NFASTBINS];
/* 指向 top chunk 的指针。top chunk 位于 arena 的顶端,
* 当所有 bin 都没有合适大小的空闲块时,malloc 会从 top chunk 切割新块。
*/
mchunkptr top;
/* 最近一次从 large bin 或 unsorted bin 中分割后剩余的“余块”。
* 用于优化连续申请小块内存时的缓存命中率,减少碎片。
*/
mchunkptr last_remainder;
/* 常规 bins 数组(双链表结构),总共有 NBINS 个 bin。
* bin 0 未被使用,bin 1 是 unsorted bin,bin 2 ~ 63 是 small bins,
* bin 64 ~ 126 是 large bins。
* 数组每个 bin 实际需要两个指针(fd, bk),因此长度是 NBINS * 2 - 2。
*/
mchunkptr bins[NBINS * 2 - 2];
/* 位图,用于快速标示哪些 bin 非空,加速分配时对 bins 的扫描。
* BINMAPSIZE 通常为 4(在 32/64 位下)。
*/
unsigned int binmap[BINMAPSIZE];
/* 指向下一个 arena 的指针,所有 arena 通过 next 字段连接成环形链表。 */
struct malloc_state *next;
/* 指向下一个空闲的 arena 链表,用于动态分配新 arena 时快速获取未使用的 arena。
* (某些 glibc 版本中可能不存在或用途略有差异,但主流版本保留)
*/
struct malloc_state *next_free;
/* 当前 arena 正在服务的线程数量(包括主线程和子线程)。
* 用于判断 arena 是否繁忙,决定是否创建新的 arena。
*/
INTERNAL_SIZE_T attached_threads;
/* 当前 arena 已向操作系统申请的总内存量(字节)。 */
INTERNAL_SIZE_T system_mem;
/* 系统内存的历史最大值,用于统计和调整。 */
INTERNAL_SIZE_T max_system_mem;
};
|
pwndbg 查看 arena
用 arenas 查看 arena 数据:

bin 链表
| 类型 |
存储大小 |
结构 |
分配策略 |
| fastbin |
16~80 bytes (64位) |
单链表 |
LIFO |
| unsorted bin |
任意大小 |
双链表 |
FIFO |
| small bin |
固定大小(<= 512 bytes) |
双链表 |
FIFO |
| large bin |
范围区间(>= 512 bytes) |
双链表(带 nextsize 指针优化) |
最佳适配 |
| tcache bin |
0x20~0x410 |
单链表 |
LIFO |
fastbin
快速小内存回收复用,速度最优。
由单链表实现
fastbin 在 malloc_state 定义如下:
1
|
mfastbinptr fastbinsY[NFASTBINS];
|
NFASTBINS

unsorted bin
unsorted bin 相当于 small bin 和 large bin 的一个缓存,可以让 malloc 有第二次机会重新利用最近 free 的 chunk。
只有一个 unsorted bin。
small bin
如果 unsorted bin 也无法满足需求,分配器就会进
large bin
top chunk
如果以上所有的 bin 都无法满足 malloc 请求,就会从 top chunk 上切割一块返回。
tcache bin
从 glibc 2.26 开始,引入tcache 来提升堆管理的性能。这里的 tcache 以 libc 2.27 为例。
tcache 引入了两个结构体: tcache_entry 和 tcache_perthread_struct。
1
2
3
4
5
6
7
8
9
10
11
|
typedef struct tcache_entry
{
struct tcache_entry *next;
/* uintptr_t key; glibc 2.32 添加 key 来防 double free*/
} tcache_entry;
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
|
tcache_entry 确定了 tcache bin 内空闲 chunk 的结构:单链表,next 指向下一个 chunk。该结构体存储直接复用空闲 chunk 的用户数据区开头。
tcache_perthread_struct 保存在堆空间的首部,用于存储每个线程的 tcache bin 内容。count 字符数组存储每个 tcache bin 的数量,最大值是 7,也就是也就是每个大小的 tcache 桶最多只能存 7 个。
后面的 entries 存储每个 bin 的头节点指针,一个元素 8 字节。
glibc 中默认有宏 TCACHE_MAX_BINS = 64 ,这样计算,是 0 ~ 63 个。最小的 chunk 大小为 0x20,chunk 每 0x10 字节分类,这样 tcache 存入最大的空闲 chunk 是 0x410。
这样来说,在堆中,前 0x10 直接会存放 tcache_perthread_struct 的 prev_size 和 size 的 chunk 段。之后 64 字节是 count,而后是 entries。
在 libc 中,tcache_perthread_struct 的 label 是 tcache,可以用 x/64bx tcache 打印每一个 count,用 x/64gx (char *)tcache+64 打印每一个 entry。

对 tcache 增删操作为 tcache_put() 和 tcache_get。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
return (void *) e;
}
|
没什么保护。
用 pwndbg 查看 bin
用 bin 查看各种 bin

也可以使用 tcachebins、unsortedbin、smallbins、largebins 查看每个类型的 bin。
对于一条 bins 下的链,比如:
1
2
|
tcachebins
0x30 [ 2]: 0x56490bff82d0 —▸ 0x56490bff82a0 ◂— 0
|
这里 tcache 由两个 chunk 地址构成一串单链表,也就是:
1
|
0x30 [ 2]: 0x56490bff82d0(chunk1) —▸ 0x56490bff82a0(chunk0) ◂— 0
|
参考资料
- how2heap深入浅出学习堆利用
- 【CTF资料-0x0002】简易Linux堆利用入门教程by arttnba3
- Part 2: Understanding the GLIBC Heap Implementation
- Cybersecurity - Notes Operations of the Fastbin
- pwndbg Documentation - vis-heap-chunks