C语言中奇怪的栈行为

10

我担心自己对 C 语言中的栈行为存在误解。

假设我有以下代码:

int main (int argc, const char * argv[]) 
{
    int a = 20, b = 25;
    {
        int temp1;
        printf("&temp1 is %ld\n" , &temp1);
    }

    {
        int temp2;
        printf("&temp2 is %ld\n" , &temp2);
    }
    return 0;
}
为什么我的打印输出没有得到相同的地址?我发现temp2与temp1之间只相差了一个整数,就好像从未回收过temp1一样。
我期望堆栈中应该包含20和25。然后将temp1放在堆栈顶部,将其移除,再将temp2放在堆栈顶部,最后将其移除。
我正在Mac OS X上使用gcc编译器。
注意,我正在使用-O0标志进行编译,以避免优化。
对于那些想了解本问题背景的人:我正在准备C语言教材,试图向学生展示他们不仅应该避免从函数返回自动变量的指针,还应该避免在嵌套块中获取变量的地址并在外部进行解引用。我试图演示这会导致问题,但无法获得截图。

编译器是否设置为使用优化等功能? - Marc Gravell
在MSVC上使用/O2会使它们具有相同的地址。没有优化,它们之间相差4个字节,正如你所观察到的那样。因此至少在这里,优化似乎实现了你想要的行为。 - Joey
为什么它们在没有优化的情况下会分开?我想优化是提前为两者分配空间的方法吧? - Uri
当我使用 -O1 和 -O2 时,这确实得到了解决。我猜想我期望 -O0 可以消除任何优化,因此可以按块基础构建堆栈帧。 - Uri
6个回答

19
编译器完全有权将temp1temp2优化为不同的位置。自从编译器一次生成一条堆栈操作指令以来,已经过去了很多年;现在整个堆栈框架一次性布局。(几年前,我和同事想出了一个特别聪明的方法。) 像你的例子一样,生命周期不重叠时,朴素的堆栈布局可能会将每个变量放在自己的插槽中。如果您好奇,可以尝试使用gcc -O1gcc -O2获取不同的结果。

这是一个好答案。我假设(不确定为什么)如果我使用 -O0 标志,它实际上会给我一定的保证(基于“教科书行为”)。 - Uri
Uri,使用-O0,你观察到的行为恰好是我所预期的:每个变量都有自己的位置,编译器不会尝试合并它们。如果在更高的优化级别下观察到相同的情况,我会感到失望。 - Rob Kennedy
是的,编译器可以自由地做它想做的事情。堆栈布局 - 或者说缺乏布局 - 如果编译器能够证明函数不会被间接递归调用,这些变量在理论上可以放在静态内存中。这完全取决于编译器。 - jakobengblom2

4

无论声明的顺序如何,栈对象将收到什么地址是没有保证的。

编译器可以愉快地重新排序栈变量的创建和持续时间,只要不影响函数的结果即可。


这是一个很好的答案。我假设(不确定为什么),如果我使用 -O0,它实际上会给我一定的保证(基于“教科书行为”)。 - Uri

4

我相信C标准只是讨论在一个块中定义的变量的作用域和生命周期。它不保证这些变量如何与堆栈交互,或者堆栈是否存在。


2
我记得曾经读到过这方面的内容。现在我只有这个不太清楚的链接
引用如下: 只是为了让大家知道(并且为了档案记录),我们的内核扩展似乎遇到了GCC已知的限制。简单地说,我们有一个非常便携、非常轻量级的库中的一个函数,由于某种原因,在编译Darwin时会使用1600多字节的堆栈。无论我尝试什么编译器选项,使用什么优化级别,堆栈都不小于1400,在相当可重复(但不频繁)的情况下会出现“机器检查”崩溃。
在网上搜索了很多资料后,学习了一些i386汇编语言,并与一些更擅长汇编语言的人交谈后,我得知GCC在堆栈分配方面有点臭名昭著。据说这是gcc的肮脏小秘密,虽然对一些人来说并不算什么秘密——Linus Torvalds在各种列表上多次抱怨过gcc的堆栈使用情况(在lkml.org上搜索“gcc stack usage”)。一旦我知道了要搜索什么,就有很多对于gcc的堆栈变量分配不足的抱怨,特别是它无法重用不同作用域中的变量的堆栈空间。
话虽如此,我的Linux版本的gcc可以正确地重用堆栈空间,我得到了相同的地址。不确定C标准对此有何规定,但在C++中,严格的作用域执行仅对代码正确性很重要(由于作用域结束时的销毁),而在C中则不重要。

如何将事物放入内存是编译器应该隐藏并按照自己的方式处理的实现细节,而不是标准所涉及的任何内容。 C标准是一本规则书,编译器会在这些规则内尽其所能。 - jakobengblom2

1

没有标准规定变量在堆栈中的位置。编译器中发生的事情要复杂得多。在您的代码中,编译器甚至可以选择完全忽略和抑制变量ab

在编译器的许多阶段中,代码可能会被转换为其SSA形式,在这种形式下,所有堆栈变量都失去了它们的地址和意义(这甚至可能使调试器更难)。

堆栈空间非常便宜,因为分配2个或20个变量的时间是恒定的。对于大多数函数调用来说,堆栈空间非常动态,因为除了一些函数(那些靠近main()和线程入口函数,具有长期事件循环等)外,它们 tend to complete quickly. 所以,你不需要担心它们。


0

这完全取决于编译器以及它的配置。


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