分配和释放堆内存的简单程序:
int main(int argc, char **argv) {
char *b1, *b2, *b3, *b4, *b_large;
b1 = malloc(8);
memset(b1, 0xaa, 8);
b2= malloc(16);
memset(b2, 0xbb, 16);
b3 = malloc(25);
memset(b3, 0xcc, 25);
b4= malloc(1000);
memset(b4, 0xdd, 1000);
free(b1);
free(b2);
free(b3);
free(b4);
第一次 free() 之前:
(gdb) x/20gx 0x555555559290
0x555555559290: 0x0000000000000000 0x0000000000000021
0x5555555592a0: 0xaaaaaaaaaaaaaaaa 0x0000000000000000
0x5555555592b0: 0x0000000000000000 0x0000000000000021
0x5555555592c0: 0xbbbbbbbbbbbbbbbb 0xbbbbbbbbbbbbbbbb
0x5555555592d0: 0x0000000000000000 0x0000000000000031
0x5555555592e0: 0xcccccccccccccccc 0xcccccccccccccccc
0x5555555592f0: 0xcccccccccccccccc 0x00000000000000cc
0x555555559300: 0x0000000000000000 0x00000000000003f1
0x555555559310: 0xdddddddddddddddd 0xdddddddddddddddd
0x555555559320: 0xdddddddddddddddd 0xdddddddddddddddd
在第一次 free() 之后:
(gdb) x/20gx 0x555555559290
0x555555559290: 0x0000000000000000 0x0000000000000021
0x5555555592a0: 0x0000000555555559 0xd13e7903c502febc
0x5555555592b0: 0x0000000000000000 0x0000000000000021
0x5555555592c0: 0xbbbbbbbbbbbbbbbb 0xbbbbbbbbbbbbbbbb
0x5555555592d0: 0x0000000000000000 0x0000000000000031
0x5555555592e0: 0xcccccccccccccccc 0xcccccccccccccccc
0x5555555592f0: 0xcccccccccccccccc 0x00000000000000cc
0x555555559300: 0x0000000000000000 0x00000000000003f1
0x555555559310: 0xdddddddddddddddd 0xdddddddddddddddd
0x555555559320: 0xdddddddddddddddd 0xdddddddddddddddd
我期望在第二行内存中看到可读的前进和后退指针,并且 第三行 0x20 在两个 8 字节段中。
任何人都可以解释为什么 free() 函数会以这种方式运行吗?
假设您使用的是 Ubuntu 22.04,因此使用 glibc 2.36。
您看到的看似随机的值是内部
tcache_key
(源)。它是一个随机的 uintptr_t
值,在程序启动时使用 getrandom(2)
初始化一次,然后插入到保存在 tcache 中的空闲块中。
tcache_key
的值被插入到进入tcache的块中,然后检查空闲作为针对双重释放的简单强化。当重新分配 tcache 块时,它会被“删除”。这是在 glibc 2.34 中实现的。以前(glibc 2.29-2.33)使用固定值而不是 tcache_key
。如果您想知道“tcache”是什么,它是由多个存储桶组成的每线程缓存。每个存储桶都保留给定的分配大小,并将该大小的已释放块保存在最多 7 个元素(按 LIFO 顺序)的单链表中(新释放的块插入到头部)。当存储桶已满时,释放该大小的块不会将其添加到 tcache 中,而是遵循“正常”释放过程,并根据相当复杂的 glibc 算法最终进入正常的竞技场“垃圾箱”之一。
指向 tcache 存储桶中下一个块的指针也通过专用宏在 free 上被破坏,在 alloc 上未破坏(如您在
here 中看到的),这些宏使用内核通过 提供的映射 (mmap_base
) 的隐式随机性ASLR。这就是为什么您在块中也看不到清晰的指针。 在
free(b1);
free(b2);` 之后,我有以下内容:
(gdb) x/20gx 0x5555555592a0 - 0x10
0x555555559290: 0x0000000000000000 0x0000000000000021
0x5555555592a0: 0x0000000555555559 0xdefa7fb306dd6989
0x5555555592b0: 0x0000000000000000 0x0000000000000021
0x5555555592c0: 0x000055500000c7f9 0xdefa7fb306dd6989
0x5555555592d0: 0x0000000000000000 0x0000000000000031
0x5555555592e0: 0xcccccccccccccccc 0xcccccccccccccccc
0x5555555592f0: 0xcccccccccccccccc 0x00000000000000cc
0x555555559300: 0x0000000000000000 0x00000000000003f1
0x555555559310: 0xdddddddddddddddd 0xdddddddddddddddd
0x555555559320: 0xdddddddddddddddd 0xdddddddddddddddd
b1
和
b2
最终都位于同一个 tcache
桶中,即最小的一个,尺寸为 16(malloc(8)
四舍五入为 16)。看起来像这样:
{
counts = {2, 0, 0, 0, ...},
entries = {0x5555555592c0, 0x0, 0x00, 0x00, ...}
}
如上所示,
0xdefa7fb306dd6989
是
tcache_key
的值。查看 0x5555555592c0
处的空闲块,其 ->next
指针已损坏,值为 0x000055500000c7f9
。真实值可以通过以下方式获得:((((size_t)0x5555555592c0) >> 12) ^ ((size_t)0x000055500000c7f9)) == 0x5555555592a0
有关更多信息,另请参阅这篇有趣的文章:Tcache Keys 原始的双免保护.