为什么 `alloca` 函数不检查能否分配内存?

4
为什么alloca不检查是否可以分配内存?
来自man 3 alloca
如果分配导致堆栈溢出,则程序行为是未定义的。……如果无法扩展堆栈帧,则没有错误指示。
为什么alloca不/不能检查是否可以分配更多内存?
我理解的方式是alloca在堆栈上分配内存,而(s)brk在堆上分配内存。 来自https://en.wikipedia.org/wiki/Data_segment#Heap
堆区域由malloc,calloc,realloc和free管理,它们可能使用brk和sbrk系统调用来调整其大小
来自man 3 alloca
alloca()函数在调用者的堆栈帧中分配大小字节的空间。
堆栈和堆沿着汇合方向增长,如此维基百科图表所示:

enter image description here

(上图来自Wikimedia Commons,作者Dougct,发布在CC BY-SA 3.0下)
现在,alloca(s)brk都返回指向新分配内存的开头的指针,这意味着它们都必须知道当前时刻堆栈/堆的结束位置。实际上,从man 2 sbrk中可以看到:
引用:

调用增量为0的sbrk()可用于查找程序断点的当前位置。

因此,我的理解是,检查alloca是否能够分配所需的内存基本上归结为检查当前堆栈末尾和堆末尾之间是否有足够的空间。如果在堆栈上分配所需的内存会使堆栈达到堆,则分配失败;否则,它成功。
那么,为什么不能使用这样的代码来检查alloca是否可以分配内存呢?
void *safe_alloca(size_t size)
{
    if(alloca(0) - sbrk(0) < size) {
        errno = ENOMEM;
        return (void *)-1;
    } else {
        return alloca(size);
    }
}

这对我来说更加令人困惑,因为显然(s)brk可以进行这样的检查。从man 2 sbrk中得知:
brk()将数据段的末尾设置为addr指定的值,当该值合理、系统有足够的内存并且进程没有超过其最大数据大小(参见setrlimit(2))时。
所以如果(s)brk可以进行这样的检查,那么为什么alloca不能呢?

另外,请注意,虽然地址空间有余地,并不意味着alloca一定会成功。在大多数情况下,堆栈大小限制将决定堆栈是否可以扩展。通常在用尽地址空间之前,这个限制就会被达到。 - Tom Karzes
你能解释一下当有10个线程时,那个图是如何工作的吗?(每个线程都有自己的堆栈,但有一个共同的堆) - M.M
顺便说一句,你的图像现在太简单了.... 尝试使用“cat /proc/$$/maps”来理解。 - Basile Starynkevitch
@M.M 好的,我明白了,这张图已经过时了。现在我在维基百科上读到了这样一段话:“栈区传统上与堆区相邻,它们互相增长;当栈指针遇到堆指针时,空闲内存被耗尽。随着大型地址空间和虚拟内存技术的出现,它们 tend to be placed more freely” https://en.wikipedia.org/wiki/Data_segment#Stack - user4385532
为什么要点踩? - user4385532
显示剩余2条评论
2个回答

4

alloca是一个非标准的编译器内部函数,其卖点是它编译成极轻量级的代码,甚至可能只有一个指令。它基本上执行了每个函数在局部变量开始处执行的操作-将堆栈指针寄存器移动指定的数量并返回新值。与sbrk不同,alloca完全位于用户空间中,并且无法知道剩余可用的堆栈大小。

堆栈向堆增长的图像是学习内存管理基础的有用心理模型,但对于现代系统来说并不真实准确:

  • 正如cmaster在他的回答中所解释的那样,栈大小将主要受到内核强制执行的限制,而不是栈与堆字面上的冲突,特别是在64位系统上。
  • 在多线程进程中,不是一个栈,而是每个线程一个栈,它们显然不能全部向堆增长。
  • 堆本身不是连续的。现代malloc实现使用多个竞技场来提高并发性能,并将大型分配卸载到匿名的mmap,确保free将它们返回给操作系统。后者也位于传统上描述的单竞技场“堆”之外。

可以想象一种版本的alloca从操作系统查询此信息并返回正确的错误条件,但这会失去其性能优势,甚至可能比malloc(仅偶尔需要向操作系统获取更多进程内存,并且通常在用户空间工作)还要慢。


1
此外,即使实现者想要编写“从操作系统查询此信息的版本”,也会存在一个问题,即操作系统可能根本没有提供查询的方法。因此,对于OP的问题,另一个答案是“修复”alloca需要进行两个步骤,但迄今为止还没有人感到有必要实现这两个步骤。 - Steve Summit
增强的alloca为什么需要从操作系统查询这些信息呢?肯定可以在分配堆栈时将其提供和存储在程序的某个位置吧? - Alex Celeste
@SteveSummit 同意,假设的固定 alloca 需要编译器和操作系统供应商共同努力。这种事情并非闻所未闻,但需要有严肃的回报才能着手进行。 - user4815162342
@SteveSummit 为什么这会那么难呢,getrlimit(RLIMIT_STACK, &response) 不就足够了吗?参见 man 2 getrlimit - user4385532
1
@Leushenko @gaazkam 我们知道栈限制不能改变,例如通过调用 setrlimit 吗? - user4815162342
显示剩余2条评论

3
这张图片有点过时了:在现代系统中,堆内存区域和包含调用栈的内存区域是完全分开的实体,并且在64位系统上它们相距很远。内存断裂的概念是为具有小地址空间的系统设计的。
因此,栈空间的限制不是它可能会增长到堆的顶部,而是内核可能没有剩余内存来支持它。或者内核可能决定你的栈已经增长太多(达到了某个限制),从而关闭你的进程。
进程通过将堆栈指针减少并将一些数据存储在那里来增长堆栈。如果该内存访问当前未映射到你的地址空间,硬件立即向操作系统内核发出信号,内核检查失败的内存访问发生在哪里,如果刚好在栈内存区域下面,它将立即扩展内存映射,在那里映射一个新的内存页,并将控制权归还给你的进程以重试其内存访问。进程看不到这一切。它只看到它对堆栈内存的访问成功了。 alloca() 在任何方面都不偏离这个规则:你要求它在栈上分配一些内存,它会相应地减少堆栈指针。然而,如果你的分配太大,以至于操作系统不认为它是有效的堆栈内存访问,它会(可能且希望)用一个SEGFAULT关闭你的进程。然而,由于行为是未定义的,任何事情都可能发生。

1
对于最后一句话的评论:嗯,堆栈溢出是未定义行为。如果你很幸运,你可能会得到一个段错误。但也可能发生其他事情(比如恶意程序通过利用安全漏洞导致溢出,清空你的银行账户并引发全球核战争)。 - hyde
@hyde 对的。我刚刚接受了 klutt 的编辑,进行了精确更改 :-) - cmaster - reinstate monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接