为什么GCC在初始化为0时不分配静态变量

时间:2019-08-18 10:58:00

标签: c gcc assembly static

我将静态变量初始化为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

3 个答案:

答案 0 :(得分:9)

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类型节。可执行输出包含操作系统程序加载器的简化视图。对于ELF文件,这是“ program header”。您可以使用objdump -p(对于任何格式)或readelf(对于ELF文件)进行查看。

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

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

有关程序头条目的说明,请参见the System V ABI specification中的第5章,特别是第5-2和5-3页。

操作系统做什么?

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

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

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

答案 1 :(得分:2)

  

为什么不分配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

答案 2 :(得分:0)

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

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

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

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

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

这对GCC放置在BSS部分中的“未初始化”静态变量没有影响,该变量由OS的程序加载器或小程序的crt0 init模块清除。

相关问题