为什么使用alloca()不被视为良好做法?

时间:2009-06-19 16:24:17

标签: c stack malloc allocation alloca

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

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

25 个答案:

答案 0 :(得分:216)

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

  

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

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

答案 1 :(得分:188)

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

在头文件中:

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

在实施文件中:

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

所以发生的事情是编译器内联DoSomething函数,并且所有堆栈分配都发生在Process()函数内部,从而将堆栈向上吹。在我的辩护中(我不是那个发现问题的人;当我无法解决时,我不得不去找其中一位高级开发人员),它不是直的alloca,而是ATL字符串转换宏之一。

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

答案 2 :(得分:69)

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

char arr[size];

而不是

char *arr=alloca(size);

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

答案 3 :(得分:57)

如果你不能使用标准局部变量,那么

alloca()非常有用,因为它的大小需要在运行时确定,你可以 绝对保证在此函数返回后,不会使用从alloca()获得的指针。

如果你

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

真正的危险来自于其他人稍后会违反这些条件的可能性。考虑到这一点,将缓冲区传递给将文本格式化为其中的函数是很好的:)

答案 4 :(得分:39)

正如this newsgroup posting中所述,使用alloca可能被视为困难和危险的原因有几个:

  • 并非所有编译器都支持alloca
  • 有些编译器以不同的方式解释alloca的预期行为,因此即使在支持它的编译器之间也无法保证可移植性。
  • 有些实施方案是错误的。

答案 5 :(得分:25)

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

答案 6 :(得分:21)

  

仍然禁止使用alloca,为什么?

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

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

      

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

  •   
  

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

alloca

答案 7 :(得分:12)

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

换句话说,alloca( 0x00ffffff )很危险,可能会导致溢出,与char hugeArray[ 0x00ffffff ];完全一样。要小心谨慎,你会没事的。

答案 8 :(得分:11)

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

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

我建议通过包装alloca并在内部跟踪来限制最大分配大小。如果你真的很蠢,你可以在你的函数顶部抛出一些范围哨事来跟踪函数范围内的任何alloca分配,并且根据项目允许的最大数量进行健全检查。

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

答案 9 :(得分:10)

alloca()既美观又高效......但它也深受打击。

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

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

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

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

答案 10 :(得分:9)

原因如下:

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

并非所有人都会编写此代码,但您传递给alloca的大小参数几乎肯定来自某种输入,这可能会恶意地使您的程序变为alloca巨大的东西像那样。毕竟,如果大小不是基于输入或者没有可能变大,为什么不直接声明一个小的,固定大小的本地缓冲区呢?

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

答案 11 :(得分:9)

很多有趣的答案#34; old&#34;问题,甚至一些相对较新的答案,但我没有找到任何提及这个....

  

如果使用得当且小心谨慎,请始终使用alloca()   (可能是应用程序范围内的)处理小的可变长度分配   (或C99 VLA,如果可用)可导致降低整体堆栈   增长比使用超大的其他等效实现   固定长度的局部数组。因此,如果您仔细使用,alloca()可能 适合您的筹码

我发现引用....好吧,我把这个引用了。但是真的,想一想......

@j_random_hacker在其他答案的评论中是非常正确的:避免使用alloca()支持超大的本地数组不会使您的程序更安全地从堆栈溢出(除非您的编译器足够大以允许内联使用alloca()的函数,在这种情况下你应该升级,或者除非你在内部循环中使用alloca(),在这种情况下你应该......在循环中不使用alloca()

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

alloca()(或VLA)可能只是该工作的正确工具。

我已经看过时间&amp;程序员制作堆栈分配缓冲区的时间再次超过了#34;大到足以处理任何可能的情况&#34;。在深度嵌套的调用树中,重复使用该(反 - ?)模式会导致堆栈使用过度。 (想象一下20级深度的调用树,在每个级别出于不同的原因,函数盲目地过度分配1024字节的缓冲区&#34;只是为了安全&#34;通常它只使用16或更少的它们并且只有在非常罕见的情况下才可以使用更多。)另一种方法是使用alloca()或VLA并仅分配与函数需要相同的堆栈空间,以避免不必要地增加堆栈负担。希望当调用树中的一个函数需要大于正常的分配时,调用树中的其他函数仍然使用它们的正常小分配,并且整个应用程序堆栈的使用量明显少于每个函数盲目地过度分配本地缓冲区的情况。

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

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

答案 12 :(得分:7)

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

答案 13 :(得分:6)

如果你不小心写了超出用alloca分配的块(例如由于缓冲区溢出),那么你将覆盖你的函数的返回地址,因为那个位于&#34;上述&#34;在堆栈上,即在分配的块后

_alloca block on the stack

这样做的后果是双重的:

  1. 程序会崩溃,并且无法判断崩溃的原因或位置(由于覆盖的帧指针,堆栈最有可能放松到随机地址)。

  2. 它使缓冲区溢出的危险性增加了许多倍,因为恶意用户可以制作一个特殊的有效载荷,这些有效载荷将放在堆栈中,因此最终可以执行。

  3. 相比之下,如果你在堆上写一个块之外你只是&#34;只是&#34;得到堆腐败。该程序可能会意外终止,但会正确展开堆栈,从而减少恶意代码执行的可能性。

答案 14 :(得分:5)

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

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

鉴于其使用的罕见性及其作为标准功能的阴暗状态,编译器设计人员很可能禁用可能导致alloca出现问题的任何优化,如果需要的话使用alloca可以让它更有效。

答案 15 :(得分:4)

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

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

  2. malloc()类似,它会在失败时返回一个无效指针,这会在使用MMU的现代系统上发生段错误(并希望重启那些没有)。

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

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

    如果你推得太深,你就可以确定是否存在段错(如果你有MMU)。

    请注意malloc()不再提供,因为当系统内存不足时,它会返回NULL(如果已分配,也会发生段错误)。即所有你能做的就是保释,或者只是试着以任何方式分配它。

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

    如果要复制任何现有数据,您也可以使用realloc()作为一般情况。如果要在realloc()之后复制或连接,则需要先检查指针。

    3.2.5.2 Advantages of alloca

答案 16 :(得分:3)

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

答案 17 :(得分:3)

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

通过使用alloca(),您可以大大增加获得Stack Overflow错误的机会(如果您很幸运,或者如果您不幸,则会出现无法解释的崩溃)。

答案 18 :(得分:3)

不是很漂亮,但如果性能真的很重要,你可以预先在堆栈上分配一些空间。

如果你现在已经是你需要的内存块的最大大小,并且你想要保持溢出检查,你可以做类似的事情:

void f()
{
    char array_on_stack[ MAX_BYTES_TO_ALLOCATE ];
    SomeType *p = (SomeType *)array;

    (...)
}

答案 19 :(得分:1)

alloca功能很棒,并且所有反对者都在简单地传播FUD。

void foo()
{
    int x = 50000; 
    char array[x];
    char *parray = (char *)alloca(x);
}

数组和parray完全相同,风险相同。说一个比另一个更好是一种语法选择,而不是技术选择。

至于选择堆栈变量与堆变量,对于具有范围内生命周期的变量,使用堆栈堆栈的长期运行程序有很多优点。您可以避免堆碎片,并且可以避免使用未使用的(不可用的)堆空间来增加进程空间。你不需要清理它。您可以控制进程的堆栈分配。

为什么这么糟糕?

答案 20 :(得分:1)

恕我直言,alloca被认为是不好的做法,因为每个人都害怕耗尽堆栈大小限制。

通过阅读这个帖子和其他一些链接,我学到了很多东西:

我主要使用alloca使我的普通C文件可以在msvc和gcc上编译而不做任何修改,C89样式,没有#ifdef _MSC_VER等。

谢谢!这个帖子让我注册了这个网站:)

答案 21 :(得分:1)

在我看来,alloca()(如果可用)应该只能以受约束的方式使用。非常像使用&#34; goto&#34;,相当多的其他合理的人不仅对alloca()的使用有强烈的厌恶,而且还存在于alloca()。

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

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

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

在嵌入式空间中,可移植性不是一个问题,但是在精心控制的情况之外,它是反对使用alloca()的一个很好的论据。

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

答案 22 :(得分:0)

这里的大多数答案都很难忽略这一点:使用_alloca()可能比仅仅在堆栈中存储大型对象更糟糕。

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

比较

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

使用:

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

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

答案 23 :(得分:0)

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

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

答案 24 :(得分:0)

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

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

<块引用>

使用 longjmp(见 Non-Local Exits)自动完成非本地退出 当它们退出时释放用 alloca 分配的空间 调用 alloca 的函数。 这是最重要的使用理由 alloca

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

  1. https://www.gnu.org/software/libc/manual/html_node/Advantages-of-Alloca.html
  2. IntroDetails 来自 Non-Local Exits
  3. Alloca Example