为什么使用 alloca()不被认为是良好的实践?

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

尽管有上述特性,为什么不鼓励使用 alloca()

173996 次浏览

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

< p >返回值 函数的作用是:返回一个指向对象开头的指针 分配空间。如果 分配的原因 堆栈溢出,程序行为未定义

这并不是说它永远不应该被使用。我工作的一个OSS项目广泛使用它,只要你没有滥用它(alloca'ing巨大的值),它是好的。一旦你超过了“几百字节”的标记,是时候使用malloc和friends了。您可能仍然会遇到分配失败,但至少您会得到一些失败的指示,而不是仅仅耗尽堆栈。

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

通过使用alloca(),你大大增加了得到Stack Overflow错误的机会(如果你幸运的话,或者如果你不幸运的话,一个莫名其妙的崩溃)。

正如此新闻组帖子中提到的,有几个原因可以解释为什么使用alloca是困难和危险的:

  • 并非所有编译器都支持alloca
  • 一些编译器对alloca的预期行为有不同的解释,因此即使在支持它的编译器之间也不能保证可移植性。
  • 一些实现存在bug。

其他答案都是正确的。然而,如果你想要使用alloca()分配的对象相当小,我认为这是一个比使用malloc()或其他方法更快更方便的好技术。

换句话说,alloca( 0x00ffffff )是危险的,可能会导致溢出,就像char hugeArray[ 0x00ffffff ];一样。小心谨慎,通情达理,你会没事的。

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

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

如果你

,你会相当安全
  • 不要返回指针或包含它的任何内容。
  • 不存储在堆上分配的任何结构的指针
  • 不让任何其他线程使用指针

真正的危险来自于以后有人违反这些条件的可能性。考虑到这一点,它非常适合将缓冲区传递给格式化文本的函数:)

原因如下:

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

并不是说任何人都会写这段代码,但是你传递给alloca的size参数几乎肯定来自某种输入,这可能是恶意的,目的是让你的程序alloca这样的一个巨大的东西。毕竟,如果大小不是基于输入,或者不可能很大,为什么不声明一个小的、固定大小的本地缓冲区呢?

几乎所有使用alloca和/或C99 vlas的代码都有严重的错误,这些错误会导致崩溃(如果你幸运的话)或特权损害(如果你不那么幸运的话)。

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

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();
}
}

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

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

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

每个人都已经指出了堆栈溢出潜在的未定义行为,但我应该提到的是,Windows环境有一个很好的机制来捕捉这种情况,使用结构化异常(SEH)和保护页面。由于堆栈只在需要时增长,因此这些保护页驻留在未分配的区域。如果你对它们进行分配(通过溢出堆栈),就会抛出一个异常。

您可以捕获这个SEH异常并调用_resetstkoflw来重置堆栈并继续您的快乐之路。这并不理想,但这是另一种机制,至少可以在事情发生时知道哪里出了问题。*nix可能有类似的东西,但我不知道。

我建议通过包装alloca并在内部跟踪它来限制您的最大分配大小。如果你真的很认真,你可以在函数的顶部设置一些作用域哨兵来跟踪函数作用域中的任何分配,并检查它与项目允许的最大数量是否一致。

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

可悲的是,真正令人敬畏的alloca()在几乎令人敬畏的tcc中缺失了。Gcc确实有alloca()

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

  2. malloc()一样,它在失败时返回一个无效指针,这将在有MMU的现代系统上分段故障(希望重新启动那些没有MMU的系统)。

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

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

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

注意,malloc()没有提供更多,因为当系统内存不足时,它返回NULL(如果分配了NULL,也会段fault)。也就是说,你所能做的就是保释或试图以任何方式转让它。

要使用malloc(),我使用全局变量并将其赋值为NULL。如果指针不是NULL,则在使用malloc()之前释放它。

如果想复制任何现有数据,也可以使用realloc()作为通用情况。你需要检查指针之前,以确定是否要复制或连接realloc()之后。

3.2.5.2 alloca的优点

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

我没有看到这样的共识。很多强大的专业人士;一些缺点:

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

        在许多系统中,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
      }
      }
      

    alloca ()是很好的和有效的…但它也被深深打破了。

    • 破坏作用域行为(函数作用域而不是块作用域)
    • 使用不一致的malloc (alloca ()-ted指针不应该被释放,因此你必须跟踪指针从哪里指向free (),只有那些你用malloc ()得到的指针)
    • 同时使用内联时的不良行为(作用域有时会转到调用方函数,取决于被调用方是否内联)。
    • 没有堆栈边界检查
    • 失败时未定义的行为(不像malloc…那样返回NULL)失败意味着什么,因为它不检查堆栈边界…)
    • 非ANSI标准

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

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

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

    的正常用例

    alloca并不比变长数组(VLA)差,但它比在堆上分配更危险。

    在x86上(最常见的是在ARM上),堆栈向下增长,这带来了一定的风险:如果你不小心写超出了分配给alloca的块(例如,由于缓冲区溢出),那么你将覆盖函数的返回地址,因为它位于"在堆栈上,即你分配的块。

    _alloca block on the stack

    这样做的后果是双重的:

    1. 程序将会崩溃,并且不可能知道它为什么或在哪里崩溃(由于覆盖了帧指针,堆栈很可能会unwind到一个随机地址)。

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

    相反,如果你在堆上写超过一个块,你"just"获得堆损坏。程序可能会意外终止,但会正确地展开堆栈,从而减少恶意代码执行的机会。

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

    当正确和小心使用时,一致使用alloca() (可能是整个应用程序)来处理小的可变长度分配 (或C99 VLAs,如果可用)可能导致整体堆栈较低 Growth 而不是使用超大的等价实现 固定长度的本地数组。因此,如果你仔细使用它,alloca()可能是good for your stack

    我在....上找到了这句话好吧,这句话是我编的。但真的,想想看....

    @j_random_hacker在他的其他回答下的评论是非常正确的:避免使用alloca()来支持超大的本地数组并不能使你的程序在堆栈溢出方面更安全(除非你的编译器足够老,允许使用alloca()的函数内联,在这种情况下你应该升级,或者除非你在循环内部使用alloca(),在这种情况下你应该……不要在循环内部使用alloca())。

    我在桌面/服务器环境和嵌入式系统工作过。很多嵌入式系统不使用一个堆(他们甚至不链接支持),原因包括感知,动态分配的内存是恶因内存泄漏的风险在多年来从来没有重新启动的应用程序,或动态内存的更合理的理由是危险的,因为它不能以确保应用程序不会片段堆错误记忆的疲惫。因此嵌入式程序员几乎没有其他选择。

    alloca()(或VLAs)可能是完成这项工作的合适工具。

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

    但如果你选择使用alloca()

    根据本页上的其他答案,似乎VLAs应该是安全的(如果从循环中调用,它们不会复合堆栈分配),但如果你正在使用alloca(),请注意不要在循环中使用它,并使确定你的函数不能内联,如果它有任何机会可能在另一个函数的循环中调用。

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

    例如,C编译器常见的优化是在函数中消除帧指针的使用,而是相对于堆栈指针进行帧访问;所以还有一种通用寄存器。但如果在函数内部调用alloca,则sp和fp之间的差异对于函数的一部分是未知的,因此无法进行此优化。

    考虑到它的使用很少,以及它作为标准函数的不良地位,编译器设计人员很可能会禁用任何优化,因为可能对alloca造成了麻烦,如果要使它与alloca一起工作需要付出更多的努力。

    < >强更新: 由于变长局部数组已经添加到C语言中,并且由于这些向编译器提出了与alloca非常相似的代码生成问题,我看到“使用的罕见性和阴暗状态”不适用于底层机制;但是我仍然怀疑使用alloca或VLA会损害使用它们的函数中的代码生成。我将欢迎来自编译器设计人员的任何反馈

    在我看来,alloca()在可用的情况下,应该仅以受约束的方式使用。就像“goto”的使用一样,相当多理智的人不仅对alloca()的使用非常反感,而且对它的存在也非常反感。

    对于嵌入式使用,其中堆栈大小是已知的,并且可以通过对分配大小的约定和分析施加限制,并且编译器不能升级到支持C99+,使用alloca()是很好的,而且我已经知道使用它。

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

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

    在嵌入式领域,可移植性不是一个太大的问题,但是它是反对在严格控制的环境之外使用alloca()的一个很好的理由。

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

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

    这里的大多数回答都忽略了一点:使用_alloca()可能比仅仅在堆栈中存储大对象更糟糕,这是有原因的。

    自动存储和_alloca()之间的主要区别是后者有一个额外的(严重的)问题:分配的块是不受编译器控制,所以编译器没有办法优化或回收它。

    比较:

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

    :

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

    后者的问题应该是显而易见的。

    alloca的一个缺陷是longjmp将它倒带。

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

    为了澄清,我在这里特别提到的是一种情况,即longjmp不会从alloca发生的函数中返回!相反,函数使用setjmp保存上下文;然后用alloca分配内存,最后对该上下文执行longjmp。该函数的alloca内存未全部释放;就是它自setjmp开始分配的所有内存。当然,我说的是观察到的行为;我所知道的任何alloca都没有这样的要求。

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

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

    我曾经遇到过这种情况,这是一个bug的根本原因。

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

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

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

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

    自动使用longjmp(参见非本地出口)完成非本地退出 方法退出时释放用alloca分配的空间 函数调用alloca。这是使用的最重要的原因 alloca < /强> < / p >

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

    1. https://www.gnu.org/software/libc/manual/html_node/Advantages-of-Alloca.html
    2. 介绍细节来自非本地出口
    3. 分配示例 . rref ="https://www.gnu.org/software/libc/manual/html_node/Alloca-Example.html" rel="nofollow noreferrer">分配

    在我看来,分配和变长数组的最大风险是,如果分配的大小出乎意料地大,它可能会以非常危险的方式失败。

    堆栈上的分配通常没有检入用户代码。

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

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

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