Linux 堆结构和 malloc() 和 free() 的行为

Posted

技术标签:

【中文标题】Linux 堆结构和 malloc() 和 free() 的行为【英文标题】:Linux heap structure and the behaviour with malloc() and free() 【发布时间】:2012-05-19 10:18:37 【问题描述】:

我有一个带有 Linux 2.6 内核的 D​​ebian,我试图了解堆在 malloc()free() 下的工作/行为方式。我试图搜索malloc()free() 算法和堆结构,但找不到任何有用的东西。不幸的是,我对 Linux 和内存的工作原理知之甚少,无法理解 free()malloc() 的源代码。

这是一个示例代码:

int main(int argc, char **argv)

    char *a, *b, *c;

    a = malloc(32);
    b = malloc(32);
    c = malloc(32);

    strcpy(a, argv[1]);
    strcpy(b, argv[2]);
    strcpy(c, argv[3]);

    free(c);
    free(b);
    free(a);

使用gdbrun AAAA BBBB CCCC 我可以检查堆。这是strcpys 之后但frees 之前的状态:

(gdb) x/32x 0x804c000
0x804c000:  0x00000000  0x00000029  0x41414141  0x00000000
0x804c010:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c020:  0x00000000  0x00000000  0x00000000  0x00000029
0x804c030:  0x42424242  0x00000000  0x00000000  0x00000000
0x804c040:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c050:  0x00000000  0x00000029  0x43434343  0x00000000
0x804c060:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c070:  0x00000000  0x00000000  0x00000000  0x00000f89

您可以很好地看到 char 数组。然后我试图弄清楚为什么会有 0x29(12 月 41 日)。我希望像 0x20(12 月 32 日)或 0x24(12 月 36 日)这样的东西。

为什么 malloc 算法会浪费这个空间? 怎么判断是0x29? 最后的 0xf89 代表什么? 程序如何跟踪分配的内容和空闲的内容?

特别想了解free() 的工作原理。三个释放后,堆看起来像这样:

(gdb) x/32x 0x804c000
0x804c000:  0x00000000  0x00000029  0x0804c028  0x00000000
0x804c010:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c020:  0x00000000  0x00000000  0x00000000  0x00000029
0x804c030:  0x0804c050  0x00000000  0x00000000  0x00000000
0x804c040:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c050:  0x00000000  0x00000029  0x00000000  0x00000000
0x804c060:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c070:  0x00000000  0x00000000  0x00000000  0x00000f89
为什么用这个特定的地址替换 char 数组? free 的伪代码是什么?

看这个例子:

(gdb) run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADDDDD BBBB CCCC
...
(gdb) x/32x 0x804c000
0x804c000:  0x00000000  0x00000029  0x41414141  0x41414141
0x804c010:  0x41414141  0x41414141  0x41414141  0x41414141
0x804c020:  0x41414141  0x41414141  0x44444444  0x00000044
0x804c030:  0x42424242  0x00000000  0x00000000  0x00000000
0x804c040:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c050:  0x00000000  0x00000029  0x43434343  0x00000000
0x804c060:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c070:  0x00000000  0x00000000  0x00000000  0x00000f89
...
(gdb) c
Program exited with code 021.

我已经覆盖了 0x29,但程序正常退出。 但是当我添加另一个字节时,我遇到了分段错误:

(gdb) run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADDDDD BBBB CCCC
...
(gdb) x/32x 0x804c000
0x804c000:  0x00000000  0x00000029  0x41414141  0x41414141
0x804c010:  0x41414141  0x41414141  0x41414141  0x41414141
0x804c020:  0x41414141  0x41414141  0x44444444  0x00004444
0x804c030:  0x42424242  0x00000000  0x00000000  0x00000000
0x804c040:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c050:  0x00000000  0x00000029  0x43434343  0x00000000
0x804c060:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c070:  0x00000000  0x00000000  0x00000000  0x00000f89
...
(gdb) c
Program received signal SIGSEGV, Segmentation fault.
0x080498b9 in free (mem=0x804c030) at common/malloc.c:3631

对我来说最重要的问题是:

为什么当您覆盖更多字节时,free() 中会出现分段错误? free() 算法是如何工作的? malloc 和 free 如何跟踪地址?

非常感谢您的阅读, 亲切的问候

【问题讨论】:

你看过Doug Lea's malloc吗? 我认为标准的 glibc malloc 实现称为ptmalloc。 这是我在 SO 上看到的最好的问题之一。 【参考方案1】:

大多数malloc() 实现通过跟踪堆本身内的堆状态来工作,就在分配的内存块之前和/或之后。超出分配的块会导致此数据损坏——其中一些数据可能包括指针或长度,损坏这些数据会导致实现尝试访问无效的内存位置。

malloc() 实现工作的细节取决于您使用的系统和 libc。如果您使用的是 glibc(如果您使用的是 Linux,则很可能),这里有一个很好的解释它的工作原理:

http://gee.cs.oswego.edu/dl/html/malloc.html

假设是这种情况,您看到的 0x29 可能是块大小 (32 = 0x20) 和一些标志的按位或。这是可能的,因为所有堆分配都四舍五入到最接近的 16 个字节(或更多!),因此始终可以假定大小的低 8 位为零。

【讨论】:

【参考方案2】:

为什么覆盖更多字节时 free() 中会出现分段错误?

一旦你通过了你要求的空间的尽头,你在技术上调用了未定义的行为,所以一切皆有可能。最终,您将破坏内部数据结构中的指针或大小字段,并且这种损坏可能会或可能不会导致引用狂野到足以引用不存在的整个页面。

也就是说,分段错误是页面保护的结果。这可以很好地保护一个完整的程序免受另一个不相关的程序的影响,并且是操作系统用来限制对单个用户模式地址空间的破坏的工具。这种机制与内部数据结构没有紧密同步。有效指针和有效页面之间存在粗略的对应关系,但并不准确。

free() 算法是如何工作的?

当 malloc() 返回一个块时,会创建一个内部数据结构,以便当该确切块传递给 free() 时,它将被识别并链接到一个空闲列表中。如果可能,它将与相邻的空闲块合并。

malloc 和 free 如何跟踪地址?

由于您运行的是 Linux,源代码是可用的,阅读它自然会得到最准确的答案。但是,一般的答案是保留一个目录。这个目录可能是一个单独的数据结构,也可能是一个链表的形式,元数据保存在返回的实际地址前面.

为什么会浪费空间?

这并没有完全浪费。一些空间可能用于目录,而另一些空间可以通过保持块在缓存边界上对齐来换取性能。对大小与高速缓存行相等或可能更小的小块进行映像。如果此块与高速缓存行边界重叠,则将其保留在高速缓存中将需要两倍的空间。如果这种情况发生在任何地方,缓存实际上将是一半大小并且命中率更低。 (嗯,除了实际需要相邻地址的情况。)使用更大的块也会导致更少的internal fragmentation,这实际上可能会在 malloc() 和 free() 调用平衡的稳态中使用更少的内存在一个长期运行的过程中。

【讨论】:

【参考方案3】:

我不知道具体的细节。但总的来说,它是这样工作的:

较大的malloc()s 由mmap() 处理,因此我们专注于较小的malloc()s。您可以在某处设置阈值。

较小的malloc()s 在数据段的末尾处理。这可以由 glibc 使用系统调用 brk()sbrk() 处理和调整大小。

在您malloc() 一个内存块之后,必须保留它以便知道在调用free() 时要释放多少,并且必须保留指向下一个块的指针以便找到它们并且将它们链接在一起。

free()ing 一个位于末尾的内存块之后,数据段被减少为sbrk()。在free()ing 一个不在末尾的块之后,该块被添加到空闲列表中。这是一个空闲内存块的链表,以便重用它们。

0x29,即41,是你分配的内存块大小加上一点内存来保存所述字段(大小和下一个指针),需要8个字节。 9th 是干什么用的,我不知道,但可能与对齐有关。

如果你写了超过“承诺”的 32 个字节,你就破坏了这个链表和与之关联的指针。因此,free() 有错误的数据,它信任并尝试将其写入不允许的位置,从而导致SIGSEGV

【讨论】:

以上是关于Linux 堆结构和 malloc() 和 free() 的行为的主要内容,如果未能解决你的问题,请参考以下文章

尝试使用 malloc() 分配一个包含内部集合的结构来堆内存 [重复]

Linux 堆溢出原理分析

Linux 堆溢出原理分析

从上到下看linux内存管理--glibc malloc

malloc() |堆栈和堆位置的内存地址长度的差异| C 编程

堆/栈的比较 以及 malloc/new动态内存的开辟