44

我试图弄清楚alloca()在内存级别上的实际工作方式。从linux 手册页

alloca() 函数在调用者的堆栈帧中分配 size 个字节的空间。当调用 alloca() 的函数返回给它的调用者时,这个临时空间会自动释放。

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

这与可变长度数组不完全相同吗?

我知道实现细节可能留给操作系统和其他东西。但我想知道一般来说这是如何完成的。

4

5 回答 5

28

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

int arr[n];

还有这个:

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

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

例如,给定以下代码:

#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 手册页指出:

符合

此功能不在 POSIX.1-2001 中。

有证据表明 alloca() 函数出现在 32V、PWB、PWB.2、3BSD 和 4BSD 中。在 4.3BSD 中有一个手册页。Linux 使用 GNU 版本。

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

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

于 2021-10-01T13:52:39.597 回答
16

另一个答案准确地描述了 VLA 和alloca().

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

如果alloca()函数返回时生命周期结束。对于 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
}
于 2021-10-01T21:07:57.847 回答
4

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

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

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

特别是对于具有 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 似乎是两个构造函数中较老的一个。


* 使用足够愚蠢/可预测的编译器,可以将 alloca 实现为汇编器中的函数。特别是编译器需要。

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

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

我想也有可能将实际实现作为汇编函数,但是在编译器中有一个特殊情况,当它看到 alloca 时,它会进入哑/可预测模式,我不知道是否有任何编译器供应商这样做了。

于 2021-10-02T01:59:15.203 回答
1

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

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

fgets(alloca(100), 100, stdin)

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

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

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

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

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

于 2021-10-03T07:41:05.743 回答
-3

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

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

VLA 不可能检测到分配失败;这是强加于语言结构的非常非 C的事情。因此,Alloca() 设计得更好。

于 2021-10-02T15:12:13.593 回答