提问人:glades 提问时间:10/1/2021 最后编辑:dbushglades 更新时间:6/10/2023 访问量:3724
alloca() 如何在内存级别工作?
How does alloca() work on a memory level?
问:
我试图弄清楚在内存层面上的实际工作方式。在 linux 手册页中:alloca()
alloca() 函数在堆栈中分配大小字节的空间 调用方的框架。此临时空间会自动释放 当调用 alloca() 的函数返回给其调用方时。
这是否意味着将按字节转发堆栈指针?或者新创建的内存究竟分配在哪里?alloca()
n
这不与可变长度数组完全相同吗?
我知道实现细节可能留给操作系统和其他东西。但我想知道这通常是如何实现的。
答:
是的,在功能上等同于局部可变长度数组,即:alloca
int arr[n];
还有这个:
int *arr = alloca(n * sizeof(int));
两者都为堆栈上的类型元素分配空间。在每种情况下,两者之间的唯一区别是 1) 一个是实际数组,另一个是指向数组第一个元素的指针,以及 2) 数组的生存期以其封闭范围结束,而内存的生存期在函数返回时结束。在这两种情况下,阵列都驻留在堆栈上。n
int
arr
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
这表明从返回的内存位于两个 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 现在是获取相同功能的标准化方式。
评论
alloca
alloca
另一个答案精确地描述了 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
}
评论
尽管从语法的角度来看,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 时,它进入了哑/可预测模式,我不知道是否有任何编译器供应商这样做。
评论
alloca
alloca 和 VLAs 之间最重要的区别是故障情况。以下代码:
int f(int n) {
int array[n];
return array == 0;
}
int g(int n) {
int *array = alloca(n);
return array == 0;
}
VLA 无法检测到分配失败;这是强加给语言结构的非常不 C 的东西。因此,Alloca() 的设计要好得多。
评论
man alloca
: 返回值 alloca() 函数返回指向已分配空间开头的指针。如果分配导致堆栈溢出,则程序行为未定义。
A pointer to the start of the allocated memory, or NULL if an error occurred (errno is set).
alloca()
int A[10000000];
malloc()
alloca()
评论
subq $xxx, %rsp
xxx
size_t n = some_func(); char *p = alloca(n+5);
call some_func ; addq $5, %rax ; subq %rax, %rsp
rax