【问题标题】:Why does GCC not assign the static variable when it is initialized to 0为什么GCC初始化为0时不给静态变量赋值
【发布时间】:2019-12-23 21:56:36
【问题描述】:

我将一个静态变量初始化为0,但是当我看到汇编代码时,我发现只有内存分配给了该变量。值未赋值
而当我将静态变量初始化为其他数字时,可以发现内存被赋值了。
我猜GCC是否认为在我们使用内存之前应该由OS将内存初始化为0。

我使用的 GCC 选项是“gcc -m32 -fno-stack-protector -c -o”

当我将静态变量初始化为0时,c代码和汇编代码:

static int temp_front=0;
.local  temp_front.1909
.comm   temp_front.1909,4,4

当我将它初始化为其他数字时,代码是:

static int temp_front=1;
    .align 4
    .type   temp_front.1909, @object
    .size   temp_front.1909, 4
temp_front.1909:
    .long   1

【问题讨论】:

  • 全局变量默认初始化为零。编译器无事可做。
  • @PaulOgilvie 如果没有人这样做,就什么也做不了。充其量,您的“解释”缺乏信息。
  • @Deduplicator, OK:未初始化的全局变量放在 .BSS 段中。加载程序为 .BSS 段分配内存并将其初始化为零。
  • 加载程序或引导程序归零 .bss。这个问题清楚地表明了 .data 与 .bss 的关系,这是正确的。

标签: c gcc assembly static


【解决方案1】:

即使在 Windows 95 中,也没有必要在每个编译模块的代码中进行零初始化。可能是 Win95 程序加载器(甚至 MS-DOS)没有初始化 bss 部分,而是“ctr0”初始化模块(链接在每个编译的 C/C++ 程序中,最终将调用 main() 或 DllEntry 点,可以直接在整个 BSS 部分的快速操作中执行此操作,其大小已经在程序头上,也可以在静态预初始化变量中确定,其值由链接器计算,并且无需更改每个的方式模块是用 gcc 编译的。

但是关于自动变量(分配在堆栈上的局部变量)有更多的困难:如果变量第一次使用是通过调用参数中的引用(对于非内联函数,可能在另一个模块中单独编译或从外部库或 DLL 链接),应该填充它。

GCC 只知道何时在函数本身中显式分配变量,但如果它仅通过引用使用,GCC 现在可以将其预初始化为零,以防止它保留堆栈中的敏感值。在这种情况下,这会在这些局部变量的编译函数前导码中添加一些零填充代码,这有助于防止一些数据泄漏(通常当变量是简单类型时不太可能发生这种泄漏,但当它是一个完整的结构时,很多子调用可能会使字段处于随机状态。

C11 表明此类假设自动变量初始化的代码具有“未定义”行为。但是 GCC 将有助于消除安全风险:这是 C11 允许的,因为这种强制归零比留下随机值更好,并且两种行为都符合“未定义”行为:零与随机泄露的值一样可以接受。

一些安全函数也避免在返回时留下敏感数据,它们明确清除不再需要的变量以避免在这些函数返回后暴露它们(特别是当它们从特权代码返回到非特权代码时):这是一个好的做法,但它与子调用中的引用在初始化之前使用的自动变量的强制初始化无关。并且 GCC 足够聪明,不会在有显式代码为它们分配显式值时强行初始化这些自动变量。所以影响很小。对于那些希望在性能方面进行微优化的应用程序,此功能可能在 GCC 中被禁用,但在这两种情况下,这都不会增加 BSS 大小,并且 Linux 内核的图像大小仅增长

这对“未初始化”的静态变量没有影响,GCC 放入 BSS 部分,由操作系统的程序加载器或小程序的 crt0 init 模块清除。

【讨论】:

  • 如果您提到您正在谈论的 GCC 选项,这将是一个更好的答案,因此人们可以在 GCC 手册中查找它们或在godbolt.org 上尝试它们,看看它们有什么实际区别.
【解决方案2】:

TL:DR:GCC 知道 BSS 保证在其目标平台上进行零初始化,因此它将零初始化的静态数据放在那里。

大图

大多数现代操作系统的程序加载器为程序的每个部分(如数据部分)提供两种不同的大小。它获得的第一个大小是存储在可执行文件中的数据大小(如 Windows 上的 PE/COFF .EXE 文件或 Linux 上的 ELF 可执行文件),而第二个大小是程序在内存中数据部分的大小正在运行。

如果正在运行的程序的数据大小大于存储在可执行文件中的数据量,则数据部分的剩余部分将由包含零的字节填充。在您的程序中,.comm 行告诉链接器保留 4 个字节而不初始化它们,以便操作系统在启动时对它们进行零初始化。

gcc 是做什么的?

gcc(或任何其他 C 编译器)在 .bss 部分分配具有静态存储持续时间的零初始化变量。该部分中分配的所有内容将在程序启动时进行零初始化。对于分配,它使用comm 指令,它只指定大小(4 字节)。

您可以使用 size 命令查看主要部分类型(代码、数据、bss)的大小。如果用 1 初始化变量,它会包含在 data 部分中,并在那里占用 4 个字节。如果您将其初始化为零(或根本不初始化),则会在 .bss 部分中分配它。

ld 是做什么的?

ld 将所有目标文件的所有数据类型部分(甚至来自静态库的文件)合并到一个数据部分,然后是所有 .bss-type 部分。可执行输出包含操作系统程序加载器的简化视图。对于 ELF 文件,这是“program header”。您可以使用objdump -p 来查看任何格式,或使用readelf 来查看ELF 文件。

程序头包含不同类型的条目。其中有几个类型为PT_LOAD 的条目描述了操作系统要加载的“段”。这些 PT_LOAD 条目之一用于数据区域(链接.data 部分)。它包含一个名为p_filesz 的条目,它指定在ELF 文件中为初始化变量提供了多少字节,以及一个名为p_memsz 的条目告诉加载器应该在地址空间中保留多少空间。关于哪些部分合并到哪些 PT_LOAD 条目中的详细信息在链接器之间有所不同并取决于命令行选项,但通常您会发现一个 PT_LOAD 条目描述了一个既可读又可写但不可执行的区域,并且具有 p_filesz小于p_memsz 条目的值(如果只有.bss,则可能为零,没有.data 部分)。 p_filesz 是所有读写数据段的大小,而p_memsz 更大,也为零初始化变量提供空间。

p_memsz 超过 p_filesz 的数量是链接到可执行文件的所有 .bss 部分的总和。 (由于与页面或磁盘块对齐,这些值可能会有所偏差)

请参阅the System V ABI specification 中的第 5 章,尤其是第 5-2 和 5-3 页以了解程序头条目的说明。

操作系统是做什么的?

Linux 内核(或其他符合 ELF 的内核)迭代程序头中的所有条目。对于包含PT_LOAD 类型的每个条目,它分配虚拟地址空间。它将该地址空间的开头与可执行文件中的相应区域相关联,如果该空间是可写的,则启用写时复制。

如果p_memsz 超过p_filesz,内核会将剩余的地址空间安排为完全清零。所以 gcc 在.bss 部分分配的变量最终会出现在 ELF 文件中读写 PT_LOAD 条目的“尾部”中,内核提供零。

任何没有后备数据的整页都可以从映射到零的共享物理页的写时复制开始。

【讨论】:

  • 很好解释。如果您可以轻松安排它,这就是您希望所有位为零作为正确初始化值的原因。即使对于非静态对象,归零通常也更便宜。
  • 我发现这是一个令人困惑的答案:“如果正在运行的程序的数据大小大于存储在可执行文件中的数据量。”请注意,该程序尚未运行。它正在加载。请查看我对该问题的新评论,这是唯一正确的解释。
  • 您能否提供任何文档或其他参考来说明加载程序获取可执行文件中存储的数据的总大小和大小并用零填充差异?我的理解是,一个可执行文件通常包含对多个段的描述,而不仅仅是两个(只读可执行代码、只读数据、可写数据、要初始化为零的数据,以及这些类型的可能附加段和其他)。然后加载程序处理每个段。它提供零不是为了填补空白,而是因为有一段明确的零。
  • @EricPostpischil 虽然描述被简化,但或多或​​少是发生了什么。可执行文件中的每个部分 (PECOFF) 或程序段 (ELF) 在磁盘上都有一个大小,在内存中也有一个大小。如果后面的大小大于前者,则内存中的部分/段用零填充。 sco.com/developers/gabi/latest/… "通常,这些未初始化的数据位于段的末尾,从而使 p_memsz 大于相关程序头元素中的 p_filesz。"
  • @EricPostpischil 您对用于链接的部分的描述是正确的,即 ELF 文件中的部分标题。我的描述适用于运行程序时的加载过程,即 ELF 文件中的程序头。我扩展了我的答案来谈论两者。
【解决方案3】:

为什么 GCC 不分配...

大多数现代操作系统会自动将 BSS 部分初始化为零。

使用这样的操作系统,“未初始化”变量与初始化为零的变量相同。

但是,有一个区别:未初始化变量的数据不存储在生成的对象和可执行文件中;初始化变量的数据是。

这意味着与未初始化的变量相比,“真正的”零初始化变量可能会导致更大的文件大小。

因此,如果变量确实是零初始化的,编译器更喜欢使用“未初始化”变量。

我使用的 GCC 选项是 ...

当然,也有操作系统不会自动将“未初始化”内存初始化为零。

据我所知,Windows 95 就是一个例子。

如果你想为这样的操作系统编译,你可以使用 GCC 命令行选项-fno-zero-initialized-in-bss。此命令行选项强制 GCC “真正地”对零初始化的变量进行零初始化。

我刚刚使用该命令行选项编译了您的代码;输出如下所示:

    .data
    .align 4
    .type     temp_front, @object
    .size     temp_front, 4
 temp_front:
    .zero  4

【讨论】:

    猜你喜欢
    • 2020-06-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-01
    • 2011-12-23
    • 1970-01-01
    • 1970-01-01
    • 2011-01-06
    相关资源
    最近更新 更多