堆介绍
堆是动态分配内存的区域,通常用于存储在运行时需要动态分配的对象。堆内存的分配和释放由程序员控制,常见的操作包括 malloc、free 等。

当前 Linux 标准发行版使用 ptmalloc2 作为堆的实现。ptmalloc2 是 glibc 的一部分,基于 Doug Lea 的 malloc 实现。
heap 相关概念
chunk 结构
在 ptmalloc2 中,堆内存被划分为多个块(chunk)。每个 chunk 都有一个头部和一个尾部,用于存储块的大小和状态信息。chunk 的结构如下:
1
2
3
4
5
6
7
8
9
| +-------------------------------+
| prev_size | size |
+-------------------------------+
| fd | bk |
+-------------------------------+
| fd_nextsize | bk_nextsize |
+-------------------------------+
| chunk data... |
+-------------------------------+
|
- prev_size:前一个 chunk 的大小(如果存在)
- size:当前 chunk 的大小和状态信息(如是否被分配)
- A / IS_MMAPPED(M):当前 chunk 是否通过
mmap 分配(A/M 在不同版本表现不同) - P / PREV_INUSE:前一块 chunk 是否被分配(即前一个 chunk 是否 inuse)
- M / NON_MAIN_ARENA:当前 chunk 是否属于主 arena(multi-arena 环境下)
- fd 和 bk:双向链表指针,用于管理空闲块的链表(如果 chunk 是空闲的)
- fd_nextsize 和 bk_nextsize:用于管理按大小排序的空闲块链表(如果 chunk 是空闲的且 chunk 较大)
- fd_nextsize 指向下一个空闲块
- bk_nextsize 指向上一个空闲块
一个已经分配的 chunk 称前两个字段称为 chunk header,后面的部分称为 user data。每次 malloc 申请得到的内存指针,其实指向 user data 的起始处
当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size 域无效,所以下一个 chunk 的该部分也可以被当前 chunk 使用。这就是 chunk 中的空间复用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
next . |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
动态调试
编写如下 c 语言代码
1
2
3
4
5
6
7
8
9
10
11
| #include <malloc.h>
int main(int argc, char *argv[]) {
char *p1 = malloc(0x18);
for (int i = 0; i < 0x18; i++) {
p1[i] = 'a';
}
malloc(0x10);
free(p1);
return 0;
}
|
在 free(p1) 下断点, 查看 heap 情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| pwndbg> heap
pwndbg will try to resolve the heap symbols via heuristic now since we cannot resolve the heap via the debug symbols.
This might not work in all cases. Use `help set resolve-heap-via-heuristic` for more details.
Allocated chunk | PREV_INUSE
Addr: 0x602000
Size: 0x20 (with flag bits: 0x21)
Allocated chunk | PREV_INUSE
Addr: 0x602020
Size: 0x20 (with flag bits: 0x21)
Top chunk | PREV_INUSE
Addr: 0x602040
Size: 0x20fc0 (with flag bits: 0x20fc1)
pwndbg> x/8gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000021
0x602010: 0x6161616161616161 0x6161616161616161
0x602020: 0x6161616161616161 0x0000000000000021
0x602030: 0x0000000000000000 0x0000000000000000
pwndbg>
|
可以看到, 这里展示了在内存中一个没有被释放的 chunk 情况
- 头部大小 0x10, 数据区域 0x10, 下一个 chunk 的 prev 字段被前一个 chunk 使用, userdata 大小为 0x10 + 0x8 = 0x18, 符合我们申请的内存
- 0x21 是 size 大小, 这里 size 的最低位 PREV_INUSE 被置为 1, 表示前一个 chunk 已经被使用, 对于 0x602000 这个 chunk 来说, 0x602020 是前一个 chunk 的地址, size 的最低位为 1, 所以这里的 size 是 0x21
执行 free(p1) 后, chunk 变成了空闲状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| pwndbg> heap
Free chunk (fastbins) | PREV_INUSE
Addr: 0x602000
Size: 0x20 (with flag bits: 0x21)
fd: 0x00
Allocated chunk | PREV_INUSE
Addr: 0x602020
Size: 0x20 (with flag bits: 0x21)
Top chunk | PREV_INUSE
Addr: 0x602040
Size: 0x20fc0 (with flag bits: 0x20fc1)
pwndbg> x/8gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000021
0x602010: 0x0000000000000000 0x6161616161616161
0x602020: 0x6161616161616161 0x0000000000000021
0x602030: 0x0000000000000000 0x0000000000000000
pwndbg> fastbins
fastbins
0x20: 0x602000 ◂— 0
pwndbg>
|
可以看到, data 区域还残留了部分的数据,但是 userdata 的前 8 个字节被清空了
其实这里的前 8 个字节就是 fd, 当前 chunk 被放到了 fastbins 中, fd 指向下一个空闲 chunk 的地址, bk 指向上一个空闲 chunk 的地址
bins 是 ptmalloc2 中的重要概念,这里简单理解就好,后文会详细介绍