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

gla*_*des 45 c alloca stack-frame variable-length-array

我试图弄清楚alloca()在记忆层面上实际上是如何工作的。来自Linux 手册页

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

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

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

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

dbu*_*ush 30

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

int arr[n];
Run Code Online (Sandbox Code Playgroud)

和这个:

int *arr = alloca(n * sizeof(int));
Run Code Online (Sandbox Code Playgroud)

两者都为堆栈上的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;
}
Run Code Online (Sandbox Code Playgroud)

当我运行这个时,我得到:

int arr[n];
Run Code Online (Sandbox Code Playgroud)

这表明返回的内存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 的早期非标准化尝试。

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

  • @UnholySheep,是的,但是这个可选功能完全失败了。支持 VLA 的编译器仍然支持它,那些不支持的编译器仍然不支持,遵守 C 标准的价值只是被削弱了。 (7认同)
  • 不使用“alloca”的原因是它是非标准的,而 VLA 是非标准的。 (4认同)
  • Alloca 在循环中的行为非常不同,它很容易耗尽堆栈。这是因为用 alloca 获取的对象的生命周期在函数返回时结束。而 VLA 的生命周期在其包含块结束时结束。所以 VLA 更安全 (4认同)
  • C11 和更新的标准不要求 VLA 支持(例如:MSVC 不支持它们) (3认同)
  • @tstanisl 在某些情况下,在函数返回之前生存是比 VLA 更喜欢“alloca”的一个原因,例如,如果您需要有条件地分配一些临时空间。 (3认同)
  • 如果有人好奇,[这个](https://www.tuhs.org/cgi-bin/utree.pl?file=32V/usr/src/libc/sys/alloca.s)可能是最早的 `alloca()` 实现,在 VAX 汇编中作为 UNIX/32V 的一部分 (1979)。它只是一个普通函数,像其他函数一样调用,而不是宏或编译器内在函数;编译器不需要知道任何事情。显然,C 编译器会引用与帧指针相关的所有局部变量,并在正常返回时从帧指针恢复堆栈指针,因此函数可以安全地使用不同的堆栈指针返回。 (2认同)

tst*_*isl 17

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

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

如果alloca()函数返回时生命周期结束。对于 VLA,对象在包含块结束时被释放。

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

{
  a = alloca(n);
}
// is still valid
Run Code Online (Sandbox Code Playgroud)

因此,可以轻松地耗尽循环中的堆栈,而 VLA 则无法做到这一点。

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

for (...) {
  int n = 1000;
  char x[n];
  // x is released
}
Run Code Online (Sandbox Code Playgroud)


plu*_*ash 5

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

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

如今,大多数编译器在普通函数中默认不使用帧指针。现代调试/异常信息格式使其变得不必要,但他们仍然了解它是什么并且可以在需要时使用它。

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

例如我在O1为arm构建了以下代码

#include <alloca.h>
int bar(void * baz);
void foo(int a) {
    bar(alloca(a));
}
Run Code Online (Sandbox Code Playgroud)

并得到了(我的评论)

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 from frame pointer 
  pop {fp, pc}      @ restore frame pointer to value at function entry and return.
Run Code Online (Sandbox Code Playgroud)

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


* 使用足够愚蠢/可预测的编译器,可以将 alloca 实现为汇编程序中的函数。具体来说,编译器需要。

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

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

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