为什么`alloca`不检查它是否可以分配内存?

时间:2017-10-14 10:51:17

标签: c memory-management stack-overflow sbrk alloca

为什么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下发布的Doug 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

2 个答案:

答案 0 :(得分:5)

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

堆栈向堆堆积的图像是学习内存管理基础知识的有用心理模型,但在现代系统上并不准确:

  • 正如cmaster在他的回答中所解释的那样,堆栈大小将主要受内核强制执行的限制,而不是堆栈直接冲入堆中,特别是在64位系统上。
  • 在多线程进程中,没有一个堆栈,但是每个线程都有一个堆栈,并且它们显然不能全部朝向堆扩展。
  • 堆本身不是连续的。现代malloc实施使用多个竞技场improve concurrent performance,并将大量分配卸载到匿名mmap,确保free returns them to the OS。后者也在单竞技场之外&#34;堆&#34;正如传统描述的那样。

可以设想alloca版本从操作系统查询此信息并返回正确的错误条件,但随后它的性能优势将会丢失,甚至可能与malloc相比(只偶尔需要去操作系统为进程获取更多内存,并且通常在用户空间中工作。)

答案 1 :(得分:4)

图片有点过时:在现代系统中,堆内存区域和包含调用堆栈的内存区域是完全独立的实体,它们在64位系统上相距很远。内存中断的概念是为具有小地址空间的系统设计的。

因此,堆栈空间的限制并不是它可能会增长到堆的顶部,限制是内核可能没有任何内存可以支持它。或者内核可能会判断您的堆栈已经增长太多(达到某个限制),从而导致您的进程崩溃。

您的进程只需通过递减堆栈指针并在那里存储一些数据来增加堆栈。如果该内存访问当前未映射到您的地址空间,则硬件会立即向OS内核发出此状态信号,该内核会检查发生内存访问失败的位置,如果它位于堆栈内存区域之下,则会立即扩展该内存映射,在那里映射新的内存页面,并将控制权交还给您的进程以重试其内存访问。 该过程未看到任何此。它只是看到它对堆栈内存的访问成功了。

alloca()不会以任何方式偏离此:您要求它在堆栈上分配一些内存,并通过相应地递减堆栈指针来实现。但是,如果您的分配太大以至于操作系统没有看到对它的内存访问作为有效的堆栈内存访问,它将(可能并且希望)使用SEGFAULT来终止您的进程。但是,由于行为未定义,任何事情都可能发生。