alloca() 如何在内存级别上工作?
Posted
技术标签:
【中文标题】alloca() 如何在内存级别上工作?【英文标题】:How does alloca() work on a memory level? 【发布时间】:2021-11-23 03:58:54 【问题描述】:我试图弄清楚alloca()
在内存级别上的实际工作方式。来自linux man page:
alloca() 函数在栈中分配 size 个字节的空间 调用者的框架。这个临时空间会自动释放 当调用 alloca() 的函数返回给它的调用者时。
这是否意味着alloca()
将堆栈指针转发n
字节?或者新创建的内存具体分配在哪里?
这不是和variable length arrays一模一样吗?
我知道实现细节可能留给操作系统和其他东西。但我想知道一般这是如何实现的。
【问题讨论】:
你的理解很准确。 大多数情况下,它完全按照 linux 手册页的描述完成,是的,在这种情况下,堆栈指针减少了 n 个字节(或者由于各种原因可能比 n 多一点像内存对齐等)。是的,当你使用 VLA 时,或多或少会发生同样的事情 @Jabberwocky 请使用“自动 VLA” 术语 如果有人愿意,可能值得展开更详细的解释来说明它是如何实现的(我不确定我是否会解释得很好)。在快速测试中,看起来 gcc 内联了alloca()
的效果,这是有道理的——编译器必须知道堆栈帧已经改变——但它似乎使用了一些与线程本地存储相同的机制,例如使用%fs
寄存器。
@sj95126:您看到的%fs
很可能是stack canary;金丝雀值保存在线程本地存储中。它与 alloca 本身并没有真正的关系,所以-fno-stack-protector
可能会清理一下。
【参考方案1】:
是的,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 现在是获得相同功能的标准化方法。
【讨论】:
不使用alloca
的原因是因为它是非标准的,而VLA是。
VLA 不需要被 C11 和更新的标准支持(例如:它们不受 MSVC 支持)
@UnholySheep,是的,但是这个可选功能完全失败了。支持 VLA 的编译器仍然支持它,那些不支持的编译器仍然不支持,而且符合 C 标准的价值只是被稀释了。
Alloca 在循环中的行为非常不同,它很容易耗尽堆栈。这是因为使用 alloca 获取的对象的生命周期在函数返回时结束。而 VLA 的生命周期在其包含块结束时结束。所以 VLA 更安全
@tstanisl 在某些情况下,在函数返回之前生存是首选 alloca
而不是 VLA 的原因,例如,如果您需要有条件地分配一些暂存空间。
【参考方案2】:
另一个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
对
for (...)
int n = 1000;
char x[n];
// x is released
【讨论】:
这让我想知道如果你混合使用 alloca 和 VLA 会发生什么...... 我不确定“a 仍然有效”是否有效 :-) a 没有用,因为您可以(应该?)既不读取也不写入它的值,因为该内存在当前内存之外堆栈“尺寸”/“大小”,并受到下一个函数调用的破坏。一个体面的 CPU/OS 将(应该?)不允许访问“超出范围”的堆栈内存。 “泄漏”有点夸张。不像未释放的 malloc 那样真正的泄漏;因为假设您没有耗尽堆栈和故障而是继续执行,在下一次函数调用或返回时,堆栈指针被重置,后续函数调用、变量或 alloca() 将重用“泄漏”的内存。换句话说,它是在堆栈而不是堆上自动“释放”的。 至少在 linux 上的 alloca 文档特别说明它在函数调用返回时被释放,而不是在您退出块时。 @plugwash 这正是我在答案中写的【参考方案3】:虽然从语法的角度来看,alloca 看起来像一个函数,但它不能在现代编程环境中作为普通函数实现*。它必须被视为具有类函数接口的编译器特性。
传统的 C 编译器维护两个指针寄存器,一个“堆栈指针”和一个“帧指针”(或基指针)。堆栈指针界定堆栈的当前范围。帧指针保存了函数入口时堆栈指针的值,用于访问局部变量并在函数退出时恢复堆栈指针。
现在大多数编译器在普通函数中默认不使用帧指针。现代调试/异常信息格式已经使它变得不那么简单,但他们仍然了解它是什么并且可以在需要的地方使用它。
特别是对于具有 alloca 或可变长度数组的函数,使用帧指针允许函数跟踪其堆栈帧的位置,同时动态修改堆栈指针以适应可变长度数组。
例如,我在 O1 为 arm 构建了以下代码
#include <alloca.h>
int bar(void * baz);
void foo(int a)
bar(alloca(a));
得到(我的cmets)
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 时,它会进入哑/可预测模式,我不知道是否有任何编译器供应商做到了。
【讨论】:
“它不能作为普通函数实现” — 并非总是如此:反例请参见 this。【参考方案4】:alloca
分配的内存在调用alloca
的函数返回时自动释放。也就是说,使用alloca
分配的内存是特定函数的“堆栈帧”或上下文的本地内存。
alloca
不能移植,并且很难在没有传统堆栈的机器上实现。当它的返回值被直接传递给另一个函数时,它的使用是有问题的(并且在基于堆栈的机器上的明显实现失败),如
fgets(alloca(100), 100, stdin)
如果您在不符合此描述的任何地方使用它,您就是在自找麻烦。如果您在这些地方使用alloca()
,您可能会遇到麻烦,因为在调用alloca()
时堆栈上可能有一些东西:
if ((pointer_variable = alloca(sizeof(struct something))) == NULL)
....
而且我希望有人会打电话给我,即使对于某些编译器生成的代码来说,这种高度限制性的限制还不够保守。现在,如果它是作为内置编译器完成的,您可能会设法解决这些问题。
一旦我终于弄明白了alloca()
函数,它就运行得相当好——我记得,它的主要用途是在Bison parser
中。每次调用浪费的 128 个字节加上固定的堆栈大小可能会令人讨厌。为什么我不直接使用GCC
?因为这是尝试将最初使用交叉编译器的GCC
移植到一台机器上,结果证明它几乎没有足够的内存来本地编译 GCC(1.35 左右)。当GCC 2
出现时,事实证明它的内存足够了,原生编译自己是不可能的。
【讨论】:
放三样东西会给你带来好处。 嘿亲爱的,如果我的回答不被接受或对所提出的问题没有帮助,你为什么要浪费你的时间。 如果对 alloca 的调用尝试在同一堆栈上为其他函数调用(在本例中为 fgets)的参数列表的准备过程中的堆栈分配一些内存,参数列表很可能会受到干扰。【参考方案5】:alloca 和 VLAs 之间最重要的区别是失败案例。以下代码:
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() 的设计要好得多。
【讨论】:
man alloca
: 返回值 alloca() 函数返回一个指向已分配空间开头的指针。如果分配导致堆栈溢出,则程序行为未定义。
我的说法不一样:A pointer to the start of the allocated memory, or NULL if an error occurred (errno is set).
但也许这就是 RTOS 集中在 Dinkum 库而不是 gnu 上的部分原因。
或者alloca()
不是“设计得更好”,而是根本没有很好的设计(并且指定得很差)?
好吧,不。 VLA 没有提供错误恢复的机会; alloca() 可以。几乎是灌篮高手。当然,alloca 的一些玩具实现已经流行起来,但这并不妨碍良好的实现。与 VLA 不同,VLA 几乎就是标准的 du-jour 机构所说的弃用它。
VLA 不会像int A[10000000];
那样提供恢复机会。 C 标准未定义任何自动对象分配的资源不足。如果您希望 VLA 具有动态存储,只需使用指向 VLA 和malloc()
的指针,甚至“安全”alloca()
。最后。 VLA没有被弃用。它们是可选的,与复数、原子、线程、宽字符相同。请更新您的答案,即它仅适用于非常特定的 RTOS。以上是关于alloca() 如何在内存级别上工作?的主要内容,如果未能解决你的问题,请参考以下文章