关于C#:为什么使用alloca()不被视为良好做法?

Why is the use of alloca() not considered good practice?

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

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


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

RETURN VALUE
The alloca() function returns a pointer to the beginning of the
allocated space. If the
allocation causes
stack overflow, program behaviour is undefined.

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


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

在头文件中:

1
2
3
4
void DoSomething() {
   wchar_t* pStr = alloca(100);
   //......
}

在实现文件中:

1
2
3
4
5
void Process() {
   for (i = 0; i < 1000000; i++) {
     DoSomething();
   }
}

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

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


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

1
char arr[size];

代替

1
char *arr=alloca(size);

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


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

如果你,你可以相当安全

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

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


正如本新闻组发布中所述,使用alloca可能被认为是困难和危险的原因有几个:

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


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


still alloca use is discouraged, why?

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

  • 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的宏版本可以实现这一点,但"宏是邪恶的",因为它们可以相互冲突,非宏代码并产生意外的替换和随之而来的难以诊断的问题
    • ValGrind,Purify等可以检测到缺少的free操作,但是根本无法检测到缺少"析构函数"的调用 - 在执行预期用法方面的一个非常微弱的好处;一些alloca()实现(例如GCC)对alloca()使用内联宏,因此不能像malloc / realloc / free那样对内存使用诊断库进行运行时替换(例如电围栏)
  • 一些实现有微妙的问题:例如,从Linux手册页:

    On many systems alloca() cannot be used inside the list of arguments of a function call, because the stack space reserved by alloca() would appear on the stack in the middle of the space for the function arguments.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include
#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() << '
'
;
        (*i)->~Base();   // optionally / else Undefined Behaviour iff the
                         // program depends on side effects of destructor
    }
}


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

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


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

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

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

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


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

When used properly and with care, consistent use of alloca()
(perhaps application-wide) to handle small variable-length allocations
(or C99 VLAs, where available) can lead to lower overall stack
growth than an otherwise equivalent implementation using oversized
local arrays of fixed length. So alloca() may be good for your stack if you use it carefully.

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

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

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

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

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

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

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


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

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

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

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

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


原因如下:

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

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

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


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


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

_alloca block on the stack

这样做的结果是双重的:

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

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

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


    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会阻碍或禁用一些可能在函数中应用的优化,因为编译器无法知道函数堆栈帧的大小。

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

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

    更新:
    由于可变长度的本地数组已添加到C和C ++中,并且由于这些数组向编译器提供与alloca非常相似的代码生成问题,因此我发现"使用稀有和阴暗状态"不适用于底层机制;但我仍然怀疑使用alloca或VLA往往会破坏使用它们的函数中的代码生成。我欢迎来自编译器设计者的任何反馈。


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

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

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

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

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

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

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

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

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

    3.2.5.2 alloca的优点


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

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

    1
    2
    3
    4
    5
    6
    7
    void f()
    {
        char array_on_stack[ MAX_BYTES_TO_ALLOCATE ];
        SomeType *p = (SomeType *)array;

        (...)
    }


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


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

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


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

    1
    2
    3
    4
    5
    6
    void foo()
    {
        int x = 50000;
        char array[x];
        char *parray = (char *)alloca(x);
    }

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

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

    为什么这么糟糕?


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

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

    • https://unix.stackexchange.com/questions/63742/what-is-automatic-stack-expansion
    • Linux 32位计算机上程序的堆栈分配限制
    • ulimit -s

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

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


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

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

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

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

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

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


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

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


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

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

    相比:

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

    有:

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

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