提问人:Vaibhav 提问时间:6/20/2009 最后编辑:Matteo UgolottiVaibhav 更新时间:7/18/2023 访问量:190297
为什么使用 alloca() 不被认为是好的做法?
Why is the use of alloca() not considered good practice?
问:
alloca()
在堆栈上分配内存,而不是在堆上分配内存,如 的情况。因此,当我从例程中返回时,内存被释放了。所以,实际上这解决了我释放动态分配内存的问题。释放通过分配的内存是一个令人头疼的问题,如果以某种方式错过,会导致各种内存问题。malloc()
malloc()
尽管有上述功能,为什么不鼓励使用?alloca()
答:
答案就在页面中(至少在 Linux 上):man
返回值 alloca() 函数返回指向 分配的空间。如果 分配原因 堆栈溢出,程序行为未定义。
这并不是说它永远不应该被使用。我从事的一个 OSS 项目广泛使用它,只要你不滥用它(“巨大的价值”),它就可以了。一旦你超过了“几百字节”的标记,就该使用和朋友了。您可能仍然会遇到分配失败的情况,但至少您会有一些失败的迹象,而不仅仅是炸毁堆栈。alloca
malloc
评论
alloca()
alloca()
进程的可用堆栈空间量有限,远远小于 的可用内存量。malloc()
通过使用你,你会大大增加你遇到 Stack Overflow 错误的机会(如果你幸运的话,或者如果你不是,就会莫名其妙地崩溃)。alloca()
评论
正如此新闻组帖子中所指出的,有几个原因可以认为使用困难和危险:alloca
- 并非所有编译器都支持 .
alloca
- 某些编译器以不同的方式解释预期行为,因此即使在支持它的编译器之间也无法保证可移植性。
alloca
- 有些实现是有缺陷的。
评论
alloca()
ESP
EBP
alloca()
f(42, alloca(10), 43);
alloca()
所有其他答案都是正确的。但是,如果您想分配使用的东西相当小,我认为这是一个很好的技术,比使用或其他方式更快、更方便。alloca()
malloc()
换句话说,是危险的,并且可能导致溢出,就像现在一样。谨慎和合理,你会没事的。alloca( 0x00ffffff )
char hugeArray[ 0x00ffffff ];
一个问题是它不是标准的,尽管它得到了广泛的支持。在其他条件相同的情况下,我总是使用标准函数而不是通用编译器扩展。
如果你不能使用标准局部变量,alloca() 非常有用,因为它的大小需要在运行时确定,并且你绝对可以保证你从 alloca() 得到的指针在这个函数返回后永远不会被使用。
如果你
- 不要返回指针或包含指针的任何内容。
- 不要将指针存储在堆上分配的任何结构中
- 不要让任何其他线程使用指针
真正的危险来自其他人在以后的某个时候违反这些条件的可能性。考虑到这一点,它非常适合将缓冲区传递给将文本格式化的函数:)
评论
alloca()
f() { char *p; if (c) { /* compute x */ p = alloca(x); } else { p = 0; } /* use p */ }
alloca
原因如下:
char x;
char *y=malloc(1);
char *z=alloca(&x-y);
*z = 1;
并不是说任何人都会编写这段代码,但你传递给的大小参数几乎可以肯定来自某种输入,这可能会恶意地让你的程序达到这样的巨大目标。毕竟,如果大小不是基于输入的,或者不可能很大,为什么不直接声明一个小型的、固定大小的本地缓冲区呢?alloca
alloca
几乎所有使用 和/或 C99 vlas 的代码都存在严重的错误,这将导致崩溃(如果你幸运的话)或特权泄露(如果你不那么幸运的话)。alloca
评论
alloca
*0=9;
无效 C。至于测试你传递到的尺寸,测试它是什么?没有办法知道限制,如果你只是要针对一个微小的固定已知安全大小(例如 8k)进行测试,你不妨在堆栈上使用一个固定大小的数组。alloca
small_constant * log(user_input)
老问题,但没有人提到它应该被可变长度数组取代。
char arr[size];
而不是
char *arr=alloca(size);
它位于标准 C99 中,在许多编译器中作为编译器扩展存在。
评论
我遇到的最令人难忘的错误之一是与使用 .它表现为程序执行的随机点的堆栈溢出(因为它在堆栈上分配)。alloca
在头文件中:
void DoSomething() {
wchar_t* pStr = alloca(100);
//......
}
在实现文件中:
void Process() {
for (i = 0; i < 1000000; i++) {
DoSomething();
}
}
因此,编译器(Microsoft VC++ 6)内联函数和所有堆栈分配都发生在函数内部,从而炸毁了堆栈。在我的辩护中(我不是发现问题的人;当我无法修复它时,我不得不去向一位高级开发人员哭泣),它不是直的,它是 ATL 字符串转换宏之一。DoSomething
Process()
alloca
所以教训是 - 不要在你认为可能是内联的函数中使用。alloca
评论
一个比内核特别危险的地方 - 典型操作系统的内核有一个固定大小的堆栈空间,硬编码到其标头之一中;它不像应用程序的堆栈那样灵活。使用不必要的大小进行调用可能会导致内核崩溃。
某些编译器会警告在编译内核代码时应该打开的某些选项下使用(甚至 VLA)——在这里,最好在未由硬编码限制固定的堆中分配内存。alloca()
malloc()
alloca()
alloca()
评论
alloca()
并不比某个任意整数更危险。int foo[bar];
bar
每个人都已经指出了一件大事,即堆栈溢出的潜在未定义行为,但我应该提到,Windows 环境有一个很好的机制来使用结构化异常 (SEH) 和保护页面来捕获这种情况。由于堆栈仅根据需要增长,因此这些保护页驻留在未分配的区域中。如果向它们分配(通过溢出堆栈),则会引发异常。
您可以捕获此 SEH 异常并调用 _resetstkoflw 重置堆栈并继续您的快乐方式。它并不理想,但它是另一种机制,至少知道当东西撞到风扇时出了问题。*nix 可能有我不知道的类似内容。
我建议通过包装 alloca 并在内部跟踪它来限制您的最大分配大小。如果你真的对它很执着,你可以在函数的顶部抛出一些范围哨兵,以跟踪函数范围中的任何分配,并健全地检查它是否与你的项目允许的最大数量。
此外,除了不允许内存泄漏外,alloca 不会导致内存碎片,这非常重要。如果你聪明地使用它,我不认为 alloca 是不好的做法,这基本上适用于所有事情。:-)
评论
alloca()
alloca()
可悲的是,真正令人敬畏的 tcc 中缺少了真正令人敬畏的东西。GCC 确实有 .alloca()
alloca()
它播下了自己毁灭的种子。将 return 作为析构函数。
就像它在失败时返回一个无效的指针一样,这将在具有 MMU 的现代系统上发生段错误(并希望重新启动那些没有 MMU 的系统)。
malloc()
与自动变量不同,您可以在运行时指定大小。
它适用于递归。您可以使用静态变量来实现类似于尾递归的功能,并仅使用其他一些变量将信息传递给每次迭代。
如果你推得太深,你肯定会出现段错误(如果你有 MMU)。
请注意,当系统内存不足时,它不会提供更多功能,因为它会返回 NULL(如果分配,也会发生段错误)。也就是说,你所能做的就是保释,或者只是尝试以任何方式分配它。malloc()
要使用,我使用全局变量并将它们分配给 NULL。如果指针不是 NULL,我会在使用 .malloc()
malloc()
如果要复制任何现有数据,也可以将其用作一般情况。您需要在之前检查指针,以确定是否要在 .realloc()
realloc()
评论
仍然不鼓励使用 Alloca,为什么?
我看不出有这样的共识。很多强大的专业人士;一些缺点:
- C99 提供可变长度数组,通常优先使用,因为符号与固定长度数组更一致,整体更直观
- 许多系统可用于堆栈的总内存/地址空间比堆的内存/地址空间少,这使得程序更容易受到内存耗尽的影响(通过堆栈溢出):这可能被视为好事或坏事 - 堆栈不会像堆那样自动增长的原因之一是防止失控程序对整个机器产生如此多的不利影响
- 当在更局部的作用域(例如 OR 循环)或多个作用域中使用时,内存会在每次迭代/作用域中累积,并且在函数退出之前不会释放: 这与在控制结构的作用域中定义的正常变量(例如 将累积在 X 处请求的 -ed 内存,但固定大小数组的内存将在每次迭代中回收)。
while
for
for {int i = 0; i < 2; ++i) { X }
alloca
- 现代编译器通常不调用 的函数,但如果你强制调用它们,那么它将在调用者的上下文中发生(即,在调用者返回之前,堆栈不会被释放)
inline
alloca
alloca
- 很久以前从不可移植的功能/hack过渡到标准化扩展,但一些负面看法可能会持续存在
alloca
- 生存期与函数范围绑定,它可能比 的显式控制更适合程序员,也可能不适合 的显式控制
malloc
- 必须使用鼓励考虑释放 - 如果这是通过包装函数(例如)管理的,那么该函数为实现清理操作(如关闭文件描述符、释放内部指针或执行一些日志记录)提供了一个点,而无需对客户端代码进行显式更改:有时它是一个很好的模型,可以始终如一地采用:
malloc
WonderfulObject_DestructorFree(ptr)
- 在这种伪 OO 风格的编程中,很自然地想要类似的东西 - 当 “constructor” 是一个返回 -ed 内存的函数时,这是可能的(因为在函数返回要存储的值后内存仍然分配),但如果 “constructor” 使用
WonderfulObject* p = WonderfulObject_AllocConstructor();
malloc
p
alloca
- 宏版本可以实现这一点,但“宏是邪恶的”,因为它们可以相互冲突,并与非宏代码发生冲突,并产生意外的替换和随之而来的难以诊断的问题
WonderfulObject_AllocConstructor
- 宏版本可以实现这一点,但“宏是邪恶的”,因为它们可以相互冲突,并与非宏代码发生冲突,并产生意外的替换和随之而来的难以诊断的问题
- ValGrind、Purify 等可以检测到缺失的操作,但丢失的“析构函数”调用并不总是能被检测到——在执行预期用途方面,这是一个非常微不足道的好处;某些实现(例如 GCC)使用 for 的内联宏,因此无法像 // 那样在运行时替换内存使用诊断库(例如 electric fence)
free
alloca()
alloca()
malloc
realloc
free
- 在这种伪 OO 风格的编程中,很自然地想要类似的东西 - 当 “constructor” 是一个返回 -ed 内存的函数时,这是可能的(因为在函数返回要存储的值后内存仍然分配),但如果 “constructor” 使用
- 一些实现存在一些微妙的问题:例如,从 Linux 手册页:
在许多系统上,alloca() 不能在函数调用的参数列表中使用,因为 alloca() 保留的堆栈空间将出现在函数参数空间中间的堆栈上。
我知道这个问题被标记为 C,但作为一名 C++ 程序员,我想我会使用 C++ 来说明以下的潜在效用: 下面的代码(以及 ideone)创建一个向量跟踪不同大小的多态类型,这些多态类型是堆栈分配的(生命周期与函数返回相关)而不是堆分配。alloca
#include <alloca.h>
#include <iostream>
#include <vector>
struct Base
{
virtual ~Base() { }
virtual int to_int() const = 0;
};
struct Integer : Base
{
Integer(int n) : n_(n) { }
int to_int() const { return n_; }
int n_;
};
struct Double : Base
{
Double(double n) : n_(n) { }
int to_int() const { return -n_; }
double n_;
};
inline Base* factory(double d) __attribute__((always_inline));
inline Base* factory(double d)
{
if ((double)(int)d != d)
return new (alloca(sizeof(Double))) Double(d);
else
return new (alloca(sizeof(Integer))) Integer(d);
}
int main()
{
std::vector<Base*> numbers;
numbers.push_back(factory(29.3));
numbers.push_back(factory(29));
numbers.push_back(factory(7.1));
numbers.push_back(factory(2));
numbers.push_back(factory(231.0));
for (std::vector<Base*>::const_iterator i = numbers.begin();
i != numbers.end(); ++i)
{
std::cout << *i << ' ' << (*i)->to_int() << '\n';
(*i)->~Base(); // optionally / else Undefined Behaviour iff the
// program depends on side effects of destructor
}
}
评论
Alloca()很好,很高效......但它也被深深地打破了。
- 损坏的作用域行为(函数作用域而不是块作用域)
- 使用 inconsentant with malloc (alloca()-ted 指针不应该被释放,从此你必须跟踪你的指针来自哪里 free() 只有你用 malloc() 得到的指针)
- 使用内联时的不良行为(作用域有时会转到调用方函数,具体取决于被调用方是否内联)。
- 无堆栈边界检查
- 失败时的未定义行为(不会像 malloc 那样返回 NULL...失败意味着什么,因为它无论如何都不会检查堆栈边界......
- 非 ANSI 标准
在大多数情况下,您可以使用局部变量和主要大小来替换它。如果它用于大型对象,将它们放在堆上通常是一个更安全的想法。
如果你真的需要它 C,你可以使用 VLA(C++ 中没有 vla,太糟糕了)。它们在范围、行为和一致性方面比 alloca() 好得多。在我看来,VLA 是一种正确的 alloca()。
当然,使用所需空间的主要局部结构或数组仍然更好,如果您没有这样的主要堆分配,使用普通的 malloc() 可能是理智的。 我没有看到任何理智的用例,您真的真的需要 alloca() 或 VLA。
评论
alloca
alloca
malloc
free
{stack|heap}_alloc_{bytes,items,struct,varstruct}
{stack|heap}_dealloc
heap_dealloc
free
stack_dealloc
alloca
并不比可变长度数组 (VLA) 差,但比在堆上分配风险更大。
在 x86 上(最常见的是在 ARM 上),堆栈向下增长,这带来了一定的风险:如果您不小心写入了分配的块(例如,由于缓冲区溢出),那么您将覆盖函数的返回地址,因为该地址位于堆栈的“上方”, 即在您分配的块之后。alloca
这样做的后果是双重的:
程序将严重崩溃,并且无法判断崩溃的原因或位置(由于覆盖的帧指针,堆栈很可能会展开到随机地址)。
它使缓冲区溢出更加危险,因为恶意用户可以制作一个特殊的有效负载,该有效负载将被放置在堆栈上,因此最终可以被执行。
相反,如果你在堆上写超过一个块,你“只会”得到堆损坏。该程序可能会意外终止,但会正确展开堆栈,从而减少恶意代码执行的机会。
这个“老”问题有很多有趣的答案,甚至还有一些相对较新的答案,但我没有找到任何提到这一点的答案......
如果使用得当且小心谨慎,请始终如一地使用(可能在应用程序范围内)来处理小的可变长度分配 (或 C99 VLA,如果可用)可能会导致整体堆栈降低 与使用超大的其他等效实现相比,增长 固定长度的局部数组。因此,如果您谨慎使用,可能对您的堆栈有好处。
alloca()
alloca()
我在......好吧,我编造了这句话。但真的,想想看......
@j_random_hacker在其他答案下的评论非常正确: 避免使用过大的局部数组并不能使您的程序更安全,不会使堆栈溢出(除非您的编译器足够旧,允许内联使用您应该升级的函数,或者除非您使用内部循环, 在这种情况下,您应该...不使用内部循环)。alloca()
alloca()
alloca()
alloca()
我曾在桌面/服务器环境和嵌入式系统上工作过。许多嵌入式系统根本不使用堆(它们甚至没有链接支持堆),原因包括认为动态分配的内存是邪恶的,因为应用程序存在内存泄漏的风险,而应用程序一次从未重新启动多年,或者更合理的理由是动态内存是危险的,因为无法确定应用程序永远不会碎片其堆到虚假记忆耗尽的地步。因此,嵌入式程序员几乎没有其他选择。
alloca()
(或 VLA)可能正是完成这项工作的正确工具。
我一次又一次地看到,程序员使堆栈分配的缓冲区“足够大,可以处理任何可能的情况”。在深度嵌套的调用树中,重复使用它(反?模式导致堆栈使用过高。(想象一下,一个 20 级深的调用树,在每个级别上,由于不同的原因,该函数盲目地过度分配了 1024 个字节的缓冲区,“只是为了安全起见”,而通常它只会使用 16 个或更少的缓冲区,只有在极少数情况下才能使用更多。另一种方法是使用 或 VLA,并仅分配函数所需的堆栈空间,以避免给堆栈带来不必要的负担。希望当调用树中的一个函数需要比正常值更大的分配时,调用树中的其他函数仍在使用其正常的小分配,并且总体应用程序堆栈使用率明显低于每个函数盲目过度分配本地缓冲区的情况。alloca()
但是,如果您选择使用...alloca()
根据此页面上的其他答案,VLA 似乎应该是安全的(如果从循环中调用,它们不会复合堆栈分配),但如果您使用 ,请注意不要在循环中使用它,并确保您的函数不能内联,如果它有可能在另一个函数的循环中被调用。alloca()
评论
alloca()
malloc()
alloca()
我认为没有人提到过这一点:在函数中使用 alloca 会阻碍或禁用一些本来可以在函数中应用的优化,因为编译器无法知道函数堆栈帧的大小。
例如,C 编译器的一个常见优化是消除在函数中使用帧指针,而是相对于堆栈指针进行帧访问;因此,还有一个供一般使用的寄存器。但是如果在函数中调用 alloca,则部分函数的 sp 和 fp 之间的差异将是未知的,因此无法进行此优化。
鉴于它的使用稀有性,以及它作为标准函数的阴暗状态,编译器设计者很可能会禁用任何可能导致 alloca 出现问题的优化,如果让它与 alloca 一起工作需要付出更多的努力。
更新:由于可变长度的局部数组已被添加到 C 中,并且由于它们向编译器提出了与 alloca 非常相似的代码生成问题,因此我看到“使用稀有性和阴暗状态”不适用于底层机制;但我仍然怀疑使用 alloca 或 VLA 往往会损害使用它们的函数中的代码生成。我欢迎编译器设计者提供任何反馈。
评论
malloc
free
在我看来,alloca()(如果可用)应该只以受约束的方式使用。就像使用“goto”一样,相当多的理性人不仅对 alloca() 的使用,而且对 alloca() 的存在都深恶痛绝。
对于嵌入式使用,堆栈大小是已知的,并且可以通过约定和分析对分配的大小施加限制,并且编译器无法升级以支持 C99+,使用 alloca() 是可以的,而且我已经知道使用它。
如果可用,VLA 可能比 alloca() 具有一些优势:编译器可以生成堆栈限制检查,当使用数组样式访问时,该检查将捕获越界访问(我不知道是否有编译器这样做,但可以做到),并且对代码的分析可以确定数组访问表达式是否正确绑定。请注意,在某些编程环境中,例如汽车、医疗设备和航空电子设备,即使对于固定大小的阵列,也必须进行此分析,包括自动(在堆栈上)和静态分配(全局或本地)。
在堆栈上存储数据和返回地址/帧指针的架构上(据我所知,这就是全部),任何堆栈分配的变量都可能是危险的,因为变量的地址可能会被获取,未经检查的输入值可能会允许各种恶作剧。
在嵌入式空间中,可移植性不是一个问题,但是它是一个很好的论据,反对在严格控制的环境之外使用 alloca()。
在嵌入式空间之外,为了提高效率,我主要在日志记录和格式化函数中使用 alloca(),并且在非递归词法扫描器中使用,其中临时结构(使用 alloca() 分配)在标记化和分类期间创建,然后在函数返回之前填充持久对象(通过 malloc() 分配)。将 alloca() 用于较小的临时结构可大大减少分配持久性对象时的碎片。
实际上,alloca 并不能保证使用堆栈。 事实上,alloca 的 gcc-2.95 实现使用 malloc 本身从堆中分配内存。此外,该实现存在错误,如果您在进一步使用 goto 的块中调用它,可能会导致内存泄漏和一些意外行为。并不是说你永远不应该使用它,但有时 alloca 导致的开销比它离开的开销更多。
评论
alloca
longjmp
alloca
这里的大多数答案在很大程度上都忽略了这一点:使用可能比仅仅在堆栈中存储大型对象更糟糕是有原因的。_alloca()
自动存储和自动存储之间的主要区别在于后者存在一个额外的(严重)问题:分配的块不受编译器控制,因此编译器无法对其进行优化或回收。_alloca()
比较:
while (condition) {
char buffer[0x100]; // Chill.
/* ... */
}
跟:
while (condition) {
char* buffer = _alloca(0x100); // Bad!
/* ... */
}
后者的问题应该是显而易见的。
评论
alloca
alloca
一个陷阱是它倒带了它。alloca
longjmp
也就是说,如果用 保存一个上下文,然后是一些内存,然后保存到上下文中,你可能会丢失内存。堆栈指针已返回到原来的位置,因此不再保留内存;如果你调用一个函数或执行另一个函数,你将破坏原来的。setjmp
alloca
longjmp
alloca
alloca
alloca
澄清一下,我在这里特别指的是一种情况,即不会从发生函数的函数中返回!相反,函数使用 ;然后分配内存,最后向该上下文进行 longjmp。该函数的内存并未全部释放;只是自 .当然,我说的是观察到的行为;据我所知,没有这样的要求。longjmp
alloca
setjmp
alloca
alloca
setjmp
alloca
文档中的重点通常是内存与函数激活相关联的概念,而不是与任何块相关联;只是获取更多堆栈内存的多次调用,这些内存在函数终止时全部释放。并非如此;内存实际上与过程上下文相关联。当上下文被还原时,先前的状态也是如此。这是堆栈指针寄存器本身用于分配的结果,并且(必然)保存和恢复在 .alloca
alloca
longjmp
alloca
jmp_buf
顺便说一句,如果它以这种方式工作,则提供了一种合理的机制,用于故意释放分配了 .alloca
我遇到了这是错误的根本原因。
评论
longjmp
alloca
man alloca
给出了以下句子: “因为 alloca() 分配的空间是在堆栈帧中分配的,所以如果通过调用 longjmp(3) 或 siglongjmp(3) 跳过函数返回,则会自动释放该空间。因此,据记载,分配的内存在 .alloca
longjmp
longjmp
setjmp
alloca
longjmp
longjmp
alloca
setjmp
alloca
volatile
setjmp
alloca
longjmp
alloca
longjmp
setjmp
man alloca
记录了这种互动。如果我使用 ,我个人会依赖这种交互,因为它是记录的。您阅读了哪些 alloca 文档,而那里没有记录?alloca
longjmp
setjmp
longjmp
我不认为有人提到过这一点,但 alloca 也有一些严重的安全问题不一定存在于 malloc 中(尽管这些问题也出现在任何基于堆栈的数组中,无论是否动态)。由于内存是在堆栈上分配的,因此缓冲区溢出/下溢的后果比仅使用 malloc 要严重得多。
具体而言,函数的返回地址存储在堆栈中。如果此值损坏,则代码可能会转到内存的任何可执行区域。编译器不遗余力地使这变得困难(特别是通过随机化地址布局)。然而,这显然比堆栈溢出更糟糕,因为如果返回值损坏,最好的情况是 SEGFAULT,但它也可能开始执行随机内存片段,或者在最坏的情况下,会损害程序安全性的某个内存区域。
为什么没有人提到GNU文档引入的这个例子?
https://www.gnu.org/software/libc/manual/html_node/Advantages-of-Alloca.html
非本地退出自动完成(请参阅非本地退出) 释放他们通过 调用 的函数。这是使用
alloca
的最重要原因longjmp
alloca
alloca
建议阅读顺序:1->2->3->1
- https://www.gnu.org/software/libc/manual/html_node/Advantages-of-Alloca.html
- 非本地出口的介绍和详细信息
- Alloca 示例
IMO 分配和可变长度数组的最大风险是,如果分配大小出乎意料地大,它可能会以非常危险的方式失败。
堆栈上的分配通常不签入用户代码。
现代操作系统通常会在下面设置一个保护页面*,以检测堆栈溢出。当堆栈溢出时,内核可能会扩展堆栈或终止进程。Linux 在 2017 年扩展了这个保护区域,使其比页面大得多,但它的大小仍然有限。
因此,作为一项规则,在使用之前的分配之前,最好避免在堆栈上分配超过一页。使用 alloca 或可变长度数组,很容易最终允许攻击者在堆栈上进行任意大小的分配,从而跳过任何保护页面并访问任意内存。
* 在当今最广泛的系统上,堆栈向下增长。
评论
free
alloca
malloca
/freea
alloca