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

How does alloca() work on a memory level?

提问人:glades 提问时间:10/1/2021 最后编辑:dbushglades 更新时间:6/10/2023 访问量:3724

问:

我试图弄清楚在内存层面上的实际工作方式。在 linux 手册页中:alloca()

alloca() 函数在堆栈中分配大小字节的空间 调用方的框架。此临时空间会自动释放 当调用 alloca() 的函数返回给其调用方时。

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

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

我知道实现细节可能留给操作系统和其他东西。但我想知道这通常是如何实现的。

c 可变长度数组 堆栈帧 分配

评论

10赞 Eugene Sh. 10/1/2021
你的理解是相当准确的。
3赞 Jabberwocky 10/1/2021
大多数情况下,它完全按照 linux 手册页的描述完成,是的,在这种情况下,堆栈指针减少了 n 个字节(或者由于各种原因(如内存对齐等)可能略多于 n)。是的,当您使用 VLA 时,或多或少会发生同样的事情
1赞 tstanisl 10/1/2021
@Jabberwocky请使用“自动 VLA”术语
1赞 torez233 12/5/2022
@NateEldredge一个愚蠢的问题:如果在编译时不知道确切的大小,汇编程序如何生成代码,例如为编译过程中需要确定的堆栈帧腾出空间?换句话说,编译器不知道要向下移动堆栈指针多少subq $xxx, %rspxxx
1赞 Nate Eldredge 12/5/2022
@torez233:然后它不使用即时指令,而是减去以其他方式计算的值,很可能是在寄存器中。例如,如果有,则编译器(而不是汇编器)会发出类似 .(中间会有更多的代码来四舍五入到 16 左右的倍数。size_t n = some_func(); char *p = alloca(n+5);call some_func ; addq $5, %rax ; subq %rax, %rsprax

答:

30赞 dbush 10/1/2021 #1

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

int arr[n];

还有这个:

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

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

例如,给定以下代码:

#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

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

VLA 首次出现在 C99 的 C 标准中,但在此之前就已经存在了。Linux 手册页指出:alloca

符合

此函数在 POSIX.1-2001 中没有。

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

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

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

评论

4赞 dbush 10/1/2021
不使用的原因是因为它是非标准的,而 VLA 是。alloca
3赞 UnholySheep 10/1/2021
VLA 不需要受 C11 和更新标准的支持(例如:MSVC 不支持它们)
7赞 tstanisl 10/1/2021
@UnholySheep,是的,但这个可选功能是完全失败的。支持 VLA 的编译器仍然支持它,那些不支持的编译器仍然不支持它,并且符合 C 标准的价值只会被稀释。
4赞 tstanisl 10/2/2021
Alloca 在循环中的行为非常不同,它很容易耗尽堆栈。这是因为使用 alloca 获取的对象的生存期在函数返回时结束。而 VLA 的生存期在其包含块结束时结束。因此,VLA 要安全得多
3赞 zwol 10/2/2021
@tstanisl 在某些情况下,在函数返回之前的生存是首选 VLA 的原因,例如,如果您需要有条件地分配一些暂存空间。alloca
17赞 tstanisl 10/2/2021 #2

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

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

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

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
}

评论

0赞 plugwash 10/2/2021
这让我想知道如果你混合 alloca 和 VLA 会发生什么......
0赞 Jaime Guerrero 10/2/2021
我不确定“a 是否仍然有效”是否有效:-)a 没有用,因为您既不能(应该?)读取也不能写入它的值,因为该内存在当前堆栈“尺寸”/“大小”之外,并且可能会被下一个函数调用所破坏。一个体面的 CPU/OS 不会(应该?)允许访问“超出范围”的堆栈内存。
0赞 Jaime Guerrero 10/2/2021
“泄漏”有点夸张。不是像未释放的马洛克那样真正的泄漏;因为假设您没有耗尽堆栈和故障,而是继续执行,那么在下一次函数调用或返回时,堆栈指针将被重置,后续的函数调用、变量或 alloca() 将重用“泄漏”的内存。换句话说,它会自动“释放”,因为它位于堆栈而不是堆上。
0赞 plugwash 10/2/2021
至少在 linux 上,alloca 的文档特别指出,当函数调用返回时,而不是在退出块时,它会被释放。
0赞 tstanisl 10/2/2021
@plugwash这正是我在答案中写的
5赞 plugwash 10/2/2021 #3

尽管从语法的角度来看,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 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 时,它进入了哑/可预测模式,我不知道是否有任何编译器供应商这样做。

评论

1赞 Ruslan 10/2/2021
“它不能作为普通函数实现”——并非总是如此:请参阅此处的反例。
0赞 Dan Lenski 5/14/2023
这是最好的答案,因为您对如何在其起源的平台上使用标准 C ABI 实际实现为“正常”函数提供了一些很好的见解......这给了我们一个强烈的提示,为什么它以这种形式添加。alloca
-3赞 mevets 10/2/2021 #4

allocaVLAs 之间最重要的区别是故障情况。以下代码:

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

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

评论

4赞 EOF 10/3/2021
man alloca: 返回值 alloca() 函数返回指向已分配空间开头的指针。如果分配导致堆栈溢出,则程序行为未定义。
0赞 mevets 10/3/2021
我说了一些不同的东西: 但也许这就是 RTOS 收敛于 Dinkum 库而不是 gnu 的部分原因。A pointer to the start of the allocated memory, or NULL if an error occurred (errno is set).
1赞 EOF 10/3/2021
或者也许不是“设计得更好”,而是根本没有设计得很好(而且规定得很差)?alloca()
0赞 mevets 10/3/2021
嗯,不。VLA 没有提供错误恢复的机会;alloca() 可以。差不多是灌篮高手。当然,alloca 的一些玩具实现已经过时了,但这并不妨碍良好的实现。与 VLA 不同,VLA 几乎是标准机构通过弃用它来表达的。
0赞 tstanisl 10/3/2021
VLA 没有提供恢复的机会,就像没有一样。C标准未定义任何自动对象分配的资源。如果要使VLA具有动态存储功能,只需使用指向VLA和的指针,甚至“安全”即可。最后。VLA 被弃用。它们是可选的,与复数、原子、线程、宽字符相同。请更新您的答案,它仅适用于非常特定的 RTOS。int A[10000000];malloc()alloca()