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

tmb3ates  于 2023-06-21  发布在  其他
关注(0)|答案(4)|浏览(125)

我试图弄清楚alloca()在内存级别上实际上是如何工作的。关于linux man page
alloca()函数的作用是在调用者的堆栈帧中分配大小字节的空间。当调用alloca()的函数返回给它的调用者时,这个临时空间会被自动释放。
这是否意味着alloca()会将堆栈指针转发n字节?或者新创建的内存分配到哪里?
这不是和variable length arrays完全一样吗?
我知道实现细节可能留给操作系统和其他东西。但我想知道一般情况下这是如何实现的。

t40tm48m

t40tm48m1#

  • alloca* 和 VLA 之间最重要的区别是失败情况。下面的代码:
int f(int n) {
    int array[n];
    return array == 0;
}
int g(int n) {
    int *array = alloca(n);
    return array == 0;
}

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

b0zn9rqh

b0zn9rqh2#

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

int arr[n];

还有这个

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

两者都为堆栈上int类型的n元素分配空间。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现在是获得相同功能的标准化方法。

6vl6ewon

6vl6ewon3#

另一个answer精确地描述了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
}

vs

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

gojuced74#

尽管alloca从语法的Angular 看起来像一个函数,但它不能在现代编程环境中实现为一个普通的函数 *。它必须被视为具有类似函数接口的编译器特性。
传统的C编译器维护两个指针寄存器,一个“栈指针”和一个“帧指针”(或基址指针)。堆栈指针界定堆栈的当前范围。帧指针在进入函数时保存堆栈指针的值,并用于访问局部变量和在函数退出时恢复堆栈指针。
现在大多数编译器在正常函数中默认不使用帧指针。现代的调试/异常信息格式已经不需要它了,但是他们仍然理解它是什么,并且可以在需要的地方使用它。
特别是对于具有alloca或可变长度数组的函数,使用帧指针允许函数跟踪其堆栈帧的位置,同时动态修改堆栈指针以适应可变长度数组。
例如,我在O 1为arm构建了以下代码

#include <alloca.h>
int bar(void * baz);
void foo(int a) {
    bar(alloca(a));
}

我的评论(Comments Mine)

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.

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

  • 有了一个足够愚蠢/可预测的编译器,就有可能在汇编程序中实现alloca函数。具体来说,编译器需要。
  • 一致地为所有函数创建帧指针。
  • 始终使用帧指针而不是堆栈指针来引用局部变量。
  • 在为函数调用设置参数时,始终使用堆栈指针而不是帧指针。

这显然是它最初的实现方式(https://www.tuhs.org/cgi-bin/utree.pl?file=32V/usr/src/libc/sys/alloca.s)。
我想也有可能有一个实际的实现作为一个汇编函数,但有一个特殊的情况下,在编译器,使它进入愚蠢/可预测的模式时,它看到alloca,我不知道是否有任何编译器供应商这样做。

相关问题