首页 文章

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

提问于
浏览
337

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

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

24 回答

  • 4

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

    char arr[size];
    

    代替

    char *arr=alloca(size);
    

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

  • 0

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

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

    哪个不是滥用它( alloca 'ing huge values), it'很好 . 一旦你超过了"few hundred bytes"标记,现在是时候使用 malloc 和朋友了 . 你可能仍然会遇到分配失败,但至少你会有一些失败的迹象而不只是吹灭堆栈 .

  • 9

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

    在头文件中:

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

    在实现文件中:

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

    所以发生的事情是编译器内联 DoSomething 函数并且所有堆栈分配都发生在 Process() 函数内部,从而使堆栈向上爆炸 . 在我的辩护中(我没有修复它),它不是直接 alloca ,它是ATL字符串转换宏之一 .

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

  • 174

    如果你不能使用标准局部变量,alloca()非常有用,因为它的大小需要在运行时确定,你可以 absolutely guarantee that the pointer you get from alloca() will NEVER be used after this function returns .

    如果你,你可以相当安全

    • 不返回指针或包含它的任何内容 .

    • 不将指针存储在堆上分配的任何结构中

    • 不要让任何其他线程使用指针

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

  • 25

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

    • 并非所有编译器都支持 alloca .

    • 有些编译器以不同的方式解释 alloca 的预期行为,因此即使在支持它的编译器之间也无法保证可移植性 .

    • 有些实现是错误的 .

  • 61

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

  • 51

    劝阻仍然使用alloca,为什么?

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

    • C99提供可变长度数组,这些数组通常会优先使用,因为符号与固定长度数组更加一致且直观整体

    • 许多系统的堆栈总可用内存/地址空间少于堆,这使得程序稍微容易受到内存耗尽的影响(通过堆栈溢出):这可能被视为好事或坏事 - 堆栈不会像堆一样自动增长的原因之一是防止失控程序对整个机器产生同样多的负面影响

    • 在更局部范围(例如 whilefor 循环)或多个范围中使用时,内存会累积每次迭代/范围,并且在函数退出之前不会释放:这与控件范围中定义的正常变量形成对比结构(例如 for {int i = 0; i < 2; ++i) { X } 将累积在X请求的 alloca -ed内存,但每次迭代将回收固定大小数组的内存) .

    • 现代编译器通常不会 inline 调用 alloca ,但是如果你强制它们那么 alloca 将在调用者中发生' context (i.e. the stack won'释放,直到调用者返回)

    • 很久以前 alloca 从非便携功能/黑客转变为标准化扩展,但一些负面看法可能会持续存在

    • 生命周期绑定到函数范围,这可能适合或不适合程序员比 malloc 的显式控制更好

    • 不得不使用 malloc 鼓励考虑解除分配 - 如果通过包装函数(例如 WonderfulObject_DestructorFree(ptr) )进行管理,那么该函数提供了一个实现清理操作的点(如关闭文件描述符,释放内部指针或执行一些日志记录)而无需显式对客户端代码的更改:有时它是一个很好的模型,可以一致地采用

    • 在这种伪OO编程风格中,很自然地需要类似 WonderfulObject* p = WonderfulObject_AllocConstructor(); 的东西 - 当"constructor"是一个返回 malloc -ed内存的函数时(由于在函数返回值存储在 p 之后内存仍然分配),这是可能的,但如果"constructor"使用 alloca 则不行

    • WonderfulObject_AllocConstructor 的宏版本可以实现这一点,但是"macros are evil"因为它们可以相互冲突并且非宏代码并且产生意外的替换以及随之而来的难以诊断的问题

    ValGrind,Purify等可以检测到

    • 缺失的 free 操作,但是根本无法检测到"destructor"次呼叫 - 在执行预期用途方面有一个非常微弱的好处;一些 alloca() 实现(例如GCC)对 alloca() 使用内联宏,因此不可能像 malloc / realloc / free (例如电围栏)那样对内存使用诊断库进行运行时替换 .

    • 某些实现存在细微问题:例如,来自Linux联机帮助页:

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


    我知道这个问题被标记为C,但作为一名C程序员,我认为我会使用C来说明 alloca 的潜在效用:下面的代码(和here at 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
        }
    }
    
  • 4

    所有其他答案都是正确的 . 但是,如果你想使用 alloca() 分配的东西相当小,我认为它比使用 malloc() 或其他方式更快更方便 .

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

  • 9

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

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

    我建议通过包装alloca并在内部跟踪来限制最大分配大小 . 如果你真的是硬核的话,你可以在你的函数顶部抛出一些范围的哨兵来跟踪函数范围内的任何alloca分配,并且理智地检查你的项目所允许的最大数量 .

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

  • 3

    原因如下:

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

    并不是说有人会写这个代码,但是你传递给 alloca 的大小参数几乎肯定来自某种输入,这可能会恶意地使你的程序变得像这样巨大的东西 . 毕竟,如果大小有可能变大,为什么不直接声明一个小的,固定大小的本地缓冲区呢?

    几乎所有使用 alloca 和/或C99 vlas的代码都有严重的错误会导致崩溃(如果你没那么幸运) .

  • 197

    alloca () 很好而且效率很高......但它也深受打击 .

    • 破坏范围行为(功能范围而不是块范围)

    • 与malloc使用不一致( alloca() -ted指针不应该被释放,因此你必须跟踪指针来自哪里 free() 只有那些你用 malloc() 获得的指针)
      当你也使用内联时,

    • 不良行为(范围有时会转到调用者函数,具体取决于被调用者是否被内联) .

    • 没有堆栈边界检查
      在失败的情况下

    • 未定义的行为(不像malloc那样返回NULL ......失败意味着什么,因为它不检查堆栈边界......)

    • 不是ansi标准

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

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

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

  • -1

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

    如果正确使用并小心使用,一致使用alloca()(可能是应用程序范围的)来处理小的可变长度分配(或C99 VLA,如果可用)可以导致整体堆栈增长低于使用超大本地的其他等效实现固定长度的数组 . 因此,如果你仔细使用它,alloca()可能对你的堆栈有好处 .

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

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

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

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

    我已经一次又一次地看到程序员创建堆栈分配的缓冲区"big enough to handle any possible case" . 在深度嵌套的调用树中,重复使用该(反 - ?)模式会导致堆栈使用过度 . (想象一下一个20级深度的调用树,在每个级别出于不同的原因,该函数盲目地过度分配1024字节的缓冲区"just to be safe"一般它只会使用16个或更少的缓冲区,并且只在极少数情况下可能会使用更多 . )另一种方法是使用 alloca() 或VLA并仅分配与函数需要一样多的堆栈空间,以避免不必要地增加堆栈负担 . 希望当调用树中的一个函数需要大于正常的分配时,调用树中的其他函数仍然使用它们的正常小分配,并且整个应用程序堆栈的使用量明显少于每个函数盲目地过度分配本地缓冲区的情况 . .

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

    基于此页面上的其他答案,似乎VLA应该是安全的(他们不要使用 alloca() ,小心不要在循环中使用它,并确保您的函数可以在任何其他函数中调用它的任何机会)环 .

  • 9

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

  • 21

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

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

    • malloc() 类似,它会在失败时返回一个无效指针,这将在具有MMU的现代系统上进行段错误(并希望重新启动那些没有) .

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

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

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

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

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

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

    3.2.5.2 Advantages of alloca

  • 4

    如果你不小心写了超出 alloca 分配的块(例如由于缓冲区溢出),那么你将覆盖函数的返回地址,因为它位于堆栈上的"above",即在你分配的块之后 .

    _alloca block on the stack

    这样做的结果是双重的:

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

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

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

  • 11

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

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

    鉴于其使用的罕见性以及作为标准函数的阴暗状态,编译器设计者很可能禁用任何可能导致alloca出现问题的优化,如果需要花费一点点努力才能使其与alloca一起使用 .

  • 4

    alloca 的一个陷阱是 longjmp 倒带它 .

    也就是说,如果使用 setjmp 保存上下文,然后 alloca 某些内存,然后 longjmp 到上下文,则可能会丢失 alloca 内存(没有任何通知) . 堆栈指针返回原处,因此不再保留内存;如果你调用一个函数或做另一个 alloca ,你将破坏原始的 alloca .

    为了澄清,我在这里具体指的是 longjmp 没有退回 alloca 发生的功能的情况!而是一个函数用 setjmp 保存上下文;然后用 alloca 分配内存,最后在该上下文中发生longjmp . 该函数的 alloca 内存并未全部释放;只是自 setjmp 以来它分配的所有内存 . 当然,我说的是观察到的行为;我所知道的任何_1690487都没有记录这样的要求 .

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

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

    我遇到过这个问题是导致bug的根本原因 .

  • 7

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

    通过使用 alloca() ,您可以大大增加获得堆栈溢出错误的机会(如果您没有这样做) .

  • 2

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

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

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

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

  • 0

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

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

    我使用alloca主要是为了使我的普通C文件可以在msvc和gcc上编译而不做任何改动,C89风格,没有#ifdef _MSC_VER等 .

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

  • 0

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

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

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

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

    为什么这么糟糕?

  • 2

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

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

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

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

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

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

  • 11

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

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

    相比:

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

    有:

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

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

相关问题