在编译时检查堆栈使用情况

35

在C语言中,有没有一种方法可以在编译时知道并输出函数所需的堆栈大小?

这是我想知道的:

假设我们有一个函数:

void foo(int a) {
    char c[5];
    char * s;
    //do something
    return;
}

编译此函数时,我想知道它被调用时会消耗多少堆栈空间。这可能有助于检测隐藏大缓冲区的结构体在堆栈上声明的情况。

我正在寻找类似以下打印信息的东西:

文件foo.c:函数foo使用的堆栈大小为n字节

是否有一种方法可以不查看生成的汇编代码来实现这一点?或者可以设置编译器的限制吗?

更新:我不是要避免给定进程的运行时堆栈溢出,我正在寻找一种在运行时之前查找函数堆栈使用情况的方法,就像编译过程的输出一样。

换句话说:是否有可能知道函数中所有本地对象的大小? 我猜编译器优化不会帮我忙,因为某些变量将会消失,但一个最高限度是可以接受的。


如果你在想,我输入了秘密的“}”字符。 - 1800 INFORMATION
这个问题对我来说不太清楚。我猜想如果您能更多地写一些关于为什么想知道这个问题以及为什么检查反汇编或可执行文件(这是检查编译器输出最简单的方法)不可接受,也许有人可以找到一些简单的解决方案? - Suma
7个回答

13

Linux内核代码在x86上运行时使用4K堆栈,因此开发者非常关注这个问题。他们用来检查这一点的工具是一个由他们编写的perl脚本,你可以在最新的内核tarball中找到它(2.6.25版本已经包含了该脚本)。它依赖于objdump的输出结果,使用文档在初始注释中有说明。

我想很久之前就已经将它用于用户空间二进制文件,并且如果你懂一点perl编程,如果出现了问题,很容易修复。

无论如何,它的基本功能是自动查看GCC的输出。内核黑客编写这样一个工具的事实意味着没有静态的方法可以在GCC中完成此操作(或许最近添加了这个功能,但我怀疑)。

顺便说一下,有了mingw项目和ActivePerl的objdump,或者Cygwin,你也可以在Windows上以及其他编译器生成的二进制文件上进行类似操作。


5
更新:Michael Greene在下面指出,GCC 4.6针对C语言已经有了-fstack-usage选项:gcc.gnu.org/gcc-4.6/changes.html;-fstack-usage如shodanex的答案所述:https://dev59.com/vXVC5IYBdhLWcg3w-mZO#126490。 - Blaisorblade

8
StackAnlyser似乎检查可执行代码本身以及一些调试信息。根据这个回答所描述的,正是我所需要的,对我来说,堆栈分析器似乎有些过度了。类似于ADA的东西也可以。看一下gnat手册中的这个页面:
22.2 静态堆栈使用分析
使用-fstack-usage编译的单元将生成一个额外的文件,该文件按函数基础指定最大堆栈使用量。该文件与目标对象文件具有相同的基本名称,但扩展名为.su。此文件的每一行由三个字段组成:
* The name of the function.
* A number of bytes.
* One or more qualifiers: static, dynamic, bounded. 

第二个字段对应于函数框架已知部分的大小。
限定符static表示函数框架大小是纯静态的。通常意味着所有局部变量都具有静态大小。在这种情况下,第二个字段是函数堆栈利用率的可靠度量。
限定符dynamic表示函数框架大小不是静态的。当一些局部变量具有动态大小时,通常会出现这种情况。当此限定符单独出现时,第二个字段不是函数堆栈分析的可靠度量。当它与bounded限定符一起使用时,它表示第二个字段是函数堆栈利用率的可靠最大值。

6
这是一个老问题,但值得一提的是,GCC 4.6 版本为 C 语言提供了 -fstack-usage 选项:http://gcc.gnu.org/gcc-4.6/changes.html。 - Michael Greene
1
@Michael Greene:这可能是一个答案!谢谢! - shodanex

4

我不明白为什么静态代码分析不能给出足够好的结果。

在任何给定函数中找到所有局部变量是很简单的,每个变量的大小可以通过C标准(对于内置类型)或通过计算(对于结构体和联合等复杂类型)来找到。

当然,答案不能保证100%准确,因为编译器可以执行各种优化,例如填充、将变量放入寄存器或完全删除不必要的变量。但它提供的任何答案至少应该是一个很好的估计。

我进行了快速的谷歌搜索,发现 StackAnalyzer,但我猜其他静态代码分析工具也有类似的功能。

如果您想要100%准确的数字,则必须查看编译器的输出或在运行时检查它(如Ralph在他的回复中建议的那样)。


1
StackAnalyzer看起来不错,但它不能完成所需的工作,因为它分析的是可执行文件,而不是源代码。虽然在理论上编写这样的工具应该是可能的,但我认为并不存在这样的工具——基于运行时或汇编检查堆栈使用情况更加实际。 - Suma
我只说一个函数名,你就知道为什么静态代码分析不足以获取函数使用的堆栈空间:alloca。一旦有一个函数使用它(带有非常量值),你就无法得到它。另外一个已经提到的问题是递归。 - flolo

1

只有编译器才真正知道,因为它是将所有内容组合在一起的工具。你需要查看生成的汇编代码,看看在前导部分保留了多少空间,但这实际上不能解释类似于alloca这样在运行时处理的东西。


理论上,静态代码分析工具(如lint)可以胜任这项工作,但实际上我认为它们并不能完全胜任。 - Ilya

1

假设您正在使用嵌入式平台,您可能会发现您的工具链可以处理这个问题。好的商业嵌入式编译器(例如Arm/Keil编译器)通常会生成堆栈使用情况报告。

当然,中断和递归通常超出了它们的能力范围,但如果有人在堆栈上犯了一些可怕的错误,比如使用了多兆字节的缓冲区,它可以给您一个大致的想法。


1

虽然不是“编译时”,但我会将其作为后构建步骤执行:

  • 让链接器为您创建一个映射文件
  • 对于映射文件中的每个函数,读取可执行文件的相应部分,并分析函数序言。

这类似于StackAnalyzer所做的事情,但要简单得多。我认为分析可执行文件或反汇编是您可以获得编译器输出的最简单方法。虽然编译器在内部知道这些东西,但恐怕您无法从中获取它(您可以要求编译器供应商实现该功能,或者如果使用开源编译器,则可以自己完成或让其他人为您完成)。

要实现此操作,您需要:

  • 能够解析映射文件
  • 了解可执行文件的格式
  • 知道函数序言可能看起来像什么,并能够“解码”它

这是否容易或困难取决于您的目标平台。(嵌入式?哪种CPU架构?什么编译器?)

所有这些都绝对可以在x86 / Win32中完成,但如果您从头开始创建所有这些并且从未做过任何类似的事情,则可能需要几天时间才能完成并获得可工作的东西。


0

一般情况下不行。在理论计算机科学中,停机问题表明你甚至无法预测一个通用程序在给定输入上是否会停止。计算通用程序运行时使用的堆栈将更加复杂。所以:不行。也许在特殊情况下可以。

假设你有一个递归函数,其递归级别取决于可以是任意长度的输入,那么你已经没有运气了。


你在谈论进程栈,而不是每次调用函数将使用多少。 - shodanex
停机问题并不能阻止静态分析(例如在编译器中),并通过查看程序文本来给出近似答案。事实上,一个大的挑战是计算两个不同的程序何时等效,因此,在静态分析下,两个等效的程序可能会给出不同的结果。 - Blaisorblade

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接