为什么使用 alloca() 不被认为是好的做法?

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

提问人:Vaibhav 提问时间:6/20/2009 最后编辑:Matteo UgolottiVaibhav 更新时间:7/18/2023 访问量:190297

问:

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

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

C 堆栈 malloc 分配 alloca

评论

53赞 merkuro 6/20/2009
只是一个简短的说明。尽管在大多数编译器中都可以找到此函数,但 ANSI-C 标准并不要求它,因此可能会限制可移植性。另一件事是,你不能!free() 你得到的指针,它会在你退出函数后自动释放。
12赞 Justicle 6/23/2009
此外,如果声明为 alloca() 的函数,则不会内联。
2赞 migajek 8/2/2010
@Justicle,你能提供一些解释吗?我很好奇这种行为背后的原因
69赞 valdo 11/2/2011
忘记所有关于可移植性的噪音,无需调用(这显然是一个优势),无法内联它(显然堆分配非常重)等等。避免的唯一原因是大尺寸。也就是说,浪费大量堆栈内存不是一个好主意,而且您有可能发生堆栈溢出。如果是这种情况 - 考虑使用freeallocamalloca / freea
14赞 Andreas 5/17/2018
另一个积极的方面是堆栈不能像堆一样碎片化。这对于硬实时永久运行风格的应用程序,甚至是安全关键型应用程序都很有用,因为 WCRU 可以进行静态分析,而无需诉诸具有自身一系列问题的自定义内存池(没有时间局部性,资源使用次优)。alloca

答:

335赞 Sean Bright 6/20/2009 #1

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

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

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

评论

49赞 T.E.D. 6/20/2009
所以它真的没有问题,你不会在声明大型数组时遇到吗?
126赞 j_random_hacker 6/27/2010
@Sean:是的,堆栈溢出风险是给出的原因,但这个原因有点愚蠢。首先,因为(正如 Vaibhav 所说)大型本地数组会导致完全相同的问题,但并没有那么被诽谤。此外,递归也可以很容易地破坏堆栈。对不起,我 -1 希望能反驳普遍存在的观点,即手册页中给出的理由是合理的。
61赞 j_random_hacker 6/30/2010
我的观点是,手册页中给出的理由毫无意义,因为 alloca() 与那些被认为是犹太教的其他东西(局部数组或递归函数)一样“糟糕”。
52赞 j_random_hacker 8/2/2010
@ninjalj:不是由经验丰富的 C/C++ 程序员编写的,但我确实认为许多害怕的人对局部数组或递归没有同样的恐惧(事实上,许多会大喊大叫的人会称赞递归,因为它“看起来很优雅”)。我同意 Shaun 的建议(“alloca() 适合小额分配”),但我不同意将 alloca() 视为 3 个中唯一邪恶的心态——它们同样危险!alloca()alloca()
43赞 Jeremy Friesner 7/28/2013
注意:鉴于 Linux 的“乐观”内存分配策略,您很可能不会得到任何堆耗尽失败的迹象......相反,malloc() 将返回一个不错的非 NULL 指针,然后当您尝试实际访问它指向的地址空间时,您的进程(或其他一些进程,不可预测)将被 OOM 杀手杀死。当然,这是 Linux 的“功能”,而不是 C/C++ 本身的问题,但在争论 alloca() 或 malloc() 是否“更安全”时要记住这一点。:)
7赞 RichieHindle 6/20/2009 #2

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

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

评论

5赞 EBlake 3/5/2020
这很大程度上取决于应用程序。内存受限的嵌入式应用程序的堆栈大小大于堆(如果甚至存在堆)并不罕见。
46赞 FreeMemory 6/20/2009 #3

正如此新闻组帖子中所指出的,有几个原因可以认为使用困难和危险:alloca

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

评论

30赞 j_random_hacker 6/27/2010
我在该链接上看到的一件事是,该链接没有在此页面上的其他地方提及,即使用需要单独的寄存器来保存堆栈指针和帧指针的函数。在 x86 CPU >= 386 上,堆栈指针可用于两者,释放 -- 除非使用。alloca()ESPEBPalloca()
16赞 j_random_hacker 6/27/2010
该页面上的另一个优点是,除非编译器的代码生成器将其作为特殊情况进行处理,否则可能会崩溃,因为在至少一个参数被推送到堆栈指针上后,堆栈指针可能会被调整。f(42, alloca(10), 43);alloca()
4赞 user318904 8/13/2011
链接的帖子似乎是由约翰·莱文(John Levine)撰写的——那个写了“链接器和加载器”的家伙,无论他说什么,我都会相信。
3赞 A. Wilcox 12/29/2014
链接的帖子是对约翰·莱文(John Levine)帖子的回复
14赞 greggo 5/3/2017
请记住,自 1991 年以来发生了很多变化。所有现代 C 编译器(即使在 2009 年)都必须将 alloca 作为特殊情况处理;它是一个内部函数,而不是一个普通函数,甚至可能不调用函数。因此,参数分配问题(从1970年代开始在K&R C中出现)现在应该不是问题。更多细节,请参阅我对 Tony D 回答的评论
19赞 JSBձոգչ 6/20/2009 #4

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

换句话说,是危险的,并且可能导致溢出,就像现在一样。谨慎和合理,你会没事的。alloca( 0x00ffffff )char hugeArray[ 0x00ffffff ];

32赞 David Thornley 6/20/2009 #5

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

76赞 Arthur Ulfeldt 6/23/2009 #6

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

如果你

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

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

评论

19赞 Jonathan Leffler 6/23/2009
C99 的 VLA(可变长度数组)功能支持动态调整大小的局部变量,而无需明确要求使用 alloca()。
2赞 Arthur Ulfeldt 6/23/2009
尼托!在 programmersheaven.com/2/Pointers-and-Arrays-page-2 的“3.4 可变长度数组”一节中找到更多信息
3赞 glglgl 10/18/2012
但这与使用指向局部变量的指针进行处理没有什么不同。他们也可以被愚弄......
3赞 Piotr Lopusiewicz 10/2/2013
@Jonathan Leffler,你可以用 alloca 做但你不能用 VLA 做的一件事就是对它们使用 restrict 关键字。像这样: float* restrict heavily_used_arr = alloca(sizeof(float)*size);而不是 float heavily_used_arr[size]。它可能有助于某些编译器(在我的情况下是 gcc 4.8)优化程序集,即使 size 是一个编译常量。请看我的问题:stackoverflow.com/questions/19026643/using-restrict-with-arrays
0赞 Pascal Cuoq 3/24/2014
@JonathanLeffler VLA 是包含它的块的本地变量。另一方面,分配持续到函数结束的内存。这意味着似乎没有直接、方便的 VLA 转换。如果您认为可以自动将 VLA 的用途转换为用途,但需要的不仅仅是注释来描述如何,我可以提出这个问题。alloca()f() { char *p; if (c) { /* compute x */ p = alloca(x); } else { p = 0; } /* use p */ }alloca
10赞 R.. GitHub STOP HELPING ICE 8/2/2010 #7

原因如下:

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

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

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

评论

1赞 GManNickG 5/26/2011
世界可能永远不会知道。:(也就是说,我希望你能澄清我关于.你说几乎所有使用它的代码都有一个错误,但我打算使用它;我通常会忽略这样的说法,但从你那里,我不会。我正在编写一个虚拟机,我想分配不会从堆栈上的函数中转义的变量,而不是动态的,因为速度大大加快了。是否有具有相同性能特征的替代方法?我知道我可以接近内存池,但这仍然不那么便宜。你会怎么做?alloca
7赞 R.. GitHub STOP HELPING ICE 11/17/2011
*0=9;无效 C。至于测试你传递到的尺寸,测试它是什么?没有办法知道限制,如果你只是要针对一个微小的固定已知安全大小(例如 8k)进行测试,你不妨在堆栈上使用一个固定大小的数组。alloca
7赞 j_random_hacker 4/16/2012
在我看来,你的“要么已知大小足够小,要么它依赖于输入,因此可能任意大”的论点的问题在于,它同样适用于递归。一个实际的折衷方案(对于这两种情况)是假设如果大小受到限制,那么我们可能有足够的内存。small_constant * log(user_input)
1赞 R.. GitHub STOP HELPING ICE 4/16/2012
事实上,您已经确定了 VLA/alloca 有用的一种情况:递归算法,其中任何调用帧所需的最大空间可能与 N 一样大,但所有递归级别所需的空间总和为 N 或 N 的某些函数不会快速增长。
1赞 bestsss 6/11/2014
没有办法知道限制 -- Stack 可以显式设置[1],所以可以知道,只是不是很实用。1 - pthread_attr_setstack
108赞 Patrick Schlüter 8/2/2010 #8

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

char arr[size];

而不是

char *arr=alloca(size);

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

评论

7赞 ninjalj 8/2/2010
乔纳森·莱夫勒(Jonathan Leffler)在对亚瑟·乌尔费尔特(Arthur Ulfeldt)的回答的评论中提到了这一点。
5赞 Patrick Schlüter 8/2/2010
确实如此,但它也表明它是多么容易被错过,因为尽管我在发布之前阅读了所有回复,但我还没有看到它。
10赞 Tim Čas 12/10/2012
需要注意的是,这些是可变长度的数组,而不是动态数组。后者是可调整大小的,通常在堆上实现。
6赞 Cristian Ciupitu 1/10/2019
Linus Torvalds 不喜欢 Linux 内核中的 VLA。从 4.20 版本开始,Linux 应该几乎没有 VLA。
6赞 opt12 9/19/2019
不幸的是,有些编译器(Keil for ARM)通过将 malloc() 静默链接到代码中来实现堆上的 VLA。这是一个很大的烦恼,因为你永远不知道内存分配在哪里,我不知道他们之后是否正确地调用了free()。
268赞 Igor Zevaka 8/5/2010 #9

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

在头文件中:

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

在实现文件中:

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

因此,编译器(Microsoft VC++ 6)内联函数和所有堆栈分配都发生在函数内部,从而炸毁了堆栈。在我的辩护中(我不是发现问题的人;当我无法修复它时,我不得不去向一位高级开发人员哭泣),它不是直的,它是 ATL 字符串转换宏之一。DoSomethingProcess()alloca

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

评论

146赞 sleske 10/18/2010
有趣。但这难道不算是编译器错误吗?毕竟,内联改变了代码的行为(它延迟了使用 alloca 分配的空间的释放)。
86赞 sleske 10/18/2010
显然,至少 GCC 会考虑到这一点:“请注意,函数定义中的某些用法可能使其不适合内联替换。这些用法包括:使用 varargs、使用 alloca、[...]”。gcc.gnu.org/onlinedocs/gcc/Inline.html
183赞 Thomas Eding 11/17/2011
你抽的是什么编译器?
27赞 moala 5/16/2012
我不明白的是为什么编译器没有充分利用作用域来确定子作用域中的分配或多或少是“释放”的:堆栈指针可以在进入作用域之前返回到其点,就像从函数返回时所做的那样(不是吗?
28赞 Evan Carroll 9/7/2018
我已经投了反对票,但答案写得很好:我同意其他人的观点,你指责 alloca 显然是一个编译器错误。编译器在它不应该做的优化中做出了错误的假设。解决编译器错误很好,但除了编译器之外,我不会责怪任何东西。
7赞 Sondhi Chakraborty 11/28/2010 #10

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

评论

8赞 Todd Lehman 8/26/2015
alloca()并不比某个任意整数更危险。int foo[bar];bar
3赞 Chris Down 4/25/2020
@ToddLehman 没错,正是出于这个原因,我们已经禁止了内核中的 VLA 好几年了,并且自 2018 年以来一直没有 VLA :-)
14赞 SilentDirge 3/22/2011 #11

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

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

我建议通过包装 alloca 并在内部跟踪它来限制您的最大分配大小。如果你真的对它很执着,你可以在函数的顶部抛出一些范围哨兵,以跟踪函数范围中的任何分配,并健全地检查它是否与你的项目允许的最大数量。

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

评论

1赞 12431234123412341234123 11/3/2018
问题是,这可能需要太多的空间,以至于堆栈指针落在堆中。这样一来,可以控制缓冲区的大小和进入该缓冲区的数据的攻击者可以覆盖堆(这是非常糟糕的)。alloca()alloca()
1赞 George 6/11/2020
SEH 是仅限 Windows 的东西。如果你只关心在 Windows 上运行的代码,那就太好了,但如果你的代码需要跨平台(或者如果你编写的代码只能在非 Windows 平台上运行),那么你就不能依赖 SEH。
5赞 zagam 3/30/2011 #12

可悲的是,真正令人敬畏的 tcc 中缺少了真正令人敬畏的东西。GCC 确实有 .alloca()alloca()

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

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

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

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

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

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

要使用,我使用全局变量并将它们分配给 NULL。如果指针不是 NULL,我会在使用 .malloc()malloc()

如果要复制任何现有数据,也可以将其用作一般情况。您需要在之前检查指针,以确定是否要在 .realloc()realloc()

3.2.5.2 alloca 的优点

评论

5赞 kriss 9/2/2014
实际上,alloca 规范并没有说它在失败(堆栈溢出)时返回一个无效的指针,而是说它有未定义的行为......对于 malloc,它说它返回 NULL,而不是随机的无效指针(好吧,Linux 乐观内存实现使它变得无用)。
0赞 craig65535 10/6/2017
@kriss Linux 可能会扼杀你的进程,但至少它不会冒险进入未定义的行为
0赞 kriss 10/7/2017
@craig65535:表达式未定义的行为通常意味着该行为不是由 C 或 C++ 规范定义的。在任何给定的操作系统或编译器上,它都不会是随机的或不稳定的。因此,将 UB 与“Linux”或“Windows”等操作系统的名称相关联是没有意义的。它与它无关。
0赞 craig65535 10/8/2017
我想说的是,malloc 返回 NULL,或者在 Linux 的情况下,内存访问会杀死您的进程,这比 alloca 的未定义行为更可取。我想我一定误读了你的第一条评论。
27赞 Tony Delroy 2/26/2013 #13

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

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

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

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


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

#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
    }
}

评论

2赞 einpoklum 7/18/2015
让我重新表述一下:这是一个很好的答案。直到我认为你是在建议人们使用一种反模式。
1赞 greggo 5/3/2017
linux 手册页中的评论非常古老,我敢肯定,已经过时了。所有现代编译器都知道 alloca() 是什么,并且不会像那样被鞋带绊倒。在旧的 K&R C 中,(1) 所有函数都使用帧指针 (2) 所有函数调用都是 {stack 上的推送参数}{call func}{add #n,sp}。alloca 是一个 lib 函数,它只会提升堆栈,编译器甚至不知道这种情况的发生。(1) 和 (2) 不再是真的,所以 alloca 不能以这种方式工作(现在它是一个内在的)。在旧 C 中,在推送 args 的过程中调用 alloca 显然也会打破这些假设。
9赞 greggo 5/3/2017
关于这个例子,我通常会担心一些需要always_inline来避免内存损坏的事情。
1赞 Joshua 9/25/2019
使用 placement new 返回 alloca 缓冲区。如果函数最终未内联,则删除堆栈。您的代码未定义。
1赞 Noone AtAll 7/22/2020
如果有人要阅读这篇文章:用于在堆栈上分配的现代 C++ 样式是通过分配器 - 创建它并使所有向量和“新”通过它定向
9赞 kriss 9/2/2014 #14

Alloca()很好,很高效......但它也被深深地打破了。

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

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

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

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

评论

0赞 gd1 2/26/2015
我看不出投反对票的原因(顺便说一句,没有任何评论)
1赞 curiousguy 10/27/2015
只有名称才有作用域。 不创建名称,只创建内存范围,该范围具有生存期alloca
0赞 kriss 10/27/2015
@curiousguy:你只是在玩文字游戏。对于自动变量,我还可以说底层内存的生命周期,因为它与名称的范围相匹配。无论如何,问题不在于我们如何称呼它,而在于 alloca 返回的内存生命周期/范围的不稳定性以及异常行为。
4赞 supercat 4/1/2016
我希望 alloca 有一个相应的“freea”,并规定调用“freea”将撤消创建对象和所有后续对象的“alloca”的影响,并要求在功能中“分配”的存储也必须在其中“freea”。这将使几乎所有的实现都能够以兼容的方式支持 alloca/freea,从而缓解内联问题,并且通常使事情变得更加干净。
4赞 Todd Lehman 4/1/2016
@supercat——我也希望如此。出于这个原因(以及更多原因),我使用抽象层(主要是宏和内联函数),这样我就不用直接调用或调用。我说像和.所以,只是打电话,是一个空操作。这样一来,堆栈分配可以很容易地降级为堆分配,并且意图也更加清晰。allocamallocfree{stack|heap}_alloc_{bytes,items,struct,varstruct}{stack|heap}_deallocheap_deallocfreestack_dealloc
8赞 rustyx 2/17/2016 #15

alloca并不比可变长度数组 (VLA) 差,但比在堆上分配风险更大。

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

_alloca block on the stack

这样做的后果是双重的:

  1. 程序将严重崩溃,并且无法判断崩溃的原因或位置(由于覆盖的帧指针,堆栈很可能会展开到随机地址)。

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

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

22赞 phonetagger 4/1/2016 #16

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

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

我在......好吧,我编造了这句话。但真的,想想看......

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

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

alloca()(或 VLA)可能正是完成这项工作的正确工具。

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

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

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

评论

3赞 Felipe Tonello 2/2/2017
我同意这一点。危险是真的,但内存泄漏也是如此(那为什么不使用 GC?有人可能会争辩说)。 如果小心使用,对于减小堆栈大小非常有用。alloca()malloc()alloca()
1赞 tehftw 9/17/2018
不使用动态内存的另一个很好的理由,尤其是在嵌入式内存中:它比坚持堆栈更复杂。使用动态内存需要特殊的过程和数据结构,而在堆栈上,它(为了简化)是从堆栈指针中添加/减去一个更大的数字的问题。
0赞 Katastic Voyage 6/23/2019
旁注:“使用固定缓冲区[MAX_SIZE]”示例强调了为什么过量使用内存策略如此有效。程序分配的内存可能永远不会触及,除非在其缓冲区长度的限制下。因此,Linux(和其他操作系统)在首次使用之前实际上不会分配内存页面(而不是malloc'd),这很好。如果缓冲区大于一页,则程序可能只使用第一页,而不会浪费其余的物理内存。
1赞 phonetagger 9/6/2019
@KatasticVoyage 除非MAX_SIZE大于(或至少等于)系统的虚拟内存页面大小,否则您的论点是站不住脚的。此外,在没有虚拟内存的嵌入式系统上(许多嵌入式 MCU 没有 MMU),从“确保程序在所有情况下都能运行”的角度来看,过量分配内存策略可能很好,但这种保证伴随着代价,即您的堆栈大小也必须分配以支持该过量分配内存策略。在一些嵌入式系统上,这是一些低成本产品制造商不愿意支付的代价。
19赞 greggo 11/5/2016 #17

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

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

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

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

评论

2赞 Kaz 6/13/2020
> 我仍然怀疑使用 alloca 或 VLA 往往会损害代码生成我认为使用 alloca 需要一个帧指针,因为堆栈指针的移动方式在编译时并不明显。可以在循环中调用 alloca 以继续获取更多堆栈内存,或者使用运行时计算的大小等。如果有帧指针,则生成的代码具有对局部变量的稳定引用,并且堆栈指针可以执行任何操作;它不被使用。
7赞 Kaz 7/9/2020
如果 VLA 或 alloca 的替代方法是调用 和 ,则该函数的使用效率可能更高,即使它需要成帧。mallocfree
2赞 Daniel Glasser 12/13/2016 #18

在我看来,alloca()(如果可用)应该只以受约束的方式使用。就像使用“goto”一样,相当多的理性人不仅对 alloca() 的使用,而且对 alloca() 的存在都深恶痛绝。

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

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

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

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

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

3赞 user7491277 1/31/2017 #19

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

评论

1赞 Kaz 9/18/2018
听起来好像 gcc-2.95 破坏了 alloca,可能无法安全地用于需要 .当用于放弃这样做的帧时,它将如何清理内存?今天什么时候会有人使用 gcc 2.95?allocalongjmpalloca
-1赞 alecov 4/28/2017 #20

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

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

比较:

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

跟:

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

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

评论

0赞 Ruslan 4/29/2017
你有没有任何实际的例子来证明VLA和(是的,我确实说VLA,因为它不仅仅是静态大小数组的创建者)之间的区别?allocaalloca
0赞 greggo 1/19/2018
第二个用例,第一个不支持。我可能希望在循环运行“n”次后拥有“n”个记录 - 可能在链接列表或树中;然后,当函数最终返回时,将释放此数据结构。这并不是说我会以这种方式编写任何代码:-)
2赞 greggo 1/19/2018
我会说“编译器无法控制它”是因为这就是 alloca() 的定义方式;现代编译器知道 Alloca 是什么,并特别对待它;它不仅仅是像 80 年代那样的库函数。C99 VLA 基本上是具有块范围(和更好的类型)的 alloca。没有或多或少的控制,只是符合不同的语义。
0赞 alecov 1/19/2018
@greggo:如果你是反对者,我很乐意听到你为什么认为我的答案没有用。
0赞 peterh 12/10/2019
在C中,回收不是编译器的任务,而是c库的任务(free())。alloca() 在返回时被释放。
12赞 Kaz 2/26/2018 #21

一个陷阱是它倒带了它。allocalongjmp

也就是说,如果用 保存一个上下文,然后是一些内存,然后保存到上下文中,你可能会丢失内存。堆栈指针已返回到原来的位置,因此不再保留内存;如果你调用一个函数或执行另一个函数,你将破坏原来的。setjmpallocalongjmpallocaallocaalloca

澄清一下,我在这里特别指的是一种情况,即不会从发生函数的函数中返回!相反,函数使用 ;然后分配内存,最后向该上下文进行 longjmp。该函数的内存并未全部释放;只是自 .当然,我说的是观察到的行为;据我所知,没有这样的要求。longjmpallocasetjmpallocaallocasetjmpalloca

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

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

我遇到了这是错误的根本原因。

评论

7赞 tehftw 9/17/2018
这就是它应该做的 - 返回并使其程序忘记堆栈中发生的所有事情:所有变量、函数调用等。并且就像堆栈上的一个数组一样,因此预计它们会像堆栈上的其他所有内容一样被破坏。longjmpalloca
6赞 tehftw 9/17/2018
man alloca给出了以下句子: “因为 alloca() 分配的空间是在堆栈帧中分配的,所以如果通过调用 longjmp(3) 或 siglongjmp(3) 跳过函数返回,则会自动释放该空间。因此,据记载,分配的内存在 .allocalongjmp
1赞 Kaz 9/17/2018
@tehftw 所描述的情况是,函数返回值没有被 跳过。目标函数尚未返回。它已经完成了,然后.可能会将状态倒退到当时的状态。也就是说,移动的指针与未标记的局部变量存在相同的问题!longjmpsetjmpallocalongjmplongjmpallocasetjmpallocavolatile
7赞 tehftw 9/18/2018
我不明白为什么它应该是出乎意料的。当你 then , 然后 , 这是正常的,会倒带。重点是回到保存的状态!setjmpallocalongjmpallocalongjmpsetjmp
1赞 tehftw 9/18/2018
man alloca记录了这种互动。如果我使用 ,我个人会依赖这种交互,因为它是记录的。您阅读了哪些 alloca 文档,而那里没有记录?allocalongjmp
2赞 Yongwei Wu 7/22/2020
在我看来,它证明了/是危险的野兽。setjmplongjmp
1赞 Dyllon Gagnier 12/31/2018 #22

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

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

2赞 Rick 8/6/2021 #23

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

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

非本地退出自动完成(请参阅非本地退出) 释放他们通过 调用 的函数。这是使用 alloca 的最重要原因longjmpallocaalloca

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

  1. https://www.gnu.org/software/libc/manual/html_node/Advantages-of-Alloca.html
  2. 非本地出口的介绍详细信息
  3. Alloca 示例
0赞 plugwash 9/18/2021 #24

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

堆栈上的分配通常不签入用户代码。

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

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

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

评论

0赞 Sapphire_Brick 1/27/2022
我听过很多次这样的解释,但这对我来说没有任何意义。调用任何函数都可能“以非常危险的方式失败”,尤其是当该函数是递归函数或使用大量堆栈内存时。
0赞 plugwash 1/27/2022
递归函数(每个递归级别使用正常数量的堆栈空间)将逐渐分配堆栈空间,因此它将命中保护页面并触发堆栈扩展或堆栈溢出处理。
0赞 plugwash 1/27/2022
在堆栈上分配固定大量空间的函数确实是一种风险,但它仍然比 alloca/VLA 风险小,因为如果它会导致内存冲突,它可能会在开发过程中导致它们。
0赞 plugwash 1/27/2022
而使用 alloca/VLA 时,您可以有一个函数,该函数通常在堆栈上分配适当的空间量,但攻击者可以操纵该函数以访问堆栈任意偏移的位置。