alloca() 如何在内存级别上工作?

How does alloca() work on a memory level?

我正在尝试弄清楚 alloca() 在内存级别上的实际工作方式。来自 linux man page:

The alloca() function allocates size bytes of space in the stack frame of the caller. This temporary space is automatically freed when the function that called alloca() returns to its caller.

这是否意味着 alloca() 会将堆栈指针转发 n 字节?或者新创建的内存具体分配在哪里?

这不是和variable length arrays一模一样吗?

我知道实施细节可能留给 OS 之类的。但我想知道一般这是如何完成的。

是的,alloca在功能上等同于局部可变长度数组,即:

int arr[n];

还有这个:

int *arr = alloca(n * sizeof(int));

两者都为堆栈上 int 类型的 n 个元素分配 space。 arr 在每种情况下的唯一区别是 1) 一个是实际数组,另一个是指向数组第一个元素的指针,以及 2) 数组的生命周期以其封闭范围结束,而 alloca内存的生命周期在函数returns时结束。在这两种情况下,数组都驻留在堆栈上。

例如,给定以下代码:

#include <stdio.h>
#include <alloca.h>

void foo(int n)
{
    int a[n];
    int *b=alloca(n*sizeof(int));
    int c[n];
    printf("&a=%p, b=%p, &c=%p\n", (void *)a, (void *)b, (void *)c);
}

int main()
{
    foo(5);
    return 0;
}

当我 运行 我得到:

&a=0x7ffc03af4370, b=0x7ffc03af4340, &c=0x7ffc03af4320

这表明从 alloca 返回的内存位于两个 VLA 的内存之间。

VLA 首次出现在 C99 的 C 标准中,但 alloca 早于此。 Linux 手册页指出:

CONFORMING TO

This function is not in POSIX.1-2001.

There is evidence that the alloca() function appeared in 32V, PWB, PWB.2, 3BSD, and 4BSD. There is a man page for it in 4.3BSD. Linux uses the GNU version.

BSD 3 可以追溯到 70 年代后期,因此 alloca 是 VLA 在被添加到标准之前的早期非标准化尝试。

今天,除非您使用不支持 VLA 的编译器(例如 MSVC),否则实际上没有理由使用此函数,因为 VLA 现在是获得相同功能的标准化方法。

另一个 精确地描述了 VLA 和 alloca() 的机制。

但是,alloca()automatic VLA 之间存在显着的功能差异。对象的生命周期。

alloca() 的情况下,生命周期在函数 returns 时结束。 对于 VLA,对象在包含块结束时被释放。

char *a;
int n = 10;
{
  char A[n];
  a = A;
}
// a is no longer valid

{
  a = alloca(n);
}
// is still valid

因此,可以很容易地耗尽循环中的堆栈,而使用 VLA 则不可能做到这一点。

for (...) {
  char *x = alloca(1000);
  // x is leaking with each iteration consuming stack
}

for (...) {
  int n = 1000;
  char x[n];
  // x is released
}

虽然 alloca 从语法的角度看起来像一个函数,但它不能在现代编程环境中作为一个普通函数来实现*。它必须被视为具有类函数接口的编译器功能。

传统上,C 编译器维护两个指针寄存器,一个“堆栈指针”和一个“帧指针”(或基指针)。堆栈指针界定堆栈的当前范围。帧指针在函数入口处保存堆栈指针的值,用于访问局部变量并在函数退出时恢复堆栈指针。

现在大多数编译器在正常函数中默认不使用帧指针。现代 debug/exception 信息格式已使它变得不那么重要,但他们仍然理解它是什么,并且可以在需要的地方使用它。

特别是对于具有 alloca 或可变长度数组的函数,使用帧指针允许函数跟踪其堆栈帧的位置,同时动态修改堆栈指针以适应可变长度数组。

例如,我在 O1 为 arm

构建了以下代码
#include <alloca.h>
int bar(void * baz);
void foo(int a) {
    bar(alloca(a));
}

得到了(我的评论)

foo(int):
  push {fp, lr}     @ save existing link register and frame pointer
  add fp, sp, #4    @ establish frame pointer for this function
  add r0, r0, #7    @ add 7 to a ...
  bic r0, r0, #7    @ ... and clear the bottom 3 bits, thus rounding a up to the next multiple of 8 for stack alignment 
  sub sp, sp, r0    @ allocate the space on the stack
  mov r0, sp        @ make r0 point to the newly allocated space
  bl bar            @ call bar with the allocated space
  sub sp, fp, #4    @ restore stack pointer and frame pointer 
  pop {fp, pc}      @ restore frame pointer to value at function entry and return.

是的,alloca 和可变长度数组非常相似(尽管另一个答案指出不完全相同)。 alloca 似乎是两个构造函数中较旧的一个。


* 使用足够 dumb/predictable 的编译器,可以在汇编程序中将 alloca 实现为一个函数。特别是编译器需要。

  • 始终为所有函数创建帧指针。
  • 始终使用帧指针而不是堆栈指针来引用局部变量。
  • 在设置调用函数的参数时始终使用堆栈指针而不是帧指针。

这显然是它最初的实现方式 (https://www.tuhs.org/cgi-bin/utree.pl?file=32V/usr/src/libc/sys/alloca.s)。

我猜也有可能将实际实现作为汇编函数,但编译器中有一种特殊情况,当它看到 alloca 时会进入 dumb/predictable 模式,我不知道如果有编译器供应商这样做的话。

allocaVLAs 之间最重要的区别是失败案例。以下代码:

int f(int n) {
    int array[n];
    return array == 0;
}
int g(int n) {
    int *array = alloca(n);
    return array == 0;
}

VLA不可能检测到分配失败;这是一个非常 un-C 强加给语言结构的东西。因此,Alloca() 的设计要好得多。

alloca 分配内存,当函数调用 alloca return 时自动释放内存。也就是说,用 alloca 分配的内存对于特定函数的“堆栈框架”或上下文是本地的。

alloca 无法移植,并且很难在没有常规堆栈的机器上实现。它的使用是有问题的 (并且在基于堆栈的机器上明显的实现失败) 当它的 return 值直接传递给另一个函数时,如

fgets(alloca(100), 100, stdin)

如果您在任何不符合此描述的地方使用它,您就是在自找麻烦。如果你在任何这些地方使用 alloca(),你可能 运行 会遇到麻烦,因为在调用 alloca() 的时候堆栈上可能有一些东西:

  • 在一个循环中。
  • 在任何以局部变量开头的块内,函数的最外层块除外,尤其是在退出此块后使用分配的内存时。
  • 在赋值的左侧使用任何比指针变量更复杂的表达式,包括指针数组的一个元素。
  • 其中 alloca() 的 return 值用作函数参数。
  • 在使用 = 运算符的值的任何上下文中,例如

if ((pointer_variable = alloca(sizeof(struct something))) == NULL) { .... }

而且我希望有人会打电话给我,即使对于某些编译器生成的代码来说,这种高度限制性的限制不够保守。现在,如果它是作为内置编译器完成的,您可能会设法解决这些问题。

一旦我终于弄明白了 alloca() 函数,它就运行得相当好——我记得,它的主要用途是在 Bison parser 中。每次调用浪费 128 字节,再加上固定的堆栈大小,这可能是一件令人讨厌的事情。为什么我不直接使用 GCC?因为这是尝试将 GCC 最初使用交叉编译器移植到一台机器上,结果证明它几乎没有足够的内存来本地编译 GCC(1.35 左右)。当GCC 2出来的时候,原来内存够用了,原生编译自己是不行的。