【问题标题】:Same-stack-frame objects' memory allocation in CC中相同堆栈帧对象的内存分配
【发布时间】:2015-12-18 23:30:45
【问题描述】:

两个小时前,我以为我已经完全理解了堆栈的工作原理(至少在 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    $0x10,%rsp
0x0000000000400535 <+8>:    movl   $0x1,-0x8(%rbp)
0x000000000040053c <+15>:   movl   $0x2,-0x4(%rbp)
...

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

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

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

【问题讨论】:

  • 您无法控制数据的布局。编译器可以以它选择的任何方式对堆栈上的变量进行排序。有时,他们会在简单变量之后移动数组,这样数组溢出就不会影响到简单变量。有时它不会为变量分配任何堆栈存储空间;他们将只生活在寄存器中。但是你无法预测它——除了对汇编程序进行逆向工程(并且在下次编译代码时该分析可能无效)。
  • 注意 sub $0x10,%rsp,它们在堆栈上分配空间。这是他们将放置整数的本地分配的空间。此时,布局取决于编译器,并且在大多数编译器(和汇编器)中,这会解析为“自上而下的图表”,即按照文本的写入顺序。但是,对齐问题和其他考虑因素会导致优化器根据复杂的规则对该区域进行排序,因此并非总是如此。它是 rsp 上的 sub 以您期望的顺序和方向工作..但是打开的区域更加“手动”组织
  • 这些 cmets 很短……还有一点。如果它们是 PUSHES,那么您是正确的……它们会按相反的顺序出现,但事实并非如此。该区域由 rsp 上的 sub 打开,因此此时它只是一块 RAM,并且可以使用编译器喜欢的任何方式。
  • cdecl 定义了 C 函数调用协议的布局。对于 cdecl,参数按在函数中声明的相反顺序推送。此链接:tenouk.com/Bufferoverflowc/Bufferoverflow2a.html> 详细介绍。注意:编译器不会重新安排参数的顺序超出“反向排序”谷歌cdecl 调用约定以获取详细信息/示例。请记住,pascal 和其他语言有不同的约定
  • 这里是cdecl 约定(C 调用约定)的链接,任何兼容的 C/C++ 编译器都必须实现该链接:msdn.microsoft.com/en-us/library/zkwh89ks.aspx> 您可能还想查看stdcall WINAPI 使用的约定 msdn.microsoft.com/en-us/library/zxk0tw93.aspx>

标签: c memory stack


【解决方案1】:

根据我读过的所有参考书目,同一堆栈帧中的对象应该越早声明它们的内存地址越高

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

一个观察:

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);
}

现在无论优化程度如何,订单都会突然得到保证。

【讨论】:

  • 我无法让编译器恢复任何优化级别的分配顺序。我真的不介意变量是否不是我阅读它们的方式。令我烦恼的是,使用非常标准的系统和非常标准的编译器,我无法让编译器将变量按“通常”顺序放置。无论如何,我的教训是永远不要在没有检查的情况下相信分配顺序是一个或另一个。
  • @mane95 “通常的顺序”实际上是最不重要的地址,因为这是分配具有其他类型存储持续时间的所有其他变量的方式。在一般的内存分配术语中,向下计数堆栈是一种特殊情况。虽然不太常见,但也有带有向上计数堆栈的 CPU。
  • 非常错误。 google cdecl 了解 C 程序实际使用的内容
  • @user3629249 怎么了?这是一个独立于平台的答案,通常适用于计算机。 C 中没有什么叫 cdecl,这是一个非标准的 PC 调用约定扩展。而且您似乎很困惑:调用约定与局部变量的分配顺序无关,它仅与参数和返回值的分配有关。随时在 SO 上发布一个问题,询问 cdecl 做什么和不做什么,或者,你知道,google cdecl。
【解决方案2】:

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

可能发生的情况是,由于您首先使用firstVar(在printf 函数中,编译器决定将secondVar 定位在firstVar 之上。firstVar 的堆栈内存(之前再次变为空闲)如果需要,secondVar) 的堆栈内存可以更快、更轻松地重用。

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

【讨论】:

  • 我正在使用 gcc。我没有使用特定的优化,尽管尝试使用 O0、O1、O2、O3、Os、Ofast 和 Og 并没有改变顺序。交换前两行确实交换了内存地址。交换 printf 只会交换输出。简而言之,无论我使用什么优化,变量越早声明它们的内存地址就越低。
  • 那我只能说你刚刚发现了gcc的内部解析机制 ;-) 切换编译器,或者切换版本,你可能会得到不同的结果。如果你真的想确保你的变量在内存中按照你想要的方式布局,你必须恢复到编译指示、链接器脚本和/或程序集——或者当然使用结构,这是 Lundin 的一个很好的收获
猜你喜欢
  • 2014-10-26
  • 1970-01-01
  • 2017-11-08
  • 2017-01-04
  • 2021-04-08
  • 2018-07-24
  • 1970-01-01
  • 2011-10-09
  • 2021-03-20
相关资源
最近更新 更多