【发布时间】:2011-12-31 13:02:18
【问题描述】:
malloc 是确定性的吗?假设我有一个分叉的进程,即另一个进程的副本,并且在某些时候它们都调用了 malloc 函数。两个进程分配的地址是否相同?假设执行的其他部分也是确定性的。
注意:这里我说的是虚拟内存,不是物理内存。
【问题讨论】:
malloc 是确定性的吗?假设我有一个分叉的进程,即另一个进程的副本,并且在某些时候它们都调用了 malloc 函数。两个进程分配的地址是否相同?假设执行的其他部分也是确定性的。
注意:这里我说的是虚拟内存,不是物理内存。
【问题讨论】:
完全没有理由让它是确定性的,事实上它不是确定性的可能有一些好处,例如increasing the complexity of exploiting bugs(另见this paper)。
这种随机性有助于使漏洞更难编写。要成功利用缓冲区溢出,您通常需要做两件事:
如果内存位置不可预测,那么跳转会变得非常困难。
标准§7.20.3.3/2中的相关引用:
malloc 函数为大小为 由大小指定,其值不确定
如果打算使其具有确定性,那么将明确说明。
即使它今天看起来是确定性的,我也不会打赌它会在更新的内核或更新的 libc/GCC 版本上保持不变。
【讨论】:
fork() 复制了所有线程,但你是对的,规范也非常清楚 - pubs.opengroup.org/onlinepubs/009695399/functions/fork.html - malloc() 不是'不是异步安全的,如果我没记错的话,这意味着你不能在 fork() 之后合法地在孩子中调用它,因为它似乎在多线程 fork() 之后对孩子强加了异步安全要求。跨度>
vfork 之后不安全——但在fork 之后就完全没问题了。
C99 规范(至少在其 final public draft 中)在“J.1 未指定行为”中声明:
以下未指定: ... 连续调用分配的存储顺序和连续性 calloc、malloc 和 realloc 函数 (7.20.3)。
因此,malloc 似乎不必是确定性的。因此,假设它是不安全的。
【讨论】:
这完全取决于malloc 的实现。特定的malloc 实现会引入非确定性并没有内在的原因(可能作为应用程序模糊测试除外,但即便如此,它也应该默认禁用)。例如,Doug Lea's malloc 不使用rand(3) 或其中的任何类似方法。
但是,由于malloc 会调用内核,例如 Linux 上的 sbrk(2) 或 mmap(2) 或 Windows 上的 VirtualAlloc,因此这些系统调用可能并不总是确定性的,即使在其他相同的进程中也是如此。无论出于何种原因,内核都可能决定在不同的进程中故意提供不同的mmap'ed 地址。
因此,对于通常在没有系统调用的用户空间中进行服务的小型分配,很可能在fork() 之后生成的指针将是相同的;由系统服务的大型分配一次调用可以是相同的。
不过,一般来说,不要依赖它。如果您确实需要在不同的进程中使用相同的指针,请在分叉之前创建它们,或者使用共享内存并适当地共享它们。
【讨论】:
malloc 请求的 mmap 调用。
是的,它在某种程度上是确定性的,但这并不一定意味着它会在一个进程的两个分支中给出相同的结果。
例如,单一 Unix 规范说:“[...] 为避免错误,子进程只能执行异步信号安全操作,直到调用其中一个 exec 函数。”
无论好坏,malloc 不在“异步信号安全”函数列表中。
此限制在讨论多线程程序的部分中,但未指定该限制是仅适用于多线程程序,还是也适用于单线程程序。
结论:您不能指望malloc 在父子节点中产生相同的结果。如果程序是多线程的,你不能指望malloc 在子进程中工作,直到它调用exec - 并且存在合理的问题,即使在单线程子进程中它实际上也能保证工作在孩子打电话给exec之前。
参考资料:
【讨论】:
fork(2) 确实完美地再现了整个虚拟地址空间。当fork() 返回时,所有指针在父子节点中仍然有效。 (这当然需要虚拟内存有两个使用相同虚拟地址的进程,这使得高效的写时复制实现成为可能。)最后我读到,Windows 本身并不提供 fork() 系统调用。 cygwin必须模仿它,这并不容易。所以如果你习惯了 Windows 系统调用,我猜 fork() 会显得很奇怪。
mmap 当然是不确定的,所以整个问题的答案是肯定的“否”。我特别评论了您的说法,即“fork 必须以相同的方式复制堆(包括所有空闲块)。”,确实如此,所以这不是障碍。
您不会得到相同的物理地址。如果您有进程 A 和 B,则每次调用 malloc 都会返回一个空闲块的地址。 A 和 B 调用 malloc 的顺序是不可预测的。但它永远不会“同时”发生。
【讨论】:
从技术上讲,如果分叉的进程都请求相同大小的块,它们应该分配相同的地址,但每个地址将指向不同的物理/实际内存位置。
Linux 为 fork 使用写时复制,因此 fork 的子代共享其父代的内存,直到任一进程中的某些内容发生更改。此时,内核会通过内存复制序列为分叉的子节点提供其内存空间的专用/唯一副本。
【讨论】:
malloc 的作者是未指定的,并且随机性通常是由内核调用本身引入的,而不是用户空间中的任何内容,因此 libc 实现不必调用 rand() 或任何类似的疯狂来使其具有不确定性。
malloc 可能只是对sbrk 或mmap 的调用(因为我们正在寻找确定性,我们不能安全地假设剩余的内存足够来自free 或之前的sbrk/mmap 调用来处理请求),这些调用都没有承诺比尝试满足请求本身更多。调用其中任何一个的新指针可以明智地在可用的虚拟地址空间内随机化
sbrk 是确定性的。父母和孩子都在同一个地方休息,因为fork() 复制了父母的地址空间。在 Linux 上,底层系统调用是 void *sys_brk(void*) 并且只是设置中断,返回新值。 glibc 会跟踪旧的中断,以了解为 sbrk 增量函数调用传递什么值。