为什么不将alloca()用作良好实践?


400

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

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


40
只是一个简短的说明。尽管此功能可以在大多数编译器中找到,但ANSI-C标准并不需要它,因此可能会限制可移植性。另一件事是,您一定不要!free()获得的指针,退出函数后将自动释放该指针。
merkuro

9
同样,如果这样声明,则不会内联具有alloca()的函数。
Justicle

2
@Justicle,您能提供一些解释吗?我很好奇这种行为的
根源

47
忘掉所有关于可移植性的烦恼,无需调用free(这显然是一个优势),无法内联它(显然,堆分配非常重)等。避免这样做的唯一原因alloca是使用大尺寸文件。也就是说,浪费大量的堆栈内存不是一个好主意,而且您还有堆栈溢出的机会。如果是这种情况,请考虑使用malloca/freea
valdo 2011年

5
另一个积极的方面alloca是,堆栈不能像堆一样分散。这对于硬实时实时运行样式的应用程序甚至安全性至关重要的应用程序都可能有用,因为可以对WCRU进行静态分析,而无需诉诸具有自身问题(无时间局部性,次优资源)的自定义内存池。采用)。
安德烈亚斯(Andreas)'18年

Answers:


245

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

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

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


35
因此,使用声明大型数组就不会有问题吗?
TED

88
@Sean:是的,堆栈溢出风险是给出的原因,但是这个原因有点愚蠢。首先,因为(如Vaibhav所说)大型局部数组会导致完全相同的问题,但并没有受到如此严重的谴责。同样,递归也可以轻松地使堆栈崩溃。抱歉,但我希望您-1能够反驳普遍存在的想法,即手册页中给出的原因是合理的。
j_random_hacker

49
我的观点是手册页中给出的理由是没有意义的,因为alloca()与其他被认为是洁食的东西(本地数组或递归函数)一样“糟糕”。
j_random_hacker 2010年

39
@ninjalj:不是由经验丰富的C / C ++程序员编写的,但我确实认为许多人alloca()对本地数组或递归没有相同的恐惧(实际上,许多会大喊大叫的人alloca()会称赞递归,因为它“看起来很优雅”) 。我同意Shaun的建议(“ alloca()适用于少量分配”),但我不同意将alloca()视为三者中唯一邪恶的思维定势-它们同样危险!
j_random_hacker 2010年

35
注意:考虑到Linux的“乐观”内存分配策略,您很可能不会得到堆耗尽失败的任何指示……相反,malloc()将返回一个不错的非NULL指针,然后当您尝试实际如果访问它所指向的地址空间,您的进程(或某些其他进程,出乎意料)将被OOM杀手杀死。当然,这本身就是Linux的“功能”,而不是C / C ++问题,但是在辩论alloca()或malloc()是否“更安全”时要牢记这一点。:)
杰里米·弗里斯纳

209

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

在头文件中:

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

在实现文件中:

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

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

因此,教训是-不要alloca在您认为可能内联的函数中使用。


91
有趣。但是那不算是编译器错误吗?毕竟,内联更改了代码的行为(它延迟了释放使用alloca分配的空间)。
sleske 2010年

60
显然,至少GCC会考虑到这一点:“请注意,函数定义中的某些用法可能使其不适合内联替换。这些用法包​​括:使用varargs,使用alloca,[...]”。gcc.gnu.org/onlinedocs/gcc/Inline.html
sleske 2010年

135
你在抽什么编译器?
Thomas Eding

22
我不明白的是,为什么编译器没有充分利用作用域来确定子作用域中的alloca或多或少被“释放了”:堆栈指针可能会在进入作用域之前回到其指向,就像什么时候完成从函数返回(不是吗?)
moala 2012年

7
我已经投票了,但是答案写得很清楚:我同意其他人的观点,因为这显然是编译器错误,因此您错失了alloca 。编译器在不应该进行的优化中做出了错误的假设。解决编译器错误是可以的,但是除了编译器之外,我不会对此提出任何错误。
埃文·卡罗尔

75

旧问题,但没有人提到应将其替换为可变长度数组。

char arr[size];

代替

char *arr=alloca(size);

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


5
乔纳森·莱夫勒(Jonathan Leffler)在评论亚瑟·乌尔费尔特(Arthur Ulfeldt)的回答时提到了这一点。
ninjalj 2010年

2
的确如此,但是它也显示了它被遗漏的容易程度,因为尽管在发布之前阅读了所有回复,但我没有看到它。
PatrickSchlüter'10

6
请注意-这些是可变长度数组,而不是动态数组。后者是可调整大小的,通常在堆上实现。
TimČas2012年

1
Visual Studio 2015编译某些C ++具有相同的问题。
ahcox '16

2
Linus Torvalds不喜欢Linux内核中的VLA。从4.20版开始,Linux应该几乎没有VLA。
克里斯蒂安·丘皮图

60

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

如果您可以相当安全

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

真正的危险来自某人稍后某个时候违反这些条件的机会。考虑到这一点,将缓冲区传递给将文本格式化为它们的函数非常好:)


12
C99的VLA(可变长度数组)功能支持动态调整大小的局部变量,而无需显式要求使用alloca()。
乔纳森·莱夫勒


1
但这与使用指向局部变量的指针进行处理没有什么不同。他们也可以被愚弄...
glglgl 2012年

2
@Jonathan Leffler您可以使用alloca进行操作但不能使用VLA进行操作的一件事是对它们使用strict关键字。像这样:float * strict heavy_used_arr = alloca(sizeof(float)* size); 而不是float heavy_used_arr [size]。即使size是一个编译常数,它也可能有助于某些编译器(在我的情况下为gcc 4.8)优化程序集。看到我的问题:stackoverflow.com/questions/19026643/using-restrict-with-arrays
Piotr Lopusiewicz

@JonathanLeffler VLA位于包含它的块的本地。另一方面,alloca()分配持续到功能结束的内存。这意味着似乎没有直接,方便的转换为的VLA f() { char *p; if (c) { /* compute x */ p = alloca(x); } else { p = 0; } /* use p */ }。如果您认为可以将allocaVLA的使用自动转换为VLA的使用,但不仅仅需要注释来描述如何使用,我可以提出这个问题。
Pascal Cuoq 2014年

40

本新闻组中所述,有一些原因导致使用alloca困难和危险:

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

24
我在该链接上看到的一件事是,该链接不在页面上的其他地方,这是使用的函数alloca()需要单独的寄存器来保存堆栈指针和帧指针。在x ==> 386的x86 CPU上,堆栈指针ESP可同时用于这两个指针,从而释放它们EBP-除非alloca()使用该指针。
j_random_hacker 2010年

10
该页面上的另一个优点是,除非编译器的代码生成器将其作为特殊情况进行处理,f(42, alloca(10), 43);否则可能会由于至少在将一个参数推入堆栈指针alloca() 之后对堆栈指针进行调整而导致崩溃。
j_random_hacker 2010年

3
链接的帖子似乎由约翰·莱文(John Levine)撰写,他是写“链接器和装载器”的家伙,我相信他所说的一切。
user318904 2011年

3
链接的帖子是对John Levine的帖子的回复
A. Wilcox 2014年

6
请记住,自1991年以来发生了很多变化。所有现代C编译器(甚至在2009年)都必须将alloca作为特殊情况来处理。它是一个内在函数,而不是普通函数,甚至可能不会调用函数。因此,参数分配问题(从1970年代在K&R C中出现)现在不应该成为问题。我在评论Tony D的评论中提供了更多详细信息
greggo

26

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


21

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

我不认为这样的共识。很多强大的职业选手;一些缺点:

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

由于处理几种类型的特殊方式而没有+1 :-(
einpoklum

@einpoklum:很好,这很有启发性...谢谢。
托尼·德罗伊

1
我再说一遍:这是一个很好的答案。直到我认为您在建议人们使用某种反模式。
einpoklum 2015年

linux联机帮助页上的评论很老,我敢肯定,它已经过时了。所有现代编译器都知道alloca()是什么,并且不会像这样绊脚石。在旧的K&R C中,(1)所有函数都使用帧指针(2)所有函数调用都是{push args on stack} {call func} {add#n,sp}。alloca是一个lib函数,只会增加堆栈,编译器甚至都不知道发生了什么。(1)和(2)不再是真实的,因此alloca不能那样工作(现在它是一个内在函数)。在旧的C语言中,在推入args的中间调用alloca显然也会破坏这些假设。
greggo,

4
关于示例,我通常会担心需要 always_inline来避免内存损坏的
事情...。– greggo

14

所有其他答案都是正确的。但是,如果您要分配使用的东西alloca()很小,我认为这是个好方法,比使用malloc()其他方法更快,更方便。

换句话说,它alloca( 0x00ffffff )是危险的,并且很可能导致溢出char hugeArray[ 0x00ffffff ];。谨慎而合理,您会没事的。


12

这个“老”问题有很多有趣的答案,甚至还有一些相对较新的答案,但我没有发现任何提及这个问题的答案。

如果使用得当,请谨慎使用alloca() (如果可能在整个应用程序范围内)一致地使用,以处理较小的可变长度分配(或C99 VLA,如果可用),与使用固定长度的超大局部数组的等效实现相比,它可能导致总体堆栈增长降低。。因此如果仔细使用它alloca()可能对您的堆栈有好处

我找到了那个报价。...好,我把那个报价编了起来。但实际上,请考虑一下...。

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

我曾在台式机/服务器环境和嵌入式系统上工作。许多嵌入式系统根本不使用堆(它们甚至不支持堆),原因包括人们认为动态分配的内存是有害的,这是由于应用程序存在内存泄漏的风险,一次会重新启动数年,或者更合理的理由认为动态内存是危险的,因为无法确定某个应用程序永远不会将其堆碎片化为错误的内存耗尽点。因此,嵌入式程序员几乎没有其他选择。

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

我一次又一次地看到程序员使堆栈分配的缓冲区“大到足以应付任何可能的情况”。在深度嵌套的调用树中,重复使用该(反-)模式会导致堆栈使用过度。(想象一下,调用树深度为20层,由于不同的原因,在每个层上,该函数盲目地过度分配1024个字节的缓冲区“为了安全起见”,而通常它只会使用16个或更少的缓冲区,极少数情况下可能会使用更多。)一种替代方法是使用alloca()或VLA并仅分配函数所需的堆栈空间,以避免不必要地增加堆栈负担。希望当调用树中的一个函数需要比正常更大的分配时,调用树中的其他函数仍在使用其正常的小分配,并且与每个函数盲目地过度分配本地缓冲区的情况相比,整个应用程序堆栈的使用率要低得多。 。

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

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


我同意这一点。的危险alloca()是正确的,但是对于内存泄漏来说也可以这样malloc()(为什么不使用GC?有人可能会争论)。alloca()谨慎使用时,对于减小堆栈大小非常有用。
Felipe Tonello

不使用动态内存的另一个很好的理由,尤其是在嵌入式环境中:动态内存比坚持堆栈要复杂得多。使用动态内存需要特殊的过程和数据结构,而在堆栈上(为简化起见)则是要从堆栈指针中添加/减去更大的数字。
tehftw

旁注:“使用固定缓冲区[MAX_SIZE]”示例强调了为什么过量使用内存策略如此有效。程序分配它们可能永远不会接触的内存,除非缓冲区长度受限制。因此,Linux(和其他操作系统)在第一次使用内存之前(而不是malloc的)实际上并不分配内存页面是很好的。如果缓冲区大于一页,则程序只能使用第一页,而不会浪费其余的物理内存。
Katastic Voyage,

@KatasticVoyage除非MAX_SIZE大于(或至少等于)系统虚拟内存页面的大小,否则您的论点不会成立。同样在没有虚拟内存的嵌入式系统上(许多嵌入式MCU没有MMU),从“确保程序在所有情况下都可以运行”的角度来看,过量使用内存策略可能是好的,但是要保证的代价是堆栈大小同样必须分配该内存以支持该过量使用内存策略。在某些嵌入式系统上,这是一些低成本产品制造商不愿支付的价格。
phonetagger

11

每个人都已经指出了一个大问题,即堆栈溢出可能导致未定义的行为,但是我应该提到Windows环境具有使用结构化异常(SEH)和保护页来捕获此错误的强大机制。由于堆栈仅根据需要增长,因此这些保护页位于未分配的区域。如果分配给它们(通过溢出堆栈),则会引发异常。

您可以捕获此SEH异常,然后调用_resetstkoflw以​​重置堆栈并继续以自己的方式继续。这不是理想的方法,但是它是至少可以在出现问题时知道出问题的另一种机制。* nix可能有一些我不知道的类似东西。

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

此外,除了不允许内存泄漏外,alloca不会导致内存碎片,这非常重要。如果您聪明地使用alloca,我认为它并不是坏习惯,这基本上适用于所有情况。:-)


问题是,这alloca()可能需要大量空间,以至于堆栈指针落在堆中。这样,可以控制alloca()缓冲区大小和进入该缓冲区的数据的攻击者可以覆盖堆(这很糟糕)。
12431234123412341234123 '11年

SEH是仅Windows的东西。如果只关心在Windows上运行的代码,那很好,但是如果您的代码需要跨平台(或者如果您编写的代码仅在非Windows平台上运行),那么您就不必依赖于SEH。
乔治

10

alloca()很好并且很有效...但是它也被深深地打断了。

  • 损坏的作用域行为(函数作用域而不是块作用域)
  • 与malloc不一致使用(不应释放alloca()- ted指针,因此,您必须跟踪指针从何处指向free(),而仅跟踪malloc()获得的指针)
  • 同时使用内联时的不良行为(scope有时会转到调用方函数,具体取决于被调用方是否内联)。
  • 没有堆栈边界检查
  • 发生故障时发生未定义的行为(不会像malloc一样返回NULL。由于什么也不会检查堆栈边界,所以故障意味着什么……)
  • 不是ansi标准

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

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

当然,使用所需空间的主要部分的局部结构或数组仍然更好,如果没有这样的主要堆分配,则使用纯malloc()可能是明智的。我看不到任何合理的用例,在这些情况下,您确实确实需要alloca()VLA。


我看不出
拒绝投票

只有名称具有范围。alloca不会创建名称,只会创建一个具有生存时间的内存范围。
curiousguy 2015年

@curiousguy:您只是在玩文字游戏。对于自动变量,我也可以说底层内存的生存期,因为它与名称的范围相匹配。无论如何,问题不是我们如何称呼它,而是alloca返回的生存期/内存范围的不稳定和异常行为。
克里斯(Kriss)2015年

2
我希望alloca拥有一个相应的“ freea”,并有一个规范,即调用“ freea”将撤消创建对象的“ alloca”以及所有后续对象的影响,并且要求必须在功能中存储“ alloca”也可以在其中“释放”。这样一来,几乎所有的实现都可以以兼容的方式支持alloca / freea,可以减轻内联的问题,并且通常使事情变得更加整洁。
超级猫

2
@supercat-我也希望如此。出于这个原因(多),我用一个抽象层(主要是宏和在线功能),使我从来没有打电话allocamallocfree直接。我说{stack|heap}_alloc_{bytes,items,struct,varstruct}{stack|heap}_dealloc。所以,heap_dealloc只是调用freestack_dealloc是一个空操作。这样,堆栈分配可以轻松地降级为堆分配,并且意图也更加清楚。
Todd Lehman

9

原因如下:

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

并不是有人会写这段代码,而是您传递给的size参数alloca几乎可以肯定来自某种输入,这可能会恶意地将您的程序扩展到alloca类似这样的规模。毕竟,如果大小不是基于输入的,或者没有很大的可能性,为什么不只声明一个固定大小的局部本地缓冲区呢?

几乎所有使用alloca和/或C99 vlas的代码都有严重的错误,这些错误会导致崩溃(如果您很幸运)或特权泄露(如果您不太幸运)。


1
世界可能永远不会知道。:(就是说,我希望您能澄清一个有关我的问题alloca。您说几乎所有使用它的代码都有错误,但是我打算使用它;通常我会忽略这样的说法,但是从你那里我不会,我正在写一个虚拟机,我想分配那些不会从堆栈上的函数中逸出的变量,而不是动态的,因为它能极大地提高速度。具有相同性能特征的方法吗?我知道我可以接近内存池,但这仍然不便宜。你会怎么做?
GManNickG 2011年

7
知道还有什么危险吗?这个:*0 = 9;太神奇了!我猜我永远不要使用指针(或至少取消对它们的引用)。等等,等等。我可以测试一下是否为空。嗯 我想我也可以测试要通过分配的内存大小alloca。奇怪的人 奇怪的。
Thomas Eding

7
*0=9;是无效的C。至于测试传递给您的大小,请alloca针对什么进行测试?没有办法知道这个限制,如果您只是要对一个微小的已知安全大小(例如8k)进行测试,那么您最好在堆栈上使用一个固定大小的数组。
R .. GitHub STOP HELPING ICE

7
我认为您的问题“要么大小足够小,要么取决于输入,因此可能任意大”,因为它对递归同样适用。一个实际的折衷(对于两种情况)是假设如果大小受限制,small_constant * log(user_input)那么我们可能有足够的内存。
j_random_hacker 2012年

1
确实,您已经确定了一种情况,其中VLA / alloca很有用:递归算法,其中在任何调用帧处所需的最大空间可能等于N,但是在所有递归级别上所需的空间总和为N或某个函数的N不会快速增长。
R .. GitHub停止帮助ICE

9

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

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

考虑到其使用的稀有性以及其作为标准函数的不安全状态,编译器设计人员很可能会禁用可能对alloca造成麻烦的任何优化,如果要花费更多的精力才能使其与alloca一起使用。

更新: 由于可变长度局部数组已添加到C中,并且由于它们在分配程序中与alloca呈现出非常相似的代码生成问题,因此我看到“使用稀有性和不可靠状态”不适用于底层机制;但是我仍然怀疑使用alloca或VLA会损害使用它们的函数中的代码生成。我欢迎编译器设计师的任何反馈。


1
可变长度数组从未添加到C ++中。
尼尔·弗里德曼

@NirFriedman确实。我认为有一个基于旧建议的Wikipedia功能列表。
greggo

> 我仍然怀疑使用alloca或VLA会损害代码生成,我会认为使用alloca需要帧指针,因为堆栈指针的移动方式在编译时并不明显。可以在循环中调用alloca来获取更多的堆栈内存,或者使用运行时计算出的大小等。它不被使用。
哈兹

8

一个陷阱allocalongjmp倒退的。

也就是说,如果使用保存上下文setjmp,则需要alloca一些内存,然后再保存到上下文,则longjmp可能会丢失该alloca内存。堆栈指针又回到了原来的位置,因此不再保留内存。如果您调用一个函数或执行另一个函数alloca,则会破坏原始函数alloca

为了澄清,我在这里具体指的是一种情况,其中这种情况longjmp不会从发生功能的alloca地方返回!而是,函数使用来保存上下文setjmp;然后使用分配内存,alloca最后对该上下文进行一次longjmp。该函数的alloca内存并没有全部释放。只是自以来分配的所有内存setjmp。当然,我说的是一种观察到的行为。alloca我所知道的任何此类要求都没有记录在案。

文档中的重点通常是这样的概念:alloca内存与功能激活关联,而不与任何块关联;alloca该函数的多次调用仅会占用更多的堆栈内存,而这些内存将在函数终止时释放。不是这样;内存实际上与过程上下文相关联。使用还原上下文后longjmp,先前alloca状态也将恢复。这是因为堆栈指针寄存器本身被用于分配,并且(有必要)保存和恢复到jmp_buf

顺便说一句,如果这样做的话,它提供了一种合理的机制来故意释放分配给的内存alloca

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


1
那是应该做的- longjmp回去做,使程序忘记堆栈中发生的所有事情:所有变量,函数调用等。alloca就像堆栈中的数组一样,因此预计它们将被破坏像堆栈上的其他所有内容一样。
tehftw

1
man alloca给出以下语句:“由于alloca()分配的空间是在堆栈帧内分配的,因此,如果通过调用longjmp(3)或siglongjmp(3)来跳过函数返回,则该空间将自动释放。” 因此,有文献记载,分配给的内存alloca在结束后会崩溃longjmp
tehftw

@tehftw发生上述情况时,没有函数返回被跳过longjmp。目标函数尚未返回。它已经做完了setjmpalloca然后longjmp。该longjmp可倒回alloca状态恢复到什么它是在setjmp时间。也就是说,所移动的指针alloca与尚未标记的局部变量存在相同的问题volatile
卡兹(Kaz)

3
我不明白为什么这应该是意外的。当您setjmp然后alloca,然后时longjmp,通常 alloca会倒带。的全部目的longjmp是回到保存在的状态setjmp
tehftw

@tehftw我从未见过这种特殊的交互记录。因此,除了通过编译器进行的经验研究之外,不能依靠任何一种方法。
卡兹(Kaz)

7

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


7
alloca()并不比任意整数int foo[bar];在哪里更危险bar
托德·雷曼

@ToddLehman没错,由于这个确切的原因,我们已经禁止内核中的VLA了好几年了,并且自2018年以来就不再使用VLA了:-)
Chris Down

6

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

_alloca堆栈上的块

其结果有两个方面:

  1. 该程序将崩溃,无法知道崩溃的原因或位置(由于帧指针被覆盖,堆栈很可能会退回到随机地址)。

  2. 由于恶意用户可以制作特殊的有效负载,因此缓冲区溢出的危险要高出许多倍,有效负载会被放入堆栈中,从而最终被执行。

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


11
在这种情况下,没有什么比缓冲区溢出固定大小的堆栈分配缓冲区的危险大不相同了。这种危险并非唯一alloca
phonetagger

2
当然不是。但是请检查原始问题。问题是:与之alloca相比,这有什么危险malloc(因此堆栈上没有固定大小的缓冲区)。
rustyx '16

较小的点,但是某些系统上的堆栈向上增长(例如PIC 16位微处理器)。
EBlake

5

可悲的alloca()是,几乎很棒的tcc缺少真正出色的工具。海湾合作委员会的确有alloca()

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

  2. malloc()它在失败时返回一个无效指针,它将在带有MMU的现代系统上进行段错误(并希望在没有MMU的情况下重新启动它们)。

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

它与递归一起很好地工作。您可以使用静态变量来实现类似于尾部递归的功能,并仅使用其他一些变量将信息传递给每次迭代。

如果推得太深,则会确保出现段故障(如果您有MMU)。

注意 malloc()由于系统内存不足时它会返回NULL(如果分配了segfault,也会返回segfault),因此不再提供任何服务。也就是说,您所能做的就是保释或只是尝试以任何方式分配它。

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

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

3.2.5.2 alloca的优势


4
实际上,alloca规范并没有说它在失败(堆栈溢出)时返回无效的指针,而是说它具有未定义的行为……而对于malloc来说,它表示返回的是NULL,而不是随机的无效指针(好,Linux乐观内存实现使得无用)。
kriss 2014年

@kriss Linux可能会杀死您的进程,但是至少它不会冒险进入未定义的行为
craig65535 '17

@ craig65535:表达式未定义行为通常表示该行为未由C或C ++规范定义。在任何给定的OS或编译器上,它都不是随机的或不稳定的。因此,将UB与操作系统名称(例如“ Linux”或“ Windows”)关联是没有意义的。它与它无关。
克里斯(Kriss)

我试图说malloc返回NULL,或者在Linux的情况下,内存访问杀死了您的进程,比alloca的未定义行为更可取。我想我一定听错了您的第一条评论。
craig65535 '17

3

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

通过使用它,alloca()可以大大增加发生堆栈溢出错误的机会(如果幸运的话,或者如果不是的话,将发生莫名其妙的崩溃)。


这在很大程度上取决于应用程序。具有内存限制的嵌入式应用程序的堆栈大小大于堆(如果甚至有一个堆),也很常见。
EBlake

3

不是很漂亮,但是如果性能确实很重要,则可以在堆栈上预分配一些空间。

如果您现在已经满足了内存块的最大大小,并且想要保留溢出检查,则可以执行以下操作:

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

    (...)
}

12
是否可以确保char数组针对任何数据类型正确对齐?alloca提供了这样的承诺。
JuhoÖstman10年

@JuhoÖstman:如果遇到对齐问题,可以使用struct(或任何类型)数组代替char。
kriss 2014年

这就是所谓的可变长度数组。C90及更高版本支持它,但C ++不支持。请参见我可以在C ++ 03和C ++ 11中使用C可变长度数组吗?
jww 2015年

3

alloca功能非常强大,所有反对者都只是在传播FUD。

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

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

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

为什么这样不好?


3

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


听起来gcc-2.95打破了alloca,可能无法安全地用于需要的程序alloca。当longjmp用于放弃这样做的帧时,它将如何清理内存alloca?今天什么时候将使用gcc 2.95?
卡兹(Kaz)

2

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

通过阅读此线程和其他链接,我学到了很多东西:

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

谢谢 !该线程使我注册了此站点:)


请记住,此站点上没有“线程”之类的东西。堆栈溢出具有问答格式,而不是讨论线程格式。在一个论坛中,“答案”不像“回复”。这意味着您实际上是在提供问题的答案,并且不应被用来回答其他答案或对该主题发表评论。至少有50位代表后,您就可以发表评论了,但是请务必阅读“ 我什么时候不应该发表评论?” 部分。请阅读“ 关于”页面,以更好地了解网站的格式。
Adi Inbar 2014年

1

我认为,alloca()(如果有)应仅以约束方式使用。非常类似于“ goto”的使用,否则,相当多的其他有理性的人不仅对alloca()的使用而且还存在强烈的反感。

对于嵌入式应用,已知堆栈大小并且可以通过约定和分析对分配大小进行限制,并且不能升级编译器以支持C99 +,则使用alloca()很好,我一直在已知使用它。

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

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

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

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


1

这里的大多数答案在很大程度上都忽略了这一点:有一个原因为什么要使用 _alloca()可能比仅将大型对象存储在堆栈中更糟糕。

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

相比:

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

与:

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

后者的问题应该很明显。


您是否有任何实际示例来说明VLA与VLA之间的区别alloca(是的,我要说VLA,因为alloca它不仅仅是静态大小的数组的创建者)?
Ruslan

有第二个用例,第一个不支持。在运行“ n”次循环后,我可能想拥有“ n”条记录-可能在链表或树中;然后,当函数最终返回时,将丢弃此数据结构。这并不是说我会这样编码:-)
greggo,

1
我会说“编译器无法控制它”是因为这就是alloca()的定义方式;现代编译器知道什么是alloca,并对其进行特殊处理;它不只是像80年代那样的库函数。C99 VLA基本上是具有块范围(以及更好的键入)的alloca。没有更多或更少的控制,只是遵循不同的语义。
greggo

@greggo:如果您是反对者,我很高兴听到您为什么认为我的回答没有用。
alecov

在C语言中,回收不是编译器的任务,而是c库(free())的任务。alloca()在返回时被释放。
peterh-恢复莫妮卡

1

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.