C 中相同栈帧对象的内存分配

Same-stack-frame objects' memory allocation in C

两个小时前,我认为我已经完全理解堆栈的工作原理(至少它是如何在 C 中处理的)。但是我开始注意到我的程序中有一些意想不到的(对我来说)行为。

我们知道堆栈向较低的内存地址增长(我说的是 PC,在我的例子中:Intel 64 位,Ubuntu)。因此,当创建一个新的堆栈帧时,属于该帧的对象的内存地址低于所有之前的对象。令我感到惊讶的是:框架内的对象声明的越晚,内存地址就越高。这让我非常震惊,因为我认为较早声明的变量可以获得更高的内存地址。

让我用 C 中的示例来说明我的意思。

#include <stdio.h>

void foo()
{
    int firstVar = 1;
    int secondVar = 2;
    printf("firstVar is at: %p\n", &firstVar);
    printf("secondVar is at: %p\n", &secondVar);
}

int main(void)
{
    int mainVar = 0;
    printf("mainVar is at: %p\n", &mainVar);
    foo();
    return 0;
}

使用 gcc(-g、-ansi 和 -pedantic 标志)编译后,输出为:

mainVar is at: 0x7ffd1ec0fadc
firstVar is at: 0x7ffd1ec0fab8
secondVar is at: 0x7ffd1ec0fabc

正如预期的那样,mainVar 的内存地址高于 foo() 堆栈帧中的内存地址。但是,firstVar 的内存地址比 secondVar 低,即使它之前已声明。查看 foo() 的反汇编显示了这种行为:

0x000000000040052d <+0>:    push   %rbp
0x000000000040052e <+1>:    mov    %rsp,%rbp
0x0000000000400531 <+4>:    sub    [=12=]x10,%rsp
0x0000000000400535 <+8>:    movl   [=12=]x1,-0x8(%rbp)
0x000000000040053c <+15>:   movl   [=12=]x2,-0x4(%rbp)
...

1 放在 2 之前四个字节,再次表明 firstVar 的内存地址低于 secondVar.

我的问题是:这是为什么?根据我读过的所有参考书目,同一个堆栈帧中的对象应该有更高的内存地址,它们越早声明。参考书目是指互联网(例如本网站)和知名书籍。我使用的是一个非常标准的系统,所以我怀疑是否存在任何 ELF 或 ABI 奇怪的东西......

有什么想法吗?感谢阅读。

您使用的是哪个编译器?编译器是非常复杂的程序。另外,他们比你更了解 C ;-)(这对我来说是件好事!) 在任何情况下,他们都没有义务遵循您的陈述顺序。你的编译设置是什么?您是针对速度还是尺寸进行优化?我假设 none?

可能发生的情况是,由于您首先使用了 firstVar(在 printf 函数中,编译器决定将 secondVar 定位在 firstVar 之上。堆栈内存对于 firstVar(在 secondVar 的堆栈内存之前再次释放)可以更快更容易地重用,如果有需要的话。

如果交换函数 foo 中的前两行会发生什么?

According to all the bibliography I've read, objects within the same stack frame should have higher memory addresses the earlier they where declared

局部变量在堆栈中的放置顺序绝不是标准化的,堆栈帧本身的格式也是如此。编译器可以随意分配局部变量,因为它不会影响函数之外的任何东西。 除非将变量返回给调用者,但这里不是这样。

一个观察:

gcc 无优化:

mainVar is at:   000000000022FE4C
firstVar is at:  000000000022FE0C
secondVar is at: 000000000022FE08

gcc -O3 全面优化:

mainVar is at:   000000000022FE4C
firstVar is at:  000000000022FE08
secondVar is at: 000000000022FE0C

无论出于何种原因,优化器认为更改这两个变量的分配顺序会有好处。要知道原因,您必须详细研究特定编译器的优化器。而且是比较有用的知识。

您在这里看不到的是,优化器可能喜欢将这些变量放入 CPU 寄存器中。但是没有办法,因为您正在打印它们的地址并且寄存器变量没有地址。通过使用变量的地址,您强制它在堆栈上分配。

所以这里要学习的唯一重要的事情是你不应该编写依赖于堆栈帧内存布局的代码,也不应该对 C 标准不能保证的内存布局做出任何假设。

如果您需要特定的顺序,您需要向编译器展示 C 标准:

typedef struct
{
  int firstVar;
  int secondVar;
} reorder_this_if_you_can;

void foo()
{
    reorder_this_if_you_can re;

    printf("firstVar is at:\t %p\n", &re.firstVar);
    printf("secondVar is at: %p\n", &re.secondVar);
}

现在无论优化级别如何突然保证顺序。