456

alloca()在堆栈上而不是在堆上分配内存,如malloc(). 因此,当我从例程返回时,内存被释放。所以,实际上这解决了我释放动态分配内存的问题。释放通过分配的内存malloc()是一个令人头疼的问题,如果以某种方式错过会导致各种内存问题。

alloca()尽管有上述功能,为什么不鼓励使用?

4

24 回答 24

292

答案就在man页面中(至少在Linux上):

返回值 alloca() 函数返回一个指向已分配空间开头的指针。如果分配导致堆栈溢出,则程序行为未定义。

这并不是说它永远不应该被使用。我从事的 OSS 项目之一广泛使用它,只要您不滥用它(alloca“巨大的价值”),就可以了。一旦超过“几百字节”标记,就该使用malloc和朋友了。你可能仍然会遇到分配失败,但至少你会有一些失败的迹象,而不是仅仅吹出堆栈。

于 2009-06-19T16:27:17.543 回答
238

我遇到的最令人难忘的错误之一是与使用alloca. 它在程序执行的随机点表现为堆栈溢出(因为它在堆栈上分配)。

在头文件中:

void DoSomething() {
   wchar_t* pStr = alloca(100);
   //......
}

在实现文件中:

void Process() {
   for (i = 0; i < 1000000; i++) {
     DoSomething();
   }
}

所以发生的事情是编译器内联DoSomething函数,所有的堆栈分配都发生在Process()函数内部,从而炸毁了堆栈。在我的辩护中(我不是发现问题的人;当我无法修复它时,我不得不去向一位高级开发人员哭泣),它不是直截了当alloca的,它是 ATL 字符串转换之一宏。

所以教训是——不要alloca在你认为可能被内联的函数中使用。

于 2010-08-04T23:35:07.060 回答
97

老问题,但没有人提到它应该被可变长度数组取代。

char arr[size];

代替

char *arr=alloca(size);

它在标准 C99 中并且作为编译器扩展存在于许多编译器中。

于 2010-08-01T20:45:08.660 回答
71

如果您不能使用标准局部变量,alloca() 非常有用,因为它的大小需要在运行时确定,并且您可以 绝对保证从 alloca() 获得的指针在此函数返回后永远不会被使用

你可以相当安全,如果你

  • 不要返回指针或任何包含它的东西。
  • 不要将指针存储在堆上分配的任何结构中
  • 不要让任何其他线程使用指针

真正的危险来自其他人稍后会违反这些条件的机会。考虑到这一点,将缓冲区传递给将文本格式化到其中的函数非常有用:)

于 2009-06-22T23:46:08.043 回答
45

正如此新闻组帖子中所述,使用alloca被认为是困难和危险的原因有几个:

  • 并非所有编译器都支持alloca.
  • 一些编译器对预期行为的解释alloca不同,因此即使在支持它的编译器之间也不能保证可移植性。
  • 一些实现是错误的。
于 2009-06-19T16:28:33.347 回答
31

一个问题是它不是标准的,尽管它得到了广泛的支持。在其他条件相同的情况下,我总是使用标准函数而不是通用编译器扩展。

于 2009-06-19T16:35:44.020 回答
25

仍然不鼓励使用alloca,为什么?

我不认为有这样的共识。许多强大的专业人士;一些缺点:

  • C99提供了变长数组,通常会优先使用,因为记法更符合定长数组,整体直观
  • 许多系统可用于堆栈的整体内存/地址空间比堆可用的总内存/地址空间少,这使得程序更容易受到内存耗尽(通过堆栈溢出)的影响:这可能被视为一件好事或坏事 - 一个堆栈不会像堆那样自动增长的原因之一是为了防止失控的程序对整个机器产生同样多的不利影响
  • 当在更局部的作用域(例如一个whilefor循环)或多个作用域中使用时,内存在每次迭代/作用域中累积,并且在函数退出之前不会释放:这与在控制结构的作用域中定义的普通变量(例如for {int i = 0; i < 2; ++i) { X }将在 X 处累积alloca请求的内存,但每次迭代都会回收固定大小数组的内存)。
  • 现代编译器通常不会inline调用函数alloca,但如果你强制它们,那么alloca它将发生在调用者的上下文中(即堆栈在调用者返回之前不会被释放)
  • 很久以前alloca从一个不可移植的特性/hack 过渡到一个标准化的扩展,但是一些负面的看法可能会持续存在
  • 生命周期绑定到函数范围,这可能比malloc显式控制更适合程序员
  • 必须使用malloc鼓励考虑释放 - 如果这是通过包装函数(例如WonderfulObject_DestructorFree(ptr))管理的,那么该函数为实现清理操作(如关闭文件描述符、释放内部指针或进行一些日志记录)提供了一个点,而无需显式更改客户端代码:有时它是一个很好的模型,可以始终如一地采用
    • 在这种伪 OO 风格的编程中,很自然地想要类似WonderfulObject* p = WonderfulObject_AllocConstructor();的东西 - 当“构造函数”是一个返回malloc-ed 内存的函数时,这是可能的(因为在函数返回要存储的值之后内存保持分配状态p),但不是如果“构造函数”使用alloca
      • 的宏版本WonderfulObject_AllocConstructor可以实现这一点,但“宏是邪恶的”,因为它们可能会与非宏代码和非宏代码发生冲突,并产生意外替换和随之而来的难以诊断的问题
    • ValGrind、Purify 等可以检测到丢失free的操作,但根本无法检测到丢失的“析构函数”调用——在强制执行预期用途方面,这是一个非常微不足道的好处;一些alloca()实现(例如 GCC 的)使用内联宏 for alloca(),因此内存使用诊断库的运行时替换是不可能的malloc// reallocfree例如电栅栏)
  • 一些实现有一些微妙的问题:例如,来自 Linux 联机帮助页:

    在许多系统上 alloca() 不能在函数调用的参数列表中使用,因为 alloca() 保留的堆栈空间将出现在函数参数空间中间的堆栈上。


我知道这个问题被标记为 C,但作为 C++ 程序员,我想我会使用 C++ 来说明以下的潜在效用alloca:下面的代码(以及在 ideone的代码)创建了一个向量跟踪堆栈分配的不同大小的多态类型(使用生命周期与函数返回相关)而不是堆分配。

#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
    }
}
于 2013-02-26T10:31:06.390 回答
18

所有其他答案都是正确的。但是,如果您要分配的东西alloca()相当小,我认为这是一种比使用malloc()或其他方式更快、更方便的好技术。

换句话说,alloca( 0x00ffffff )它是危险的并且可能导致溢出,就像是一样多char hugeArray[ 0x00ffffff ];。谨慎和合理,你会没事的。

于 2009-06-19T16:32:27.013 回答
16

这个“老”问题有很多有趣的答案,甚至是一些相对较新的答案,但我没有找到任何提到这个的答案......

如果使用得当且小心谨慎,始终如一地使用alloca() (可能是应用程序范围内的)来处理小的可变长度分配(或 C99 VLA,如果可用)可能会导致整体堆栈增长低于使用固定长度的超大局部数组的其他等效实现. 所以如果你小心使用它alloca()可能对你的堆栈有好处。

我在……中找到了那句话。好吧,我做了那句话。但真的,仔细想想......

@j_random_hacker 在其他答案下的评论中非常正确:避免使用有alloca()利于过大的本地数组不会使您的程序免受堆栈溢出的影响(除非您的编译器足够老以允许内联alloca()在这种情况下使用的函数,您应该升级,或者除非你使用alloca()内部循环,在这种情况下你应该......不要使用alloca()内部循环)。

我曾在桌面/服务器环境和嵌入式系统上工作过。许多嵌入式系统根本不使用堆(它们甚至不链接以支持它),原因包括认为动态分配的内存是邪恶的,因为应用程序存在内存泄漏的风险,永远不会一次重启多年,或者更合理的理由是动态内存是危险的,因为无法确定应用程序永远不会将其堆碎片化到虚假内存耗尽的程度。所以嵌入式程序员几乎没有选择。

alloca()(或 VLA)可能只是适合这项工作的工具。

我已经一次又一次地看到程序员使堆栈分配的缓冲区“大到足以处理任何可能的情况”。在深度嵌套的调用树中,重复使用该(反?)模式会导致过度使用堆栈。(想象一个 20 层深的调用树,在每一层由于不同的原因,函数盲目地过度分配 1024 字节的缓冲区“只是为了安全”,而通常它只会使用 16 个或更少的缓冲区,并且仅在非常极少数情况下可能会使用更多。)另一种方法是使用alloca()或 VLA 并仅分配函数所需的堆栈空间,以避免不必要的堆栈负担。希望当调用树中的一个函数需要大于正常的分配时,调用树中的其他函数仍在使用它们正常的小分配,并且总体应用程序堆栈使用量明显少于每个函数盲目过度分配本地缓冲区的情况.

但是,如果您选择使用alloca()...

根据此页面上的其他答案,似乎 VLA 应该是安全的(如果从循环内调用它们不会复合堆栈分配),但如果您使用alloca(),请注意不要在循环内使用它,并制作如果有可能在另一个函数的循环中调用它,请确保您的函数不能被内联。

于 2016-03-31T22:00:52.420 回答
14

我认为没有人提到这一点:在函数中使用 alloca 会阻碍或禁用一些原本可以应用于函数的优化,因为编译器无法知道函数堆栈帧的大小。

例如,C 编译器的一个常见优化是消除函数中帧指针的使用,而是相对于堆栈指针进行帧访问;所以还有一个通用寄存器。但是如果在函数内部调用alloca,sp和fp的区别对于部分函数是未知的,所以这个优化是做不到的。

考虑到它的稀有性,以及它作为标准函数的可疑状态,编译器设计者很可能会禁用任何可能导致 alloca 问题的优化,如果需要付出更多努力才能使其与 alloca 一起使用。

更新: 由于可变长度的局部数组已添加到 C 中,并且由于这些向编译器提出了与 alloca 非常相似的代码生成问题,我看到“使用稀有和可疑状态”不适用于底层机制;但我仍然怀疑使用 alloca 或 VLA 往往会损害使用它们的函数中的代码生成。我欢迎编译器设计人员提供任何反馈。

于 2016-11-04T16:00:30.210 回答
13

每个人都已经指出了堆栈溢出的潜在未定义行为,但我应该提到,Windows 环境有一个很好的机制来使用结构化异常 (SEH) 和保护页来捕获这一点。由于堆栈只根据需要增长,因此这些保护页驻留在未分配的区域中。如果您分配给它们(通过溢出堆栈),则会引发异常。

您可以捕获此 SEH 异常并调用 _resetstkoflw 来重置堆栈并继续您的快乐方式。它并不理想,但它是另一种机制,至少可以知道当东西撞到风扇时出现了问题。*nix 可能有类似的东西,我不知道。

我建议通过包装 alloca 并在内部跟踪它来限制您的最大分配大小。如果你真的很顽固,你可以在你的函数顶部放置一些范围哨兵来跟踪函数范围内的任何 alloca 分配,并根据项目允许的最大数量检查它。

此外,除了不允许内存泄漏之外,alloca 不会导致内存碎片,这非常重要。如果你聪明地使用它,我不认为alloca是不好的做法,这基本上适用于所有事情。:-)

于 2011-03-21T16:19:04.990 回答
11

一个陷阱alloca是它longjmp倒带。

也就是说,如果你用 保存一个上下文setjmp,那么alloca一些内存,然后longjmp到上下文,你可能会丢失alloca内存。堆栈指针回到原来的位置,因此不再保留内存;如果你调用一个函数或做另一个alloca,你会破坏原来的alloca

澄清一下,我在这里具体指的是一种情况,即longjmp不会从发生的函数中返回alloca!相反,函数使用setjmp;保存上下文。然后分配内存,alloca最后在该上下文中发生 longjmp。该函数的alloca内存并未全部释放;只是它自setjmp. 当然,我说的是观察到的行为;alloca据我所知,没有记录任何此类要求。

文档中的重点通常是alloca内存与功能激活相关联的概念,而不是与任何块相关联;多次调用alloca只是获取更多的堆栈内存,这些内存在函数终止时全部释放。不是这样;内存实际上与过程上下文相关联。当使用 恢复上下文时longjmp,先前的alloca状态也是如此。这是堆栈指针寄存器本身用于分配的结果,并且(必然)在jmp_buf.

顺便说一句,如果它以这种方式工作,它提供了一种合理的机制来故意释放分配给alloca.

我已经将此作为错误的根本原因。

于 2018-02-26T04:12:23.547 回答
10

Here's why:

char x;
char *y=malloc(1);
char *z=alloca(&x-y);
*z = 1;

Not that anyone would write this code, but the size argument you're passing to alloca almost certainly comes from some sort of input, which could maliciously aim to get your program to alloca something huge like that. After all, if the size isn't based on input or doesn't have the possibility to be large, why didn't you just declare a small, fixed-size local buffer?

Virtually all code using alloca and/or C99 vlas has serious bugs which will lead to crashes (if you're lucky) or privilege compromise (if you're not so lucky).

于 2010-08-01T20:32:39.617 回答
10

alloca()很好而且很高效......但它也被深深地破坏了。

  • 破坏范围行为(函数范围而不是块范围)
  • 使用与 malloc 不一致(alloca() -ted 指针不应被释放,因此您必须跟踪指针从哪里指向free()只有那些您使用malloc()获得的指针)
  • 当您还使用内联时的不良行为(范围有时会转到调用者函数,具体取决于被调用者是否内联)。
  • 没有堆栈边界检查
  • 失败时未定义的行为(不会像 malloc 那样返回 NULL ......失败意味着什么,因为它无论如何都不检查堆栈边界......)
  • 不是ANSI标准

在大多数情况下,您可以使用局部变量和主要大小来替换它。如果它用于大型对象,将它们放在堆上通常是一个更安全的想法。

如果你真的需要它 C,你可以使用 VLA(C++ 中没有 vla,太糟糕了)。在作用域行为和一致性方面,它们比 alloca() 好得多。在我看来,VLA是一种正确的alloca()

当然,使用所需空间的主要部分的本地结构或数组仍然更好,如果您没有这样的主要堆分配,使用普通 malloc() 可能是理智的。我没有看到真正需要alloca()VLA 的合理用例。

于 2014-09-02T10:26:20.137 回答
7

alloca()一个比内核特别危险的地方malloc()——典型操作系统的内核有一个固定大小的堆栈空间,硬编码到它的一个标题中;它不像应用程序的堆栈那样灵活。以不合理的大小进行调用alloca()可能会导致内核崩溃。某些编译器会警告alloca()在编译内核代码时应该打开的某些选项下(甚至是 VLA)的使用 - 在这里,最好在未通过硬编码限制固定的堆中分配内存。

于 2010-11-27T18:49:15.397 回答
6

进程只有有限的可用堆栈空间——远远少于可用的内存量malloc()

通过使用alloca()你,大大增加了发生 Stack Overflow 错误的机会(如果你幸运的话,或者如果你不幸运的话,则会出现莫名其妙的崩溃)。

于 2009-06-19T16:27:46.547 回答
6

如果您不小心写入了分配的块之外alloca(例如由于缓冲区溢出),那么您将覆盖函数的返回地址,因为该地址位于堆栈的“上方”,即您分配的块之后。

堆栈上的 _alloca 块

这样做的后果是双重的:

  1. 程序将严重崩溃,并且无法说明崩溃的原因或位置(由于被覆盖的帧指针,堆栈很可能会展开到随机地址)。

  2. 它使缓冲区溢出更加危险,因为恶意用户可以制作一个特殊的有效负载,该有效负载将被放入堆栈并因此最终被执行。

相反,如果你写超出堆上的一个块,你“只是”得到堆损坏。程序可能会意外终止,但会正确展开堆栈,从而减少恶意代码执行的机会。

于 2016-02-17T10:43:45.337 回答
5

可悲的alloca()是,几乎令人敬畏的 tcc 缺少真正令人敬畏的东西。Gcc 确实有alloca().

  1. 它播下了自己毁灭的种子。以 return 作为析构函数。

  2. 就像malloc()它在失败时返回一个无效指针,这将在具有 MMU 的现代系统上出现段错误(并希望重新启动那些没有)。

  3. 与自动变量不同,您可以在运行时指定大小。

它适用于递归。您可以使用静态变量来实现类似于尾递归的功能,并仅使用其他几个变量将信息传递给每次迭代。

如果你推得太深,你肯定会出现段错误(如果你有 MMU)。

请注意,malloc()当系统内存不足时,它会返回 NULL(如果分配也会出现段错误),因此不会提供更多功能。即你所能做的就是保释或尝试以任何方式分配它。

要使用malloc()我使用全局变量并将它们分配为 NULL。如果指针不为 NULL,我在使用之前释放它malloc()

realloc()如果要复制任何现有数据,您也可以将其用作一般情况。如果要在realloc().

3.2.5.2 alloca的优势

于 2011-03-30T06:58:49.993 回答
3

实际上,alloca 并不能保证使用堆栈。实际上,alloca 的 gcc-2.95 实现使用 malloc 本身从堆中分配内存。此外,该实现是错误的,如果您在块内调用它并进一步使用 goto,它可能会导致内存泄漏和一些意外行为。不是,说你永远不应该使用它,但有时 alloca 导致的开销比它释放的开销更大。

于 2017-01-30T18:14:22.037 回答
2

在我看来,alloca(),如果可用,应该只以一种受约束的方式使用。非常像“goto”的使用,相当多的其他合理的人不仅对alloca()的使用,而且对alloca()的存在都有强烈的反感。

对于嵌入式使用,堆栈大小已知并且可以通过约定和分析对分配大小施加限制,并且编译器无法升级以支持 C99+,使用 alloca() 很好,我一直知道使用它。

如果可用,VLA 可能比 alloca() 有一些优势:编译器可以生成堆栈限制检查,当使用数组样式访问时,这些检查将捕获越界访问(我不知道是否有任何编译器这样做,但它可以完成),并且分析代码可以确定数组访问表达式是否正确有界。请注意,在某些编程环境中,例如汽车、医疗设备和航空电子设备,即使对于固定大小的数组,也必须进行这种分析,包括自动(在堆栈上)和静态分配(全局或本地)。

在将数据和返回地址/帧指针存储在堆栈上的架构上(据我所知,这就是全部),任何堆栈分配的变量都可能是危险的,因为可以获取变量的地址,并且未经检查的输入值可能允许各种恶作剧。

可移植性在嵌入式空间中不太受关注,但是它是反对在仔细控制的环境之外使用 alloca() 的一个很好的论据。

在嵌入空间之外,我主要在日志记录和格式化函数中使用 alloca() 以提高效率,并在非递归词法扫描器中使用临时结构(使用 alloca() 分配)在标记化和分类期间创建,然后是持久的对象(通过 malloc() 分配)在函数返回之前被填充。对于较小的临时结构使用 alloca() 可以大大减少分配持久对象时的碎片。

于 2016-12-13T14:36:17.440 回答
1

为什么没有人提到 GNU 文档介绍的这个例子?

https://www.gnu.org/software/libc/manual/html_node/Advantages-of-Alloca.html

完成的非本地退出longjmp(请参阅非本地退出alloca)在通过调用的函数退出时自动释放分配的空间alloca这是使用的最重要原因 alloca

建议阅读顺序1->2->3->1

  1. https://www.gnu.org/software/libc/manual/html_node/Advantages-of-Alloca.html
  2. 非本地出口的介绍详细信息
  3. Alloca 示例
于 2021-08-05T17:08:22.937 回答
1

我认为没有人提到过这一点,但是 alloca 也有一些严重的安全问题,malloc 不一定存在(尽管这些问题也出现在任何基于堆栈的数组中,无论是否动态)。由于内存是在堆栈上分配的,因此缓冲区溢出/下溢比仅使用 malloc 具有更严重的后果。

特别是,函数的返回地址存储在堆栈中。如果此值损坏,您的代码可能会转到内存的任何可执行区域。编译器竭尽全力使这变得困难(特别是通过随机化地址布局)。但是,这显然比堆栈溢出更糟糕,因为如果返回值损坏,最好的情况是 SEGFAULT,但它也可能开始执行随机的内存块,或者在最坏的情况下,某些内存区域会危及程序的安全性.

于 2018-12-30T22:29:57.113 回答
0

IMO 使用 alloca 和可变长度数组的最大风险是,如果分配大小出乎意料地大,它可能会以非常危险的方式失败。

堆栈上的分配通常不检查用户代码。

现代操作系统通常会在下面放置一个保护页面*以检测堆栈溢出。当堆栈溢出时,内核可能会扩展堆栈或终止进程。Linux 在 2017 年扩展了这个保护区,使其比页面大得多,但它的大小仍然是有限的。

因此,作为一项规则,在使用之前的分配之前,最好避免在堆栈上分配多于一个页面。使用 alloca 或可变长度数组很容易导致攻击者在堆栈上进行任意大小的分配,从而跳过任何保护页并访问任意内存。

* 在当今最广泛的系统上,堆栈向下增长。

于 2021-09-17T16:48:04.927 回答
-1

_alloca()这里的大多数答案很大程度上都没有抓住重点:使用可能比仅仅在堆栈中存储大对象更糟糕是有原因的。

与自动存储的主要区别在于_alloca()后者存在一个额外的(严重的)问题:分配的块不受编译器控制,因此编译器无法优化或回收它。

相比:

while (condition) {
    char buffer[0x100]; // Chill.
    /* ... */
}

和:

while (condition) {
    char* buffer = _alloca(0x100); // Bad!
    /* ... */
}

后者的问题应该很明显。

于 2017-04-28T14:31:41.020 回答